R 데이터프레임 행 합계 - r deiteopeuleim haeng habgye

Data Analysis

『R』 결측치 처리 및 열끼리 행끼리 계산하기 (is.na, na.rm)

'결측치'라는 건 비어있다정도로 해석을 하고 있다.

실습데이터들은 대체로 비어있는 것이 없지만, 실제 데이터를 보다보면 결측치는 어마어마하게 많이 존재한다.

결측치는 그냥 비어있는 칸이기 때문에 계산할때 제대로 실행되지 않는 경우가 많다.

첨부되어 있는 파일을 보면 몇 개의 칸은 비어있는 걸 볼 수 있다.

'시험을 안 봤다면 점수가 없고 아예 비어둔다.' 정도로 생각 할 수 있다.

우리가 손쉽게 할 수 있는 덧셈이나 평균을 낼 때 이런 값들이 있다면, 계산불가...

RStudio에서 살펴보자.

#### 결측치 처리 및 열끼리 행끼리 계산하기 ####
# 파일 읽어오기
score = read.csv("score.csv", stringsAsFactors = FALSE)

# 파일 확인하기
score

# 파일을 보면 NA라는 값들이 들어있다. 저걸 결측치라고 한다.

# 실제 엑셀 파일에서는 비어있는 값을 R에서는 NA라고 표기한 것.

# 이대로 계산해보기
# 열 더하기
sum(score$국어) # 국어 열을 모두 더해라.
mean(score$영어) # 영어 열을 평균내라.

# 계산하는 열에 NA값이 존재하면 이런 식으로 계산 불가하다.

# NA를 0으로 바꾸자
# 국어열 NA = 0 으로 바꾸기
score_2 = score
score_2[ is.na(score_2$국어), "국어"] = 0
score_2

# score_2라는 곳에 일단 score를 복사해놓고

# score_2에서 국어 열이 na라면 0을 넣어라. 라는 뜻이다.

# 결과를 보면 국어 열만 NA대신에 0이 들어간 것을 알 수 있다.

# 계산해보기
# 열 더하기
sum(score_2$국어)
mean(score_2$영어)

# 국어열은 NA를 0으로 바꿨기 때문에 계산이 가능하지만

# 영어열은 여전히 NA값이 존재하여 계산 불가능

# 모든 데이터의 결측치를 처리하는 법(0으로 바꾸자)
score_3 = score
score_3[is.na(score_3)] = 0
score_3

# 우선 score_3에 score을 복사하고

# score_3에 있는 모든 NA를 0으로 바꿔라. 라는 뜻이다.

# NA값이 모두 0으로 바뀐 것을 볼 수 있다.

# 계산하기
# 열 더하기
sum(score_3$국어)
mean(score_3$영어)

#### 일일히 열을 계산하기에는 귀찮은 짓이다. 한번에 계산하는 방법 ####

# 열끼리 행끼리 계산하기

colSums(score_3)
rowSums(score_3)

# 이대로 계산하면 에러가 나온다. 왜냐하면 score_3을 보게되면 사람 이름이 적혀있다.

# 합이나, 평균을 구하려하는데 사람이름이 나오면 구할 수가 없기 때문이다.

# 열끼리 행끼리 계산하기

# 그래서 이름란을 뺴야한다. [,-1]이라는 건 첫 번째 열을 삭제한다는 것이다.

score_3 = score_3[,-1]
score_3

# 제거한 상태로 계산하면 아래와 같이 결과가 나온다.

colSums(score_3)
rowSums(score_3)

#### 따로 결측치를 처리하지 않고 계산하는 방법 ####

# 또 다른 방법으로는 colSums의 사용법을 보면 na를 제거하고 계산할 수 있는 사용법이 적혀있다.

# 함수의 기본 사용법과 어떻게 사용하는 지 궁금할 때는 R에 "?함수이름"라고 적으면 친절하게 설명해준다.

?colSums

# 오른쪽 밑에 Help에 창이 뜬다.

# Description을 보면 na.rm = FALSE 가 기본사용법으로 적혀있는 것을 볼 수 있다.

# 이 뜻은 따로 na.rm을 적지 않으면 na.rm  = FALSE로 간주한다는 뜻이다.

# score_4에 복사해주고 이 역시도 사람 이름은 계산할 수 없기 때문에 이름열을 뺴준다.

score_4 = score
score_4 = score_4[,-1]
score_4

# NA값이 그대로 존재하는 것을 볼 수 있다.

# 이대로 colSums와 rowSums를 계산해보자.

colSums(score_4, na.rm = TRUE)
rowSums(score_4, na.rm = TRUE)

# na.rm이란 na remove. 즉, 계산할 때 na값을 제거하고 계산한다는 뜻이다. 

# na값을 처리하지 않아도 na.rm = TRUE 라고 적으면 알아서 0값으로 계산하고 처리해준다.

R 데이터프레임 행 합계 - r deiteopeuleim haeng habgye

이번에도 페이스북 '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 데이터프레임 행 합계 - r deiteopeuleim haeng habgye

개념 정리까지 끝났으니 실제로 문제를 풀어보겠습니다.

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

R 데이터프레임 행 합계 - r deiteopeuleim haeng habgye