R 데이터 매니지먼트: tidyverse

파일을 읽는 readr, 읽기 쉬운 코드를 만드는 %>% 연산자, 데이터를 다루는 dplyr 그리고 반복문을 다루는 purrr 패키지를 중심으로 tidyverse 생태계에서 데이터를 다루는 방법을 정리하였습니다.

lecture
tidyverse
data.table
purrr
R
Author
Affiliation

ANPANMAN Co.,Ltd

Published

January 23, 2019

김진섭 대표는 1월 28일(월) 성균관의대 사회의학교실를 방문, tidyverse 생태계에서의 데이터 매니지먼트 방법을 강의할 예정입니다. 강의 내용을 미리 정리하였습니다.

시작하기 전에

R 데이터 매니지먼트 방법은 크게 3 종류가 있다.

  1. 원래의 R 문법을 이용한 방법으로 과거 홈페이지1에 정리했었다.

  2. tidyverse는 직관적인 코드를 작성할 수 있는 점을 장점으로 원래의 R 문법을 빠르게 대체하고 있다.

  3. data.table 패키지는 빠른 실행속도를 장점으로 tidyverse 의 득세 속에서 살아남았으며, 역시 과거 홈페이지2에 정리한 바 있다.

본 강의는 이중 두 번째의 기초에 해당하며

  1. readr 패키지의 read_csv 함수로 데이터를 빠르게 읽은 후

  2. magrittr 패키지의 %>% 연산자와 dplyr 패키지의 select, mutate, filter, group_by, summarize 함수로 직관적인 코드를 작성하고

  3. purrr 패키지의 map 함수로 쉽게 반복문을 처리하는 것을 목표로 한다.

각각의 패키지를 따로 설치할 수도 있고 install.packages("tidyverse")tidyverse 생태계의 패키지를 모두 설치할 수도 있다.

데이터 읽기: readr

readr 패키지에서 csv 파일을 읽는 함수는 read_csv이며, 구분자(ex: 공백, 탭)가 다를 때는 read_delim 함수를 이용하여 구분자를 설정할 수 있다. 예제 데이터를 읽어보자.

library(readr)
a <- read_csv("https://raw.githubusercontent.com/jinseob2kim/R-skku-biohrs/master/data/smc_example1.csv")
Rows: 1000 Columns: 14
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr  (2): Sex, STRESS_EXIST
dbl (12): Age, Height, Weight, BMI, DM, HTN, Smoking, MACCE, Death, MACCE_da...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

데이터를 읽으면 각 변수들을 어떤 형태(숫자형, 문자형)로 읽었는지 표현되는데, 바꾸고 싶은 것이 있으면 아래와 같이 col_types 옵션을 이용하면 된다.

## Character: col_character() or "c"
a <- read_csv("https://raw.githubusercontent.com/jinseob2kim/R-skku-biohrs/master/data/smc_example1.csv",col_types = cols(HTN = "c"))

이제 데이터를 살펴보면 HTN 변수가 문자형이 된것을 볼 수 있다.

a

기본 함수인 read.csv 와 비교했을 때 아주 작은 데이터에서는 장점이 없으나, 그 크기가 커질수록 read_csv가 더 좋은 성능을 보임이 알려져 있다(Figure @ref(fig:readfunc)).

파일 읽기 함수 비교3

read_csv로 읽은 데이터는 tibble이라는 새로운 클래스로 저장된다. 직접 확인해보자.

[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 

기존의 data.frame 외에도 tbl_df, tbl 와 같은 것들이 추가되어 있다. tibbledata.frame보다 좀 더 날 것(?)의 정보를 보여주는데, 범주형 변수를 factor가 아닌 character 그대로 저장하고 변수명을 그대로 유지하는 것(data.frame에서 변수명의 특수문자나 공백은 .으로 바뀜)이 가장 큰 특징이다. tibble의 추가 내용은 jumpingrivers 블로그4를 참고하기 바란다.

직관적인 코드: %>% 연산자

tidyverse 생태계에서 가장 중요한 것을 하나만 고르라면 magrittr 패키지의 %>% 연산자를 선택하겠다. %>%은 Rstudio에서 단축키 Ctrl + Shift + M (OS X: Cmd + Shift + M)로 입력할 수 있는데, 이것을 이용하면 의식의 흐름대로 코딩을 할 수 있어 직관적인 코드를 작성할 수 있다. head함수를 통해 %>%의 장점을 알아보자.

library(magrittr)

## head(a)
a %>% head
a %>% head(n = 10)

head(a)a %>% head는 같은 명령어 “ahead를 보여줘”로, a %>% head가 생각의 흐름이 반영된 코드임을 알 수 있다. 일반적으로 %>% 연산자는 함수의 맨 처음 인자를 앞으로 빼오는 역할을 하고 f(x, y)x %>% f(y)로 바꿀 수 있다. 맨 처음 인자가 아닐 때에는 y %>% f(x, .)과 같이 앞으로 빼온 변수의 자리에 .를 넣으면 된다. 예를 들면 아래에 나오는 세 명령어는 모두 같다.

a %>% head(n = 10)
10 %>% head(a, .)   
10 %>% head(a, n = .)

%>%의 진가는 여러 함수를 한 번에 사용할 때 나타나는데, headsubset을 동시에 쓰는 경우를 예로 살펴보겠다.

## head(subset(a, Sex == "M"))
a %>% subset(Sex == "M") %>% head

이 명령어는 a에서 남자만 뽑아서 head를 보여줘로 기존의 head(subset(a, Sex == "M"))보다 훨씬 직관적이며 함수가 3개, 4개로 늘어날수록 비교가 안될 정도의 가독성을 보여준다. 남자만 뽑아 회귀분석을 수행하고 그 계수와 p-value를 구하는 예를 살펴보자. 먼저 기존의 코딩 스타일대로 명령을 수행하면 아래와 같다.

b <- subset(a, Sex == "M")
model <- glm(DM ~ Age + Weight + BMI, data = b, family = binomial)
summ.model <- summary(model)
summ.model$coefficients

다음은 %>% 를 이용한 코딩이다.

a %>% 
  subset(Sex == "M") %>% 
  glm(DM ~ Age + Weight + BMI, data = ., family = binomial) %>% 
  summary %>% 
  .$coefficients

%>% 연산자로 쓴 코드는 읽기 쉬운 것은 물론 불필요한 중간변수(b, model, summ.model)가 없어 깔끔하고 메모리의 낭비도 없다. 딱 하나 주의할 점은 코드를 내려쓸 때 각 줄은 반드시 %>%로 끝나야 한다는 점이다. 예를 들어 아래의 코드를 실행하면 맨 윗줄만 실행되는 것을 확인할 수 있다.

a %>% subset(Sex == "M")
  %>% head 

본 강의 후 다른 것은 까먹어도 %>% 만 익숙해지면 성공이라고 생각한다.

데이터 정리: dplyr

dplyr 는 데이터를 효과적으로 다룰 수 있는 일련의 함수들을 제공한다. 이중 group_bysummarize는 쉽게 그룹 별 요약통계량을 보여줌으로서 기존 R 문법과 차별화된 가치를 제공하므로 꼭 익혀두자.

filter

filtersubset 함수와 같은 기능으로 특정 조건으로 데이터를 필터링하는 데 이용된다. 아래는 데이터에서 남자만 추출하는 예시이다.

library(dplyr)
a %>% filter(Sex == "M") 

filter에서는 & 외에 ,으로도 AND 조건을 쓸 수 있어 가독성이 좋다. between 함수를 이용하면 연속변수의 특정 범위를 선택할 수도 있는데, 이것 역시 기존의 &를 활용하는 것보다 직관적이다. 50~60세 사이를 필터링하는 예시를 살펴보자.

## Age between 50 and 60.
a %>% filter(between(Age, 50, 60))

아래의 &,로 표현한 조건도 between을 이용한 것과 같은 결과를 보여준다.

a %>% filter(Age >= 50 & Age <= 60)
a %>% filter(Age >= 50, Age <= 60)

arrange: 정렬

arrange는 특정 순서에 따라 데이터를 정렬하는 함수로, 정렬 순서만 알려주는 order 함수와는 달리 정렬된 데이터를 보여주는 것이 특징이다.

## a[order(a$Age), ]
a %>% arrange(Age)

정렬 조건이 2개 이상이면 ,로 같이 적을 수 있으며 내림차순 정렬은 desc 명령어를 이용한다. 아래는 Age에 대해 오름차순, BMI에 대해 내림차순 정렬을 수행하는 예시이다.

## a[order(a$Age, -a$BMI), ]
a %>% arrange(Age, desc(BMI))

조건에 Age 대신 "Age"와 같이 문자열을 넣을 때는 언더바(_)가 붙은 arrange_ 함수를 이용하며, 이것은 나머지 함수들에서도 마찬가지이다.

a %>% arrange_("Age")

select: 변수 선택

select는 데이터에서 특정 변수들을 선택하는 함수로 기본 R 에는 없는 유용한 기능들을 제공한다.

## a[, c("Sex", "Age")]
a %>% select(Sex, Age, Height)

변수명 대신에 열 번호를 넣어도 되며 Sex:Height을 이용해서 SexHeight 사이의 모든 변수를 선택할 수도 있다. arrange 함수와는 달리 "Sex"와 같은 문자열도 그대로 입력 가능하며, 아래의 방법들은 모두 a %>% select(Sex, Age, Height)와 같은 코드이다.

a %>% select(Sex:Height)
a %>% select("Sex":"Height")
a %>% select(2, 3, 4)
a %>% select(c(2, 3, 4))
a %>% select(2:4)

특정 변수들을 빼려면 -Sex-(Sex:Height)와 같이 적으면 된다.

## a[, -c("Sex", "Age", "Height")]
a %>% select(-Sex, -Age, -Height)

아래의 코드도 a %>% select(-Sex, -Age, -Height)와 같은 결과를 준다.

## a[, -c("Sex", "Age", "Height")]
a %>% select(-2, -3, -4)
a %>% select(-(2:4))
a %>% select(-c(2, 3, 4))

a %>% select(-(Sex:Height))
a %>% select(-"Sex", -"Age", -"Height")
a %>% select(-("Sex":"Height"))

만약 MACCE_date, Death_date와 같이 _date로 끝나는 변수들을 선택하고 싶다면 end_with 함수를 이용하면 된다.

## a[, grep("_date", names(a))]
a %>% select(ends_with("date"))

이외에 select와 함께 쓸 수 있는 유용한 함수들을 정리하면 아래와 같다.

  • start_with("abc"): ’abc’로 시작하는 이름

  • end_with("xyz"): ’xyz’로 끝나는 이름

  • contains("ijk"): ’ijk’를 포함하는 이름

  • one_of(c("a", "b", "c")): 변수명이 a, b, c 중 하나

  • matches("(.)\\1"): 정규표현식 조건

  • num_range("x", 1:3): x1, x2, x3

mutate: 변수 생성

mutate는 새로운 변수를 만드는 함수이다. AgeBMI 변수에서 고령과 비만을 뜻하는 Old, Overweight 변수를 만들어 보자.

## a$old <- as.integer(a$Age >= 65); a$overweight <- as.integer(a$BMI >= 27)
a %>% mutate(Old = as.integer(Age >= 65),
             Overweight = as.integer(BMI >= 27)
             )

새로운 변수만 보여주려면 mutate 대신 transmute를 사용한다.

a %>% transmute(Old = as.integer(Age >= 65),
             Overweight = as.integer(BMI >= 27)
             )

group_bysummarize

group_by, summarize를 이용하여 원하는대로 그룹을 나누고 각 그룹의 요약통계량을 간단히 구할 수 있다. 기본 R에서는 aggregate함수가 같은 기능을 수행한다.

a %>% 
  group_by(Sex, Smoking) %>% 
  summarize(count = n(),
            meanBMI = mean(BMI),
            sdBMI = sd(BMI))

group_by"Age" 같은 문자열을 넣으려면 언더바(_)가 붙은 group_by_ 함수를 이용해야 한다.

모든 변수에 같은 요약방법을 적용하려면 summarize_all 함수를 사용한다. 아래는 50세 이상을 대상으로 모든 변수에 그룹별 평균을 적용한 예시이다.

a %>% 
  filter(Age >= 50) %>% 
  group_by(Sex, Smoking) %>% 
  summarize_all(mean) 

평균값을 계산할 수 없는 범주형 변수는 NA를, 그 외 변수들은 평균값을 보여줌을 확인할 수 있다. 여러 요약값을 동시에 보여주려면 funs 명령어로 여러 함수를 같이 적어주면 된다.

a %>% 
  filter(Age >= 50) %>% 
  select(-Patient_ID, -STRESS_EXIST) %>%       ## Except categorical variable
  group_by(Sex, Smoking) %>% 
  summarize_all(funs(mean = mean, sd = sd)) 

%>%와 기본함수만으로 똑같이 구현하기.

마지막에 수행했던 작업을 %>%R 기본함수만으로 구현하면서 본 단원을 마무리하겠다. filter 대신 subset, select 대신 [], group_bysummarize_all 대신에 aggregate를 이용하면 된다.

a %>% 
  subset(Age >= 50) %>%
  .[, -c(1, 14)] %>% 
  aggregate(list(Sex = .$Sex, Smoking = .$Smoking), 
            FUN = function(x){c(mean = mean(x), sd = sd(x))})

위와 같이 기본 함수로도 %>% 연산자를 이용하여 그럴듯하게 코드를 작성할 수 있으나, dplyr 와 비교했을 때 아무래도 가독성이 떨어진다는 점에서 dplyr가 유용한 패키지임을 확인할 수 있었다.

반복문 처리: purrr

본 내용에서는 for문이나 멀티코어의 사용은 언급하지 않는다. 해당 내용은 과거 강의5를 참고하기 바란다.

R에서 반복문을 처리하는데 가장 많이 이용되는 함수는 lapply(또는 sapply)일 것이다. purrr의 대표 함수인 map은 이 lapplytidyverse 철학으로 구현한 것이다. 이제부터 차이점을 알아보자.

map

데이터의 모든 변수들의 형태를 살펴보는 lapply 구문은 아래와 같다.

lapply(a, class)
$Sex
[1] "character"

$Age
[1] "numeric"

$Height
[1] "numeric"

$Weight
[1] "numeric"

$BMI
[1] "numeric"

$DM
[1] "numeric"

$HTN
[1] "character"

$Smoking
[1] "numeric"

$MACCE
[1] "numeric"

$Death
[1] "numeric"

$MACCE_date
[1] "numeric"

$Death_date
[1] "numeric"

$STRESS_EXIST
[1] "character"

$Number_stent
[1] "numeric"

이것을 map으로 구현하면 lapply와 정확히 같은 형태가 된다.

library(purrr)
map(a, class)
$Sex
[1] "character"

$Age
[1] "numeric"

$Height
[1] "numeric"

$Weight
[1] "numeric"

$BMI
[1] "numeric"

$DM
[1] "numeric"

$HTN
[1] "character"

$Smoking
[1] "numeric"

$MACCE
[1] "numeric"

$Death
[1] "numeric"

$MACCE_date
[1] "numeric"

$Death_date
[1] "numeric"

$STRESS_EXIST
[1] "character"

$Number_stent
[1] "numeric"

maplist 형태로 결과를 반환하며 특정 형태를 지정하려면 아래와 같은 함수들을 이용한다.

  • map: 리스트

  • map_lgl: 논리값(T, F)

  • map_int: 정수

  • map_dbl: 실수

  • map_chr: 문자열

  • map_dfr: rbinddata.frame

  • map_dfc: cbinddata.frame

앞의 예를 map_chr을 이용해 다시 실행하자.

map_chr(a, class)
         Sex          Age       Height       Weight          BMI           DM 
 "character"    "numeric"    "numeric"    "numeric"    "numeric"    "numeric" 
         HTN      Smoking        MACCE        Death   MACCE_date   Death_date 
 "character"    "numeric"    "numeric"    "numeric"    "numeric"    "numeric" 
STRESS_EXIST Number_stent 
 "character"    "numeric" 

이것은 sapply함수의 tidyverse 버전으로 기억하면 좋다. 아래의 명령어들은 모두 같은 결과를 나타낸다.

map_chr(a, class)
a %>% map_chr(class)
sapply(a, class)
unlist(map(a, class))
unlist(lapply(a, class))

각 변수에서 첫 번째 값을 뽑는 아래의 코드들도 sapplymap_*의 차이는 없다.

a %>% sapply(function(x){x[1]})
         Sex          Age       Height       Weight          BMI           DM 
         "M"         "52"        "160"         "63"  "24.609375"          "0" 
         HTN      Smoking        MACCE        Death   MACCE_date   Death_date 
         "1"          "1"          "0"          "0"       "1056"       "1056" 
STRESS_EXIST Number_stent 
   "No test"          "3" 
a %>% sapply(`[`, 1)
         Sex          Age       Height       Weight          BMI           DM 
         "M"         "52"        "160"         "63"  "24.609375"          "0" 
         HTN      Smoking        MACCE        Death   MACCE_date   Death_date 
         "1"          "1"          "0"          "0"       "1056"       "1056" 
STRESS_EXIST Number_stent 
   "No test"          "3" 
a %>% map_chr(`[`, 1)
          Sex           Age        Height        Weight           BMI 
          "M"   "52.000000"  "160.000000"   "63.000000"   "24.609375" 
           DM           HTN       Smoking         MACCE         Death 
   "0.000000"           "1"    "1.000000"    "0.000000"    "0.000000" 
   MACCE_date    Death_date  STRESS_EXIST  Number_stent 
"1056.000000" "1056.000000"     "No test"    "3.000000" 

classmean, 그리고 `[` 등 간단한 함수들은 lapply를 쓰나 map을 쓰나 차이가 없다. map의 진가는 반복할 함수가 복잡할 때 드러난다. 성별로 같은 회귀분석을 반복하는 코드를 예로 들어 보자. 먼저 lapply를 이용한 코드는 아래와 같다.

a %>% 
  group_split(Sex) %>% 
  lapply(function(x){lm(Death ~ Age, data = x, family = binomial)})
[[1]]

Call:
lm(formula = Death ~ Age, data = x, family = binomial)

Coefficients:
(Intercept)          Age  
  0.0461858    0.0001931  


[[2]]

Call:
lm(formula = Death ~ Age, data = x, family = binomial)

Coefficients:
(Intercept)          Age  
  -0.208833     0.004355  

위 예시에서 알 수 있듯이 lapply에서 복잡한 함수를 반복하려면 function(x) 문이 꼭 필요하고 가독성을 저해하는 원인이 된다. 그러나 map에서는 ~로 간단히 function(x)를 대체할 수 있다. 아래 코드를 살펴보자.

a %>% 
  group_split(Sex) %>% 
  map(~lm(Death ~ Age, data = ., family = binomial))
[[1]]

Call:
lm(formula = Death ~ Age, data = ., family = binomial)

Coefficients:
(Intercept)          Age  
  0.0461858    0.0001931  


[[2]]

Call:
lm(formula = Death ~ Age, data = ., family = binomial)

Coefficients:
(Intercept)          Age  
  -0.208833     0.004355  

map함수를 이용하여 function(x)~로, 성별 데이터에 해당하는 x.으로 바꾸니 훨씬 읽기가 쉬워졌다.

이번엔 같은 회귀분석을 수행한 후 Age의 p-value만 뽑는다고 하자.

a %>% 
  group_split(Sex) %>% 
  sapply(function(x){
    lm(Death ~ Age, data = x, family = binomial) %>% 
      summary %>% 
      .$coefficients %>% 
      .[8]     ## p-value: 8th value
    }) 
[1] 9.016998e-01 2.094342e-07

%>% 연산자를 이용해 나름대로 읽기 쉬운 코드가 되었다. 이것을 map_dbl로 다시 표현하면 아래와 같다.

a %>% 
  group_split(Sex) %>% 
  map_dbl(~lm(Death ~ Age, data = ., family = binomial) %>% 
        summary %>% 
        .$coefficients %>% 
        .[8]
  )
[1] 9.016998e-01 2.094342e-07

function(x) 가 없어 더 잘 보이기는 하나 고작 이 정도의 장점이라면 map을 쓸 필요가 없을 것 같다. map을 좀 더 적극적으로 사용해 보자.

a %>% 
  group_split(Sex) %>% 
  map(~lm(Death ~ Age, data = ., family = binomial)) %>% 
  map(summary) %>% 
  map("coefficients") %>% 
  map_dbl(8)
[1] 9.016998e-01 2.094342e-07

기존 코드는 .$coefficient.[8] 같은 부분이 거슬렸는데, map("coefficients"), map_dbl(8)으로 바꾸니 보기에 훨씬 깔끔하다. 참고로 map("coefficients")map(`[[`, "coefficients")의, map_dbl(8)map_dbl(`[`, 8)의 축약형 표현이다. 사실 아까 다루었던 첫 번째 값 뽑기의 경우도 아래와 같이 더 간단하게 쓸 수 있다.

a %>% map_chr(`[`, 1)
a %>% map_chr(1) 

map을 통해 함수의 모든 중간과정을 따로따로 구현한 것도 장점이다. 에러가 발생했을 때 드래그로 중간과정까지만 실행할 수 있어 함수의 어느 부분에서 에러가 발생했는지를 쉽게 알아낼 수 있다.

map2, pmap: 입력 변수 2개 이상

2개 이상의 입력값에 대해 반복문을 수행하는 함수로는 mapply가 있다. 이것의 tidyverse 버전이 map2pmap인데 전자는 2개의 조건에, 후자는 리스트 형태로 입력값의 갯수에 상관없이 반복문을 구현할 수 있다. 본 강의에서는 간단한 예만 다루어 보겠다. 먼저 mapply를 이용해서 여러 입력값의 합을 구하는 코드를 살펴보자.

mapply(sum, 1:5, 1:5)
[1]  2  4  6  8 10
sum %>% mapply(1:5, 1:5)
[1]  2  4  6  8 10
sum %>% mapply(1:5, 1:5, 1:5)
[1]  3  6  9 12 15

mapply는 첫번째 인수에 함수를, 그 다음부터는 입력값들을 2개, 3개… 계속 받을 수 있다. 반면 map2pmap은 입력값을 먼저 받는데, 이 때문에 pmap에서는 입력값들을 리스트 형태로 받는다.

map2(1:5, 1:5, sum)
[[1]]
[1] 2

[[2]]
[1] 4

[[3]]
[1] 6

[[4]]
[1] 8

[[5]]
[1] 10
pmap(list(1:5, 1:5, 1:5), sum)
[[1]]
[1] 3

[[2]]
[1] 6

[[3]]
[1] 9

[[4]]
[1] 12

[[5]]
[1] 15

리턴 형태를 지정하려면 map과 마찬가지로 map2_*pmap_* 꼴의 함수를 이용하면 되며 아래의 코드들은 같은 결과를 나타낸다.

pmap_int(list(1:5, 1:5, 1:5), sum)
[1]  3  6  9 12 15
list(1:5, 1:5, 1:5) %>% pmap_int(sum)
[1]  3  6  9 12 15

마지막으로 paste함수로 문자열을 합치는 예를 살펴보겠다. 먼저 map2_chr로 두 문자열을 합쳐보자.

name <- c("Alice", "Bob")
place <- c("LA", "New york")
map2_chr(name, place, ~paste(.x, "was born at", .y))
[1] "Alice was born at LA"     "Bob was born at New york"

첫 번째 입력값은 함수에서 .x로 두 번째 입력값은 .y로 표현할 수 있다. pmap 함수를 이용할 때는 ..1, ..2, ..3으로 바꿔 표현하면 된다.

life <- c("born", "died")
list(name, life, place) %>% pmap_chr(~paste(..1, "was", ..2, "at", ..3))
[1] "Alice was born at LA"     "Bob was died at New york"

마치며

지금까지 tidyverse 생태계에서 몇 가지 패키지를 이용해 데이터를 다루는 방법을 살펴보았다. 앞서 말했듯이 이 생태계에서 가장 중요한 것은 %>% 연산자를 이용하여 의식의 흐름대로 코딩을 수행하는 것이며, 이후 나머지 내용을 하나씩 적용해나가면 어느 순간 tidyverse 없이 살 수 없는(?) 자신을 발견하게 될 것이다. 본 글에서 다루지 않은 내용인 long, short 포맷을 다루는 tidyr, 문자열을 다루는 stringr, factor를 다루는 forcats, 날짜를 다루는 lubridate 그리고 모델을 다루는 modelrbroom 등은 R for Data Science[^8]와 Rstudio cheetsheet[^9]를 참고하기 바란다. 다음번에는 빠른 속도가 장점인 data.table를 다시 한번 정리해 볼 생각이다.

Footnotes

  1. https://jinseob2kim.github.io/rbasic.html↩︎

  2. https://jinseob2kim.github.io/radv1.html↩︎

  3. https://csgillespie.github.io/efficientR/5-3-importing-data.html↩︎

  4. https://www.jumpingrivers.com/blog/the-trouble-with-tibbles/↩︎

  5. https://jinseob2kim.github.io/radv1.html#faster_for-loop↩︎

Citation

BibTeX citation:
@online{kim2019,
  author = {Kim, Jinseob},
  title = {R {데이터} {매니지먼트:} Tidyverse},
  date = {2019-01-23},
  url = {https://blog.zarathu.com/posts/2019-01-03-rdatamanagement/},
  langid = {en}
}
For attribution, please cite this work as:
Kim, Jinseob. 2019. “R 데이터 매니지먼트: Tidyverse.” January 23, 2019. https://blog.zarathu.com/posts/2019-01-03-rdatamanagement/.