聰明不如鈍筆
총명불여둔필
assignment KindeR

R로 깔끔하게 누적 개별 개수(합계) 계산하기(feat. accumulate)

이번에도 페이스북 '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 -_-)/

 

데이터 과학 입문서 '친절한 R with 스포츠 데이터'를 썼습니다

어쩌다 보니 '한국어 사용자에게 도움이 될지도 모르는 R 언어 기초 회화 교재'를 세상에 내놓게 됐습니다. 책 앞 부분은 '한국어 tidyverse 사투리' 번역, 뒷부분은 'tidymodels 억양 따라하기'에 초점

kuduz.tistory.com

 

댓글,

KindeR | 카테고리 다른 글 더 보기