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

R로 (깔끔하게) 데이터 구조를 바꾸는 몇 가지 방법


노동절을 보내는 가장 좋은 방법은 역시 노동 아니겠습니까?


노동절 맞이 야근을 하던 중 (이번에도) 페이스북 R 스터디 그룹에 올라온 질문을 뒤늦게 보게 됐습니다.


안녕하세요.

이제야 R을 처음 접하고 있는 사람입니다.


혹시 R에서 데이터 구조를

이런 그림 형태로 바꿀 수 있는 방법이 있을까요?



이 그림을 보고 와이드 폼 vs 롱 폼을 떠올리신 분이 적지 않으실 겁니다.


R에서 이 두 형태 사이를 오갈 때는 기본적으로 tidyr 패키지를 쓰시면 됩니다.


혹시 이런 내용을 처음 접해 보셨다면 '최대한 친절하게 쓴 R로 데이터 깔끔하게 만들기(feat. tidyr)' 포스트가 도움이 될 수도 있습니다.


그런데 질문자께서 요구하시는 건 내용이 조금 다릅니다.


원래 저 와이드 폼을 롱 폼으로 바꾸는 건 데이터를 이렇게 바꾸는 걸 뜻합니다.

## # A tibble: 4 x 3
##   지역  성별   인원
##     
## 1 서울  남자      5
## 2 서울  여자      3
## 3 부산  남자      3
## 4 부산  여자      2


질문자께서는 서울 남자는 다섯 번, 서울 여자는 세 번 하는 식으로 출력하는 방법을 궁금해하고 계십니다.


이럴 때는 어떻게 접근해야 할까요?


답은 금방 올라왔습니다.


이 질문이 처음 올라온 건 4월 28일 오후 9시 20분이었습니다.


이로부터 55분 뒤에 '기본 함수들로 간단하게 함수를 구현해 봤습니다'라는 답글이 붙었습니다.


답변자께서 남기신 코드는 이렇습니다.

wide.format <- data.frame(
  male = c(5, 3),
  female = c(3, 2)
)
rownames(wide.format) <- c('seoul','busan')
wide.format
##       male female
## seoul    5      3
## busan    3      2
unbox <- function(df){
  cn <- colnames(df)
  rn <- rownames(df)
  result <- data.frame()

  for(i in 1:length(rn)){
    for(j in 1:length(cn)){
      temp <- 
        data.frame(
          rep(rn[i], df[i,j]),
          rep(cn[j], df[i,j])
        )
      result <- rbind(result, temp)
    }
  }
  
  colnames(result) <- paste('x', 1:ncol(result), sep='')
  result
}
finish <- unbox(wide.format)
colnames(finish) <- c('region', 'sex')
finish
##    region    sex
## 1   seoul   male
## 2   seoul   male
## 3   seoul   male
## 4   seoul   male
## 5   seoul   male
## 6   seoul female
## 7   seoul female
## 8   seoul female
## 9   busan   male
## 10  busan   male
## 11  busan   male
## 12  busan female
## 13  busan female


위에서 보시는 것처럼 잘 작동합니다. 코드가 이해가 가시나요?


기본적으로 같은 문자(또는 숫자)를 여러 번 반복해 출력하는 rep() 함수를 이용해 코드를 쓰셨습니다.


rep() 함수는 'rep(반복 출력하고 싶은 문자 또는 숫자, 반복 횟수)' 형태로 씁니다.


예를 들어 아래처럼 쓰면 숫자 1을 두 번 출력하게 됩니다.

rep(1, 2)
[1] 1 1


물론 제목에서 말씀드린 것처럼 이렇게 데이터 구조를 바꾸는 방법이 하나만 있는 건 아닙니다.


46분 뒤에 '아 이거 코드 배틀인가요?'라는 멘트와 함께 이런 코드가 등장합니다.

df = data.frame(지역 = c("서울", "부산"),
                             남자 = c(5, 3),
                             여자 = c(3, 2))
df_new = data.frame(성별 = rep(colnames(df)[-1], times = colSums(df[, -1])),
                                      지역 = rep(rep(df[, 1], ncol(df) - 1), times = unlist(df[, -1])))
df_new
##    성별 지역
## 1  남자 서울
## 2  남자 서울
## 3  남자 서울
## 4  남자 서울
## 5  남자 서울
## 6  남자 부산
## 7  남자 부산
## 8  남자 부산
## 9  여자 서울
## 10 여자 서울
## 11 여자 서울
## 12 여자 부산
## 13 여자 부산


이번에도 마찬가지로 역시나 잘 작동합니다.


이번에는 코드 길이가 줄어든 대신 colSums(), unlist() 같은 함수가 추가로 등장했습니다.


어느 방법이 더 좋거나 나쁘다는 말씀을 드리려는 게 아닙니다.


그냥 같은 문제를 해결하는 코드가 여러 종류가 있을 수 있다는 말씀을 드리는 것뿐입니다.


R에 익숙하신 분이라면 짧은 코드를 선호하실 수도 있지만 최종 결과가 나오는 이유를 알기 어려워 코드를 수정하는 데 어려움을 겪을 수도 있습니다.


제가 저 문제를 보고 생각한 코드는 두 답변 사이에 올라온 코드와 가깝습니다.


먼저 세 번째 코드는 이렇게 생겼습니다.

df <- tibble(loc = c('서울', '부산'),
                    남자 = c(5, 3),
                    여자 = c(3, 2))
setup <- gather(df, sex, value, -1)
result <- tibble(성별 = rep(setup$sex, setup$value),
                           장소 = rep(setup$loc, setup$value))
result
## # A tibble: 13 x 2
##    성별  장소 
##    <chr> <chr>
##  1 남자  서울 
##  2 남자  서울 
##  3 남자  서울 
##  4 남자  서울 
##  5 남자  서울 
##  6 남자  부산 
##  7 남자  부산 
##  8 남자  부산 
##  9 여자  서울 
## 10 여자  서울 
## 11 여자  서울 
## 12 여자  부산 
## 13 여자  부산


아, 이 코드는 tidyverse 패키지를 먼저 불러오셔야 작동합니다.

#install.packages('tidyverse')
library('tidyverse')
## -- Attaching packages --------------------------------------------------------------------------------- tidyverse 1.3.0 --
## √ ggplot2 3.2.1     √ purrr   0.3.2
## √ tibble  2.1.3     √ dplyr   0.8.3
## √ tidyr   0.0.3     √ stringr 1.4.0
## √ readr   1.3.1     √ forcats 0.3.0
## -- Conflicts ------------------------------------------------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()


이 블로그를 꾸준히 읽어 오신 분은 아시겠지만 저도 물론 tidyverse를 이용할 겁니다.


그리고 순전히 저 위에 있는 코드와 다른 코드를 쓰고 말겠다는 이유로 이렇게 코드를 쓸 겁니다.

tribble(
  ~지역, ~남자, ~여자,
  '서울', 5, 3,
  '부산', 3, 2
) %>%
  pivot_longer(-지역, names_to='성별', values_to='인원') %>%
  group_by(성별, 지역) %>%
  expand(count=seq(1:인원)) %>%
  arrange(성별, desc(지역)) %>%
  select(-count)
## # A tibble: 13 x 2
## # Groups:   성별, 지역 [4]
##    성별  지역 
##    <chr> <chr>
##  1 남자  서울 
##  2 남자  서울 
##  3 남자  서울 
##  4 남자  서울 
##  5 남자  서울 
##  6 남자  부산 
##  7 남자  부산 
##  8 남자  부산 
##  9 여자  서울 
## 10 여자  서울 
## 11 여자  서울 
## 12 여자  부산 
## 13 여자  부산


이 코드 가운데 arrange() 함수를 이용해 정렬하는 부분은 역시 순전히 질문자께서 남기신 그림과 똑같은 모양을 출력하는 게 존재 이유입니다.


select() 함수로 중간에 활용한 count 열을 삭제하는 것도 마찬가지입니다.


위에 있는 두 문장이 이해가 가지 않으시면 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트가 도움이 될 수 있습니다.


두 줄을 지우면 이런 결과가 나타납니다.


다시 말씀드리지만 코드를 이렇게 쓴 건 순전히 tidyverse를 이용하되 저 위에 있는 코드와 다른 코드를 쓰려는 목적입니다.


이것 말고도 얼마든 다른 방법이 있을 수 있습니다.


개인적으로는 맨 처음에 기본 함수로 기능을 구현하신 두 분이 진짜 실력자라고 생각합니다.


저 같은 '초짜'는 아예 저렇게 접근할 엄두 자체를 내지 못하니까요.


그래도 tidyverse를 사랑하는 한 사람으로서 오늘도 외칩니다. 


여러분 모두 Happy Tidyversing!


댓글,

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