이번에도 페이스북 'R Korea - KRSG(Korean R Study Group)' 그룹에서 재미난 문제를 발견했습니다.
먼저 문제부터 읽어보도록 하겠습니다.
그룹별 누적합은 다들 쉽게 구할줄 아실 텐데요
그룹별 누적 distinct count 는 어떨까요...?
실전에서 마주친 문제인데 꽤 흥미로운 퀴즈가 될 것 같아 가져왔습니다 🙂
=============
Q) 주어진 데이터셋을 이용해 주석 처리된 결과처럼 그룹별 누적매출 및 누적고객수를 구해주세요!
library(dplyr)
library(tidyr)
data = tribble( ~user, ~time, ~group, ~revenue, 'A', 1, 'a', 100, 'A', 4, 'a', 200, 'A', 3, 'b', 700, 'A', 2, 'b', 500, 'B', 1, 'a', 1000, 'B', 4, 'b', 300, 'B', 2, 'a', 600, 'C', 1, 'a', 400, 'C', 3, 'a', 100, 'C', 2, 'b', 200, 'C', 1, 'b', 1200 )
## # A tibble: 8 x 4 ## group time c_revenue c_user_count ## <chr> <dbl> <dbl> <int> ## 1 a 1 1500 3 ## 2 a 2 2100 3 ## 3 a 3 2200 3 ## 4 a 4 2400 3 ## 5 b 1 1200 1 ## 6 b 2 1900 2 ## 7 b 3 2600 2 ## 8 b 4 2900 3
조금 더 정확히 말하면 group & time별 누적 매출 및 누적 고객 숫자를 구하는 게 이번 퀴즈 목적입니다.
본격적으로 문제를 풀기 전에 개념 정리를 한 번 하고 가겠습니다.
누적 합계는 이름 그대로 1, 2, 3, 4, 5가 있을 때 1, 3, 6, 10, 15처럼 차례로 더하는 걸 뜻합니다.
그러면 누적 개별 개수(distinct count)는 뭘까요?
c(a, b, c)가 있을 때 b(b, c, d)가 들어와도 6개가 아니라 c(a, b, c, d) = 4개로 세는 걸 뜻합니다.
개념 정리까지 끝났으니 실제로 문제를 풀어보겠습니다.
R 문제 풀이 때마다 말씀드리는 것처럼 해법은 차고 넘칩니다.
이번 포스트는 '아, 이런 방법으로 푸는 방법도 있구나'하고 읽어주시면 됩니다.
일단 제가 생각하는 정답은 이겁니다.
data %>%
group_by(group, time) %>%
summarise(sum_revenue = sum(revenue),
users = user %>% list(),
.groups = 'drop') %>%
group_by(group) %>%
transmute(group,
time,
c_revenue = cumsum(sum_revenue),
c_user_count = users %>% accumulate(c) %>% map_dbl(n_distinct)) %>%
ungroup()
이제부터 어떻게 이런 결론에 도달하게 됐는지 하나 하나 짚어 보도록하겠습니다.
그 전에 최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr), R에서 깔끔하게 반복 작업 처리하기(feat. purrr) 포스트를 읽어 보시면 이번 글을 이해하시는 데 도움이 될 수 있습니다.
이번에도 늘 그렇듯 일단 tidyverse 패키지 (설치하고) 불러오는 걸로 시작합니다.
library('tidyverse')
## -- Attaching packages --------------------------------------- tidyverse 1.3.1 --
## v ggplot2 3.3.5 v purrr 0.3.4 ## v tibble 3.1.5 v dplyr 1.0.7 ## v tidyr 1.1.4 v stringr 1.4.0 ## v readr 2.0.2 v forcats 0.5.1
## -- Conflicts ------------------------------------------ tidyverse_conflicts() -- ## x dplyr::filter() masks stats::filter() ## x dplyr::lag() masks stats::lag()
출제자께서 말씀하신 것처럼 누적합은 cumsum() 함수를 쓰면 간단하게 계산 가능합니다.
예컨대 1부터 10까지 차례로 더하는 코드는 이렇게 쓰면 됩니다.
1:10 %>% cumsum()
## [1] 1 3 6 10 15 21 28 36 45 55
purrr 패키지에 들어 있는 accumulate() 함수를 활용해도 같은 결과를 얻을 수 있습니다.
1:10 %>% accumulate(sum)
## [1] 1 3 6 10 15 21 28 36 45 55
이 코드는 합계를 구하는 sum() 함수를 누적해서 적용하라는 뜻입니다.
물론 문자 데이터에도 특정 함수를 누적 적용할 수 있습니다.
R에는 로마자 소문자가 들어 있는 letters 객체가 들어 있습니다.
letters[3:1]
## [1] "c" "b" "a"
벡터 또는 리스트로 자료를 묶어주는 c() 함수를 이 데이터에 적용하면 이런 결과가 나옵니다.
letters[3:1] %>% accumulate(c)
## [[1]] ## [1] "c" ## ## [[2]] ## [1] "c" "b" ## ## [[3]] ## [1] "c" "b" "a"
물론 이 자료를 데이터 프레임에 넣는 것도 가능합니다.
리스트를 데이터 프레임 셀에 넣으려면 list() 함수를 써서 이 데이터가 리스트라고 알려줘야 합니다.
그리고 이 함수를 각 행별로 적용해야 하기 때문에 데이터 프레임 다음에 rowwise() 함수를 먼저 씁니다.
tibble(
x = 3:1
) %>%
rowwise() %>%
mutate(y = letters[1:x] %>% list())
## # A tibble: 3 x 2 ## # Rowwise: ## x y ## <int> <list> ## 1 3 <chr [3]> ## 2 2 <chr [2]> ## 3 1 <chr [1]>
y열 안에 어떤 데이터가 들어 있는지 궁금하시죠?
특정 열에 어떤 데이터가 들어 있는지 속살을 확인해 보고 싶다면 pull() 함수를 쓰면 됩니다.
tibble(
x = 3:1
) %>%
rowwise() %>%
mutate(y = letters[1:x] %>% list()) %>%
pull(y)
## [[1]] ## [1] "a" "b" "c" ## ## [[2]] ## [1] "a" "b" ## ## [[3]] ## [1] "a"
이제 y 열에 accumulate() 함수를 적용해 보도록 하겠습니다.
rowwise() 함수도 행 단위로 자료 그룹을 만드는 형태라 ungroup() 함수를 한 번 써줍니다.
pull() 함수까지 써 보면 자료가 차례 차례 잘 쌓여 있다는 걸 확인할 수 있습니다.
tibble(
x = 3:1
) %>%
rowwise() %>%
mutate(y = letters[1:x] %>% list()) %>%
ungroup() %>%
mutate(z = accumulate(y, c)) %>%
pull(z)
## [[1]] ## [1] "a" "b" "c" ## ## [[2]] ## [1] "a" "b" "c" "a" "b" ## ## [[3]] ## [1] "a" "b" "c" "a" "b" "a"
이제 서로 다른(distinct) 데이터가 몇 개나 들어 있는지 세기만 하면 됩니다.
이때는 n_distinct() 함수를 쓰면 끝입니다.
이 작업을 반복해야 하니까 purrr 패키지 기본 함수인 map() 안에 넣습니다.
tibble(
x = 3:1
) %>%
rowwise() %>%
mutate(y = letters[1:x] %>% list()) %>%
ungroup() %>%
mutate(z = accumulate(y, c) %>% map(n_distinct)) %>%
pull(z)
## [[1]] ## [1] 3 ## ## [[2]] ## [1] 3 ## ## [[3]] ## [1] 3
여기서 우리에게 필요한 건 숫자뿐이니까 map()을 map_dbl()으로 바꿔봅니다.
tibble(
x = 3:1
) %>%
rowwise() %>%
mutate(y = letters[1:x] %>% list()) %>%
ungroup() %>%
mutate(z = accumulate(y, c) %>% map_dbl(n_distinct))
## # A tibble: 3 x 3 ## x y z ## <int> <list> <dbl> ## 1 3 <chr [3]> 3 ## 2 2 <chr [2]> 3 ## 3 1 <chr [1]> 3
잘 나왔습니다. 이제 이 원리를 원래 문제에 있던 데이터에 그대로 적용하면 됩니다.
먼저 데이터를 data라는 객체에 넣고,
tribble(
~user, ~time, ~group, ~revenue,
'A', 1, 'a', 100,
'A', 4, 'a', 200,
'A', 3, 'b', 700,
'A', 2, 'b', 500,
'B', 1, 'a', 1000,
'B', 4, 'b', 300,
'B', 2, 'a', 600,
'C', 1, 'a', 400,
'C', 3, 'a', 100,
'C', 2, 'b', 200,
'C', 1, 'b', 1200
) -> data
group_by() 함수로 그룹을 만든 다음 필요한 값을 계산해 봅니다.
data %>%
group_by(group, time) %>%
summarise(sum_revenue = sum(revenue),
users = user %>% list())
## `summarise()` has grouped output by 'group'. You can override using the `.groups` argument.
## # A tibble: 8 x 4 ## # Groups: group [2] ## group time sum_revenue users ## <chr> <dbl> <dbl> <list> ## 1 a 1 1500 <chr [3]> ## 2 a 2 600 <chr [1]> ## 3 a 3 100 <chr [1]> ## 4 a 4 200 <chr [1]> ## 5 b 1 1200 <chr [1]> ## 6 b 2 700 <chr [2]> ## 7 b 3 700 <chr [1]> ## 8 b 4 300 <chr [1]>
데이터 프레임 위에 뜨는 상태 메시지는 summarise() 함수 안에 .groups = 'drop' 옵션을 주면 없앨 수 있습니다.
일단 계산하기 쉬운 누적 합계부터 계산합니다.
data %>%
group_by(group, time) %>%
summarise(sum_revenue = sum(revenue),
users = user %>% list(),
.groups = 'drop') %>%
group_by(group) %>%
mutate(c_revenue = cumsum(sum_revenue))
## # A tibble: 8 x 5 ## # Groups: group [2] ## group time sum_revenue users c_revenue ## <chr> <dbl> <dbl> <list> <dbl> ## 1 a 1 1500 <chr [3]> 1500 ## 2 a 2 600 <chr [1]> 2100 ## 3 a 3 100 <chr [1]> 2200 ## 4 a 4 200 <chr [1]> 2400 ## 5 b 1 1200 <chr [1]> 1200 ## 6 b 2 700 <chr [2]> 1900 ## 7 b 3 700 <chr [1]> 2600 ## 8 b 4 300 <chr [1]> 2900
이어서 사용자 누적 인원도 계산해 봅니다.
data %>%
group_by(group, time) %>%
summarise(sum_revenue = sum(revenue),
users = user %>% list(),
.groups = 'drop') %>%
group_by(group) %>%
mutate(c_revenue = cumsum(sum_revenue),
c_user_count = users %>% accumulate(c) %>% map_dbl(n_distinct))
## # A tibble: 8 x 6 ## # Groups: group [2] ## group time sum_revenue users c_revenue c_user_count ## <chr> <dbl> <dbl> <list> <dbl> <dbl> ## 1 a 1 1500 <chr [3]> 1500 3 ## 2 a 2 600 <chr [1]> 2100 3 ## 3 a 3 100 <chr [1]> 2200 3 ## 4 a 4 200 <chr [1]> 2400 3 ## 5 b 1 1200 <chr [1]> 1200 1 ## 6 b 2 700 <chr [2]> 1900 2 ## 7 b 3 700 <chr [1]> 2600 2 ## 8 b 4 300 <chr [1]> 2900 3
여기까지 왔으면 사실 ungroup() 함수만 한 번 쓰면 끝이라고 할 수 있습니다.
transmute() 함수를 써서 아예 출제자께서 제시한 것과 똑같은 디자인(?)으로 만들겠습니다.
mutate()는 이미 있는 데이터에 새로운 열을 더하라는 함수고 transmute는 계산 결과만 뽑아내라는 함수라고 이해하시면 됩니다.
data %>%
group_by(group, time) %>%
summarise(sum_revenue = sum(revenue),
users = user %>% list(),
.groups = 'drop') %>%
group_by(group) %>%
transmute(group,
time,
c_revenue = cumsum(sum_revenue),
c_user_count = users %>% accumulate(c) %>% map_dbl(n_distinct)) %>%
ungroup()
## # A tibble: 8 x 4 ## group time c_revenue c_user_count ## <chr> <dbl> <dbl> <dbl> ## 1 a 1 1500 3 ## 2 a 2 2100 3 ## 3 a 3 2200 3 ## 4 a 4 2400 3 ## 5 b 1 1200 1 ## 6 b 2 1900 2 ## 7 b 3 2600 2 ## 8 b 4 2900 3
아, 생각해 보니까 출제자는 dplyr, tidyr 패키지만 가지고 문제를 해결하라고 하셨네요.
그럴 때는 아래 코드로 같은 결과를 얻을 수 있습니다.
data %>%
select(group, time, revenue, user) %>%
arrange(group, time, user) %>%
group_by(group) %>%
mutate(c_revenue = cumsum(revenue),
c_user_count = cumsum(!duplicated(user))) %>%
group_by(group, time) %>%
filter(row_number() == max(row_number())) %>%
ungroup() %>%
select(-revenue, -user)
## # A tibble: 8 x 4 ## group time c_revenue c_user_count ## <chr> <dbl> <dbl> <int> ## 1 a 1 1500 3 ## 2 a 2 2100 3 ## 3 a 3 2200 3 ## 4 a 4 2400 3 ## 5 b 1 1200 1 ## 6 b 2 1900 2 ## 7 b 3 2600 2 ## 8 b 4 2900 3
물론 이것 말고도 또 다른 방법이 있을 겁니다.
더 재미있는(?) 코드를 찾으셨다면 제게도 알려주셔요.
그럼 모두들 Happy Tidyversing -_-)/
댓글,