Introduction to Tidyverse and Improving Reading Files

Introduction to {tidyverse}

{tidyverse}는 데이터 과학을 위해 설계된 R 패키지 모음입니다. {tidyverse}에 속한 모든 패키지는 기본적인 설계 철학, 문법 및 데이터 구조를 공유합니다. {tidyverse}에 속한 패키지 군은 https://www.tidyverse.org/ 에서 확인하실 수 있습니다. 이 워크샵에서는 크게 세 패키지를 중심으로 살펴보겠습니다. dplyr, tidyr, 그리고 ggplot2 입니다.

{tidyverse} 패키지의 가장 큰 장점은 문법이 명료하다는 것입니다. 원래는 %>% 라는 파이프 인자를 처음으로 {tidyverse} 측에서 제공했습니다만, 이 기능이 효율적이라는 평가를 받자 Base R 팀에서도 |>라고 하는 파이프를 제공했습니다. 이 둘은 거의 상호 교체해서 사용할 수 있습니다.

예를 들어, 어떤 데이터에셋에서 특정한 열을 선택하려면 R의 기본함수는 data$variable의 형태로 달러 사인을 이용해 접속하거나 data[, "variable"]의 함수를 사용했어야 합니다. 하지만 {tidyverse}에서는 data |> select(variable)과 같은 식으로 데이터셋 > 변수를 선택하는 식으로 문법을 구성할 수 있습니다.

The Pipe Operator %>% and |>

거의 모든 {tidyverse} 코드에서 %>%를 찾을 수 있습니다. {magrittr} 패키지의 파이프 연산자 %>%는 읽기 쉽고 이해하기 쉬운 코드를 작성하거나 코드의 가독성을 높이기 위해 개발되었습니다. {tidyverse}%>%를 자동으로 로드하기 때문에 %>%를 사용하기 위해 {magrittr} 패키지를 수동으로 로드할 필요가 없습니다. Base R에서 제공하는 자연파이프, |>를 사용하더라도 마찬가지로 별도의 패키지를 불러올 필요가 없습니다. 파이프 연산자의 기능은 다음과 같습니다:

  • x %>% f(y)f(x, y)로 바뀝니다.

  • x %>% f(y) %>% g(z)g(f(x,y), z)로 바뀝니다.

  • 파이프 연산자 %>%는 변환되는 대상이 아니라 변환 작업 그 자체에 초점을 맞춥니다. f()를 하고 나서 g()를 실행하라는 식으로 일련의 명령문으로 읽을 수 있습니다. 코드를 읽을 때 %>%를 발음하는 좋은 방법은 “then”입니다.

구체적인 예를 살펴보겠습니다.

by_dest <- flights |> group_by(dest) 
delay <- by_dest |> summarize(count = n(),
                              dist = mean(distance, na.rm = TRUE), 
                              delay = mean( arr_delay, na.rm = TRUE)) 
delay <- delay |> filter(count > 20, dest != "HNL") 
delay
# A tibble: 96 × 4
   dest  count  dist delay
   <chr> <int> <dbl> <dbl>
 1 ABQ     254 1826   4.38
 2 ACK     265  199   4.85
 3 ALB     439  143  14.4 
 4 ATL   17215  757. 11.3 
 5 AUS    2439 1514.  6.02
 6 AVL     275  584.  8.00
 7 BDL     443  116   7.05
 8 BGR     375  378   8.03
 9 BHM     297  866. 16.9 
10 BNA    6333  758. 11.8 
# ℹ 86 more rows

최대 750마일까지 거리가 멀어질수록 지연이 증가하다가 다시 감소하는 것으로 보입니다. 비행 시간이 길어질수록 항공 지연을 만회할 수 있는 능력이 더 많아지는 거라고 볼 수 있을까요?

delay |> 
  ggplot(aes(x = dist, y = delay)) + 
  geom_point( aes( size = count), alpha = 1/3) + 
  geom_smooth(se = FALSE)

위의 코드는 각 중간 데이터 프레임에 이름을 지정해야 하기 때문에 작성하기가 약간 번거롭습니다. 이름을 짓는 것은 어렵기 때문에 분석 속도가 느려집니다. 아래 코드는 변환 대상이 아닌 변환에 초점을 맞추기 때문에 코드를 더 쉽게 읽을 수 있습니다. 그룹화, 요약, 필터링, ggplot과 같은 일련의 명령문으로 읽을 수 있습니다. 이 예제에서 알 수 있듯이 코드를 읽을 때 %>%를 발음하는 좋은 방법은 “then”입니다.

flights |> 
  group_by(dest) |> 
  summarize(count = n(),
            dist = mean( distance, na.rm = TRUE), 
            delay = mean(arr_delay, na.rm = TRUE)) |> 
  filter(count > 20, dest != "HNL") |> 
  ggplot(mapping = aes(x = dist, y = delay)) + 
  geom_point(aes(size = count), alpha = 1/3) + 
  geom_smooth(se = FALSE)

Style Guide

코드를 사람(특히 사용자)이 쉽게 읽을 수 있도록 만드는 것이 중요합니다. 코드의 가독성을 높이려면 다음과 같이 하는 것이 좋습니다.

  • 주석 추가(R에서는 #)

  • 프로그래밍 스타일 가이드 따르기

프로그래밍 스타일 가이드는 특정 프로그래밍 언어의 소스 코드를 작성할 때 사용되는 일련의 지침입니다. 스타일 가이드를 따르지 않더라도 R 코드는 계속 작동합니다. 그러나 스타일 가이드는 코드의 가독성을 향상시킵니다. R에는 몇 가지 스타일 가이드가 있습니다:

How to Improve Reading Files with the read_* functions

데이터 전처리 없이 CSV 파일을 읽어오는 경우는 거의 없습니다. 이 CSV 파일의 열 이름을 소문자로 변환하고 문자 “m”으로 시작하는 열만 선택하려고 한다고 가정해 보겠습니다:

MANUFACTURER,MODEL,DISPL,YEAR,CYL,TRANS,DRV,CTY,HWY,FL,CLASS audi,a4,1.8,1999,4,auto(l5),f,18,29,p,compact audi,a4,1.8,1999,4,manual(m5),f,21,29,p,compact audi,a4,2,2008,4,manual(m6),f,20,31,p,compact audi,a4,2,2008,4,auto(av),f,21,30,p,compact audi,a4,2.8,1999,6,auto(l5),f,16,26,p,compact audi,a4,2.8,1999,6,manual(m5),f,18,26,p,compact

대부분의 경우 CSV 파일을 먼저 읽은 다음 데이터를 전처리합니다. 예를 들어, {janitor} 패키지의 clean_names 함수를 사용하면 변수의 이름을 사용하기 편하고 가독성이 좋게 일관된 형태로 변환합니다. 여기서는 분량 상 출력 결과를 보이지 않도록 show_col_types 인자를 사용하였습니다.

library(tidyverse)
library(janitor)
mpg_new <- read_csv("data/mpg_uppercase.csv", 
                    show_col_types = FALSE) |> 
  clean_names() |> 
  select(c(manufacturer, model)) |> 
  glimpse()
Rows: 6
Columns: 2
$ manufacturer <chr> "audi", "audi", "audi", "audi", "audi", "audi"
$ model        <chr> "a4", "a4", "a4", "a4", "a4", "a4"

이렇게 전처리를 해도 되지만, read_* 함수에는 데이터 전처리를 할 수 있게끔 도와주는 인수가 일부 내장되어 있습니다. 이러한 인수를 사용하면 새로운 작업을 수행할 수는 없지만 데이터 전처리를 통해 데이터 읽기를 정형화할 수 있습니다.

Converting Column Names to Lowercase

이전 예제에서는 {janitor} 패키지의 clean_names 함수를 사용하여 열 이름을 소문자로 변환했습니다. read_csv 내부에서 name_repair 인수에 대한 함수, make_clean_names를 사용하여 동일한 작업을 수행할 수 있습니다:

read_csv("data/mpg_uppercase.csv",
         show_col_types = FALSE,
         name_repair = make_clean_names) |> 
  glimpse()
Rows: 6
Columns: 11
$ manufacturer <chr> "audi", "audi", "audi", "audi", "audi", "audi"
$ model        <chr> "a4", "a4", "a4", "a4", "a4", "a4"
$ displ        <dbl> 1.8, 1.8, 2.0, 2.0, 2.8, 2.8
$ year         <dbl> 1999, 1999, 2008, 2008, 1999, 1999
$ cyl          <dbl> 4, 4, 4, 4, 6, 6
$ trans        <chr> "auto(l5)", "manual(m5)", "manual(m6)", "auto(av)", "auto…
$ drv          <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE
$ cty          <dbl> 18, 21, 20, 21, 16, 18
$ hwy          <dbl> 29, 29, 31, 30, 26, 26
$ fl           <chr> "p", "p", "p", "p", "p", "p"
$ class        <chr> "compact", "compact", "compact", "compact", "compact", "c…

보시다시피 여기서는 clean_names가 아닌 make_clean_names 함수를 사용합니다. 그 이유는 clean_names 함수는 벡터와 함께 작동하지 않지만 make_clean_names는 작동하기 때문입니다.

Replacing and Removing Character Strings in Your Column Names

make_clean_names를 사용하면 열 이름에서 특정 문자를 바꿀 수도 있습니다. 예를 들어, 열 이름에서 “%” 문자를 실제 단어 “_percent”로 바꾸고 싶다고 해봅시다:

make_clean_names(c("A", "B%", "C"),
                 replace = c("%" = "_percent"))
[1] "a"         "b_percent" "c"        

정규표현식(regular expression)에 익숙하다면 더 복잡한 문자열 대체를 수행할 수 있습니다. 예를 들어, 문자 “A”로 시작하는 모든 열 이름에서 밑줄을 제거할 수 있습니다:

make_clean_names(c("A_1", "B_1", "C_1"),
                 replace = c("^A_" = "a"))
[1] "a1"  "b_1" "c_1"

Using a Specific Naming Convention for Column Names

이전 make_clean_names 예제에서 열 이름을 소문자로 변환해보았습니다. 이 함수는 기본적으로 스네이크 명명 규칙(snake naming convention)을 사용하기 때문입니다. Snake는 모든 이름을 소문자로 변환하고 밑줄로 단어를 구분합니다:

make_clean_names(c("myHouse", "MyGarden"),
                 case = "snake")
[1] "my_house"  "my_garden"

열 이름의 명명 규칙을 특정 및 반영하지 않으려면 “none”으로 옵션을 지정합니다:

make_clean_names(c("myHouse", "MyGarden"),
                 case = "none")
[1] "myHouse"  "MyGarden"

사실 여러가지 옵션이 있지만, snake 옵션이 제일 가독성이 좋고 사용이 용이합니다.

How to Read Many Files into R

데이터가 수백, 수천 개의 파일에 흩어져 있는 경우가 종종 있습니다. 이렇게 읽어와야 할 파일이 여럿 있다고 할 때, 파일을 수동으로 불러오고 싶지는 않을 것입니다. 따라서 파일을 자동으로 읽어오는 방법이 필요합니다. {tidyverse}에서 이것이 어떻게 작동하는지 설명하기 위해 25개의 CSV 파일을 만들어 보겠습니다. 여기서 CSV 파일을 만드는 방법은 중요하지 않습니다. 기본적으로 mpg 데이터셋에서 20개의 행을 25번 샘플링하고 이 데이터프레임을 디스크에 저장합니다. 또한 코드의 시작 부분에 생성한 CSV들을 저장할 many_files 라는 새 디렉토리를 만듭니다.

library(tidyverse)
library(fs) # install.packages("fs")
# 디렉토리를 만들어봅시다
dir_create(c("data/many_files"))
mpg_samples <- map(1:25, ~ slice_sample(mpg, n = 20))
iwalk(mpg_samples, ~ write_csv(., paste0("data/many_files/", .y, ".csv")))

이제 지정한 디렉토리에 20개의 CSV 파일들이 생겨난 것을 확인하실 수 있을 것입니다.

How to Create a Character Vector of File Paths?

파일을 R로 읽으려면 먼저 파일 경로의 문자 벡터를 만들어야 합니다. 이러한 벡터를 만드는 데는 몇 가지 옵션이 있습니다. R 의 기본 함수인 list.files를 사용하여 디렉터리에 있는 파일 이름의 문자 벡터를 반환하거나, {fs} 패키지의 함수 dir_ls를 사용할 수 있습니다.

(csv_files_list_files <- list.files(path = "data/many_files",
                                    pattern = "csv", full.names = TRUE))
 [1] "data/many_files/1.csv"  "data/many_files/10.csv" "data/many_files/11.csv"
 [4] "data/many_files/12.csv" "data/many_files/13.csv" "data/many_files/14.csv"
 [7] "data/many_files/15.csv" "data/many_files/16.csv" "data/many_files/17.csv"
[10] "data/many_files/18.csv" "data/many_files/19.csv" "data/many_files/2.csv" 
[13] "data/many_files/20.csv" "data/many_files/21.csv" "data/many_files/22.csv"
[16] "data/many_files/23.csv" "data/many_files/24.csv" "data/many_files/25.csv"
[19] "data/many_files/3.csv"  "data/many_files/4.csv"  "data/many_files/5.csv" 
[22] "data/many_files/6.csv"  "data/many_files/7.csv"  "data/many_files/8.csv" 
[25] "data/many_files/9.csv" 

이 함수에는 여러 인수(arguments)가 있습니다. 경로(path)를 사용하면 파일을 찾을 위치를 지정할 수 있습니다. 경로는 상대적이므로 RStudio 프로젝트에서 작업 중이거나 작업 디렉터리를 정의했는지 확인해봅시다. pattern은 정규표현식으로 지정할 수 있습니다. 위의 경우에는 파일에 “csv” 문자열이 포함된 경우를 찾으라고 설정했습니다. 마지막으로 full.names 인수는 파일 이름뿐만 아니라 파일의 전체 경로를 저장할 것임을 나타냅니다. 이 인수를 TRUE로 설정하지 않으면 나중에 파일을 읽는 데 문제가 발생할 수 있습니다.

다른 옵션은{fs} 패키지의 dir_ls 함수를 사용하는 것입니다. {fs}는 하드 디스크의 파일에 액세스하기 위한 플랫폼 교차 인터페이스를 제공합니다. 모든 파일 작업(삭제, 생성, 파일 이동 등)을 지원합니다.

# library(fs) # install.packages("fs")
(csv_files_dir_ls <- dir_ls(path = "data/many_files/",
                            glob = "*.csv", type = "file"))
data/many_files/1.csv  data/many_files/10.csv data/many_files/11.csv 
data/many_files/12.csv data/many_files/13.csv data/many_files/14.csv 
data/many_files/15.csv data/many_files/16.csv data/many_files/17.csv 
data/many_files/18.csv data/many_files/19.csv data/many_files/2.csv  
data/many_files/20.csv data/many_files/21.csv data/many_files/22.csv 
data/many_files/23.csv data/many_files/24.csv data/many_files/25.csv 
data/many_files/3.csv  data/many_files/4.csv  data/many_files/5.csv  
data/many_files/6.csv  data/many_files/7.csv  data/many_files/8.csv  
data/many_files/9.csv  

결과는 위와 동일합니다. 이번에도 파일이 저장된 경로를 지정합니다. glob 인수는 파일의 파일 유형을 지정하는 데 사용됩니다. type을 사용하면 폴더나 다른 것이 아닌 “파일”을 찾고 있음을 나타냅니다.

How to Read the Files into R from a Character Vector of Paths

이제 파일 경로를 알았으므로 파일을R에 로드할 수 있습니다. 이를 위한 {tidyverse}적 방식은 다음과 같습니다. {purrr} 패키지의 map_dfr 함수를 사용하는 것입니다. map_dfr은 모든 파일 경로를 반복하여 을 반복하고 데이터 프레임을 단일 데이터 프레임으로 결합합니다. 다음 코드에서 .x는 파일 이름을 나타냅니다. 파일 이름이 아닌 실제 csv 파일을 출력하려면 read_* 함수에 .x( 경로)를 넣어야 합니다. 이 예제에서는 CSV 파일로 작업하고 있습니다. 이 트릭은 모든 직사각형 파일 형식(표와 같이 2차원으로 나타낼 수 있는)에 동일하게 적용됩니다.

data_frames <- map_dfr(csv_files_dir_ls,
                       ~ read_csv(.x, show_col_types = FALSE))
glimpse(data_frames)
Rows: 500
Columns: 11
$ manufacturer <chr> "volkswagen", "nissan", "volkswagen", "audi", "toyota", "…
$ model        <chr> "passat", "pathfinder 4wd", "gti", "a4", "4runner 4wd", "…
$ displ        <dbl> 1.8, 4.0, 2.0, 2.8, 4.0, 6.2, 3.6, 3.0, 2.8, 2.7, 1.8, 2.…
$ year         <dbl> 1999, 2008, 1999, 1999, 2008, 2008, 2008, 1999, 1999, 199…
$ cyl          <dbl> 4, 6, 4, 6, 6, 8, 6, 6, 6, 4, 4, 6, 4, 6, 6, 4, 6, 8, 4, …
$ trans        <chr> "auto(l5)", "auto(l5)", "auto(l4)", "auto(l5)", "auto(l5)…
$ drv          <chr> "f", "4", "f", "f", "4", "r", "f", "f", "f", "4", "f", "f…
$ cty          <dbl> 18, 14, 19, 16, 16, 15, 17, 18, 17, 16, 25, 18, 18, 19, 1…
$ hwy          <dbl> 29, 20, 26, 26, 20, 25, 26, 26, 24, 20, 36, 26, 26, 25, 2…
$ fl           <chr> "p", "p", "r", "p", "r", "p", "r", "r", "r", "r", "r", "r…
$ class        <chr> "midsize", "suv", "compact", "compact", "suv", "2seater",…

But What If the Column Names of the Files Are Not Consistent?

안타깝게도 우리는 굉장히 복잡하거 엉망진창인 세상에 살고 있습니다. 즉, 데이터 상의 모든 열 이름이 다 같은 것은 아닙니다. 열 이름이 일관되지 않은 지저분한 데이터셋을 만들어 보겠습니다:

mpg_samples <- map(1:10, ~ slice_sample(mpg, n = 20))
inconsistent_dframes <- map(mpg_samples,
                            ~ janitor::clean_names(dat = .x, case = "random"))

이 10개의 데이터 프레임의 열 이름은 동일한 이름으로 구성되지만 대문자 또는 소문자로 무작위로 작성됩니다.

map(inconsistent_dframes, ~ colnames(.x)) |>
  head()
[[1]]
 [1] "MANuFaCturER" "mODEL"        "dispL"        "yEAr"         "cYl"         
 [6] "TrANS"        "dRV"          "Cty"          "Hwy"          "Fl"          
[11] "ClASs"       

[[2]]
 [1] "MAnUFACTUreR" "MOdeL"        "dIsPl"        "YeAR"         "cyl"         
 [6] "traNS"        "dRV"          "cTy"          "HWy"          "fL"          
[11] "cLAsS"       

[[3]]
 [1] "ManuFACturer" "MODEl"        "dISpL"        "YeAr"         "cyl"         
 [6] "TRAnS"        "DrV"          "cTy"          "hwy"          "fL"          
[11] "cLAsS"       

[[4]]
 [1] "mAnuFAcTUreR" "ModEL"        "DiSPl"        "Year"         "cyl"         
 [6] "traNS"        "drV"          "CTy"          "HWy"          "fL"          
[11] "ClasS"       

[[5]]
 [1] "MANUFacTureR" "MOdEl"        "DiSPL"        "YEAr"         "cyL"         
 [6] "TRAnS"        "DRv"          "Cty"          "HWY"          "FL"          
[11] "CLASS"       

[[6]]
 [1] "MAnUFActurER" "mODeL"        "DISpl"        "yEAr"         "cyl"         
 [6] "TrAnS"        "dRV"          "ctY"          "hwy"          "Fl"          
[11] "clAsS"       

이 데이터셋을 더욱 복잡하게 만들기 위해 데이터 프레임당 임의의 열 집합을 선택해 보겠습니다:

inconsistent_dframes <- map(inconsistent_dframes,
                            ~ .x[sample(1:length(.x), 
                                        sample(1:length(.x), 1))])
map(inconsistent_dframes, ~ colnames(.x)) |> head()
[[1]]
[1] "ClASs" "dRV"   "yEAr"  "dispL" "Cty"   "Fl"    "Hwy"  

[[2]]
[1] "HWy" "cyl"

[[3]]
 [1] "MODEl"        "DrV"          "TRAnS"        "dISpL"        "YeAr"        
 [6] "cyl"          "fL"           "ManuFACturer" "hwy"          "cTy"         
[11] "cLAsS"       

[[4]]
[1] "ClasS"        "DiSPl"        "mAnuFAcTUreR" "Year"         "traNS"       

[[5]]
[1] "YEAr"  "CLASS" "DiSPL" "TRAnS"

[[6]]
[1] "mODeL"        "hwy"          "clAsS"        "DISpl"        "dRV"         
[6] "MAnUFActurER" "yEAr"         "TrAnS"       

이제 이 파일들을 새로운 디렉토리에 저장해봅시다.

dir_create(c("data/unclean_files"))
iwalk(inconsistent_dframes,
      ~ write_csv(.x, paste0("data/unclean_files/", .y, ".csv")))

이전 접근법을 사용하여 이 데이터를 읽어드리려 하면 작동은 하지만 열 이름이 일관되지 않아 계속 새로운 열로 추가해 열이 너무 많아집니다:

many_columns_data_frame <- dir_ls(path = "data/unclean_files/",
                                  glob = "*.csv", type = "file") |> 
map_dfr(~ read_csv(.x, show_col_types = FALSE) |> 
          mutate(filename = .x))
colnames(many_columns_data_frame) |> sort()
 [1] "clAsS"        "clASs"        "cLaSS"        "cLAss"        "cLAsS"       
 [6] "ClasS"        "ClASs"        "CLASS"        "cty"          "cTy"         
[11] "Cty"          "CTY"          "cyl"          "Cyl"          "CYL"         
[16] "dispL"        "disPl"        "dISpL"        "DisPL"        "DiSPl"       
[21] "DiSPL"        "DIsPL"        "DISpl"        "drv"          "dRV"         
[26] "Drv"          "DrV"          "filename"     "fl"           "fL"          
[31] "Fl"           "hwy"          "hwY"          "Hwy"          "HwY"         
[36] "HWy"          "manuFAcTurER" "mAnuFAcTUreR" "ManuFACturer" "ManUfActUreR"
[41] "MAnUfactUreR" "MAnUFActurER" "mODel"        "mODeL"        "MoDeL"       
[46] "MODEl"        "traNS"        "TrAnS"        "TRans"        "TRanS"       
[51] "TRAnS"        "yEar"         "yEAr"         "Year"         "YeAr"        
[56] "YEaR"         "YEAr"        

열 이름이 엉망진창인 것을 확인할 수 있습니다. 따라서 데이터 프레임을 정리하고 열 이름을 특정 명명 규칙을 바탕으로 변환하여 일관되게 만들 수 있습니다. 모두 소문자로 작성하고 함께 하나의 데이터 프레임으로 결합하도록 하겠습니다:

many_columns_data_frame <- dir_ls(path = "data/unclean_files/",
                                  glob = "*.csv", type = "file") |> 
map_dfr(~ read_csv(.x, name_repair = tolower, show_col_types = FALSE) |> 
          mutate(filename = .x))
many_columns_data_frame |> glimpse()
Rows: 200
Columns: 12
$ class        <chr> "suv", "pickup", "suv", "suv", "suv", "compact", "minivan…
$ drv          <chr> "r", "4", "4", "4", "4", "f", "f", "f", "4", "f", "f", "4…
$ year         <dbl> 1999, 1999, 2008, 2008, 1999, 2008, 2008, 1999, 2008, 199…
$ displ        <dbl> 5.4, 3.9, 4.7, 4.6, 5.7, 2.0, 3.3, 2.4, 3.7, 1.9, 2.0, 4.…
$ cty          <dbl> 11, 13, 13, 13, 11, 22, 17, 18, 15, 33, 21, 12, 19, 16, 2…
$ fl           <chr> "r", "r", "r", "r", "r", "p", "r", "r", "r", "d", "p", "r…
$ hwy          <dbl> 17, 17, 17, 19, 15, 29, 24, 24, 19, 44, 29, 16, 25, 25, 3…
$ filename     <fs::path> "data/unclean_files/1.csv", "data/unclean_files/1.cs…
$ model        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ trans        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ cyl          <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ manufacturer <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…

보시다시피, 새 파일에는 원래 mpg 데이터 프레임과 동일한 수의 열이 있습니다. 파일당 임의의 열 집합을 선택했기 때문에 일부 값만 NA를 가지게 됩니다.

What If the Files Are Not in the Same Folder?

지금까지는 모든 파일이 같은 폴더에 있다고 상정했습니다. 문제는 항상 그런 것은 아니라는 점입니다. 때로는 파일이 깊숙이 중첩되어(deeply nested) 있을 수도 있습니다. 이 경우 각 폴더를 재귀적으로(recursively) 검색해야 합니다. 재귀적으로 검색한다는 것은 크롤링할 다른 폴더를 찾을 수 없을 때까지 각 폴더를 검색한다는 뜻입니다. 이것이 어떻게 작동하는지 알아보기 전에 데이터를 두 개의 폴더에 저장해 보겠습니다(이 경우, 원하는 만큼 많은 폴더에서 작동합니다):

mpg_samples <- map(1:40, ~ slice_sample(mpg, n = 20))
# Create directories
dir_create(c("data/nested_folders",
             "data/nested_folders/first_nested_folder",
             "data/nested_folders/second_nested_folder"))
# First folder
iwalk(mpg_samples[1:20],
      ~ write_csv(.x,
                  paste0("data/nested_folders/first_nested_folder/", 
                         .y, "_first.csv")))
# Second folder
iwalk(mpg_samples[21:40],
      ~ write_csv(.x, paste0("data/nested_folders/second_nested_folder/", 
                             .y, "_second.csv")))

이제 nested_folders 폴더에서 모든 csv 파일을 로드하려고 하면 비어 있는 벡터가 생성됩니다:

(csv_files_nested <- dir_ls("data/nested_folders/", 
                            glob = "*.csv", type = "file"))
character(0)

이는 dir_ls가 중첩된 폴더를 찾지 않고 상위 폴더만 찾기 때문입니다. dir_ls가 폴더를 재귀적으로 검색하도록 하려면 재귀 인수를 TRUE로 설정해야 합니다:

(csv_files_nested <- dir_ls("data/nested_folders/", 
                            glob = "*.csv", type = "file",
                            recurse = TRUE))
data/nested_folders/first_nested_folder/10_first.csv
data/nested_folders/first_nested_folder/11_first.csv
data/nested_folders/first_nested_folder/12_first.csv
data/nested_folders/first_nested_folder/13_first.csv
data/nested_folders/first_nested_folder/14_first.csv
data/nested_folders/first_nested_folder/15_first.csv
data/nested_folders/first_nested_folder/16_first.csv
data/nested_folders/first_nested_folder/17_first.csv
data/nested_folders/first_nested_folder/18_first.csv
data/nested_folders/first_nested_folder/19_first.csv
data/nested_folders/first_nested_folder/1_first.csv
data/nested_folders/first_nested_folder/20_first.csv
data/nested_folders/first_nested_folder/2_first.csv
data/nested_folders/first_nested_folder/3_first.csv
data/nested_folders/first_nested_folder/4_first.csv
data/nested_folders/first_nested_folder/5_first.csv
data/nested_folders/first_nested_folder/6_first.csv
data/nested_folders/first_nested_folder/7_first.csv
data/nested_folders/first_nested_folder/8_first.csv
data/nested_folders/first_nested_folder/9_first.csv
data/nested_folders/second_nested_folder/10_second.csv
data/nested_folders/second_nested_folder/11_second.csv
data/nested_folders/second_nested_folder/12_second.csv
data/nested_folders/second_nested_folder/13_second.csv
data/nested_folders/second_nested_folder/14_second.csv
data/nested_folders/second_nested_folder/15_second.csv
data/nested_folders/second_nested_folder/16_second.csv
data/nested_folders/second_nested_folder/17_second.csv
data/nested_folders/second_nested_folder/18_second.csv
data/nested_folders/second_nested_folder/19_second.csv
data/nested_folders/second_nested_folder/1_second.csv
data/nested_folders/second_nested_folder/20_second.csv
data/nested_folders/second_nested_folder/2_second.csv
data/nested_folders/second_nested_folder/3_second.csv
data/nested_folders/second_nested_folder/4_second.csv
data/nested_folders/second_nested_folder/5_second.csv
data/nested_folders/second_nested_folder/6_second.csv
data/nested_folders/second_nested_folder/7_second.csv
data/nested_folders/second_nested_folder/8_second.csv
data/nested_folders/second_nested_folder/9_second.csv

이제 nested_folders 디렉터리 내의 모든 파일에 액세스할 수 있습니다:

map_dfr(csv_files_nested, ~ read_csv(.x, show_col_types = FALSE)  |> 
mutate(filename = .x)) |> 
glimpse()
Rows: 800
Columns: 12
$ manufacturer <chr> "hyundai", "subaru", "toyota", "honda", "jeep", "audi", "…
$ model        <chr> "sonata", "impreza awd", "corolla", "civic", "grand chero…
$ displ        <dbl> 2.5, 2.5, 1.8, 1.6, 3.7, 2.8, 3.1, 3.9, 4.0, 2.0, 5.2, 1.…
$ year         <dbl> 1999, 2008, 2008, 1999, 2008, 1999, 2008, 1999, 2008, 200…
$ cyl          <dbl> 6, 4, 4, 4, 6, 6, 6, 6, 6, 4, 8, 4, 6, 6, 4, 8, 4, 8, 6, …
$ trans        <chr> "manual(m5)", "manual(m5)", "manual(m5)", "auto(l4)", "au…
$ drv          <chr> "f", "4", "f", "f", "4", "4", "4", "4", "4", "4", "4", "f…
$ cty          <dbl> 18, 19, 28, 24, 15, 15, 17, 14, 16, 19, 11, 21, 15, 18, 3…
$ hwy          <dbl> 26, 25, 37, 32, 19, 24, 25, 17, 20, 27, 15, 29, 22, 26, 4…
$ fl           <chr> "r", "p", "r", "r", "r", "p", "p", "r", "r", "p", "r", "p…
$ class        <chr> "midsize", "compact", "compact", "subcompact", "suv", "mi…
$ filename     <fs::path> "data/nested_folders/first_nested_folder/10_first.cs…

What If I Don’t Need Some of These Files?

디렉토리에 있는 모든 파일이 항상 필요한 것은 아니므로 파일 경로 목록에서 일부 파일을 제거해야 할 수도 있습니다. 이 작업을 수행하는 좋은 방법은 {stringr} 패키지의 str_detect 함수를 사용하는 것입니다. 예제를 살펴보겠습니다. 다음 예제에서는 문자 벡터를 생성하고 문자열 beach가 포함된 파일들을 남겨보겠습니다:

str_detect(c("house", "beach"), pattern = "beach")
[1] FALSE  TRUE

이 함수는 논리값을 반환합니다. 실제 문자 벡터를 변경하려면 문자 벡터 자체에 이러한 논리값을 추가해야 합니다:

c("my house", "my beach")[str_detect(c("house", "beach"), pattern = "beach")]
[1] "my beach"

하지만 이러한 파일을 제거하려면 어떻게 해야 할까요? negate 인수를 사용하면 패턴과 일치하지 않는 파일만 찾을 수 있습니다:

c("my house", "my beach")[str_detect(c("house", "beach"), 
                                     pattern = "beach",
                                     negate = TRUE)]
[1] "my house"

어려운 부분은 파일에 적합한 패턴을 찾는 것입니다. 숫자 2, 3, 4가 포함된 CSV 파일을 보관하고 싶지 않다고 해보겠습니다:

csv_files_nested[str_detect(csv_files_nested, pattern = "[2-4]",
                            negate = TRUE)]
data/nested_folders/first_nested_folder/10_first.csv
data/nested_folders/first_nested_folder/11_first.csv
data/nested_folders/first_nested_folder/15_first.csv
data/nested_folders/first_nested_folder/16_first.csv
data/nested_folders/first_nested_folder/17_first.csv
data/nested_folders/first_nested_folder/18_first.csv
data/nested_folders/first_nested_folder/19_first.csv
data/nested_folders/first_nested_folder/1_first.csv
data/nested_folders/first_nested_folder/5_first.csv
data/nested_folders/first_nested_folder/6_first.csv
data/nested_folders/first_nested_folder/7_first.csv
data/nested_folders/first_nested_folder/8_first.csv
data/nested_folders/first_nested_folder/9_first.csv
data/nested_folders/second_nested_folder/10_second.csv
data/nested_folders/second_nested_folder/11_second.csv
data/nested_folders/second_nested_folder/15_second.csv
data/nested_folders/second_nested_folder/16_second.csv
data/nested_folders/second_nested_folder/17_second.csv
data/nested_folders/second_nested_folder/18_second.csv
data/nested_folders/second_nested_folder/19_second.csv
data/nested_folders/second_nested_folder/1_second.csv
data/nested_folders/second_nested_folder/5_second.csv
data/nested_folders/second_nested_folder/6_second.csv
data/nested_folders/second_nested_folder/7_second.csv
data/nested_folders/second_nested_folder/8_second.csv
data/nested_folders/second_nested_folder/9_second.csv

정규표현식 [2-4]는 숫자 2에서 4까지를 의미합니다. 하지만 2, 3 또는 4로 끝나는 파일을 제외하고 불러오려면 어떻게 해야 할까요? 이 경우에는 다른 패턴이 필요합니다:

csv_files_nested[str_detect(csv_files_nested,
                            pattern = "[2-4]_first|second\\.csv$",
                            negate = TRUE)]
data/nested_folders/first_nested_folder/10_first.csv
data/nested_folders/first_nested_folder/11_first.csv
data/nested_folders/first_nested_folder/15_first.csv
data/nested_folders/first_nested_folder/16_first.csv
data/nested_folders/first_nested_folder/17_first.csv
data/nested_folders/first_nested_folder/18_first.csv
data/nested_folders/first_nested_folder/19_first.csv
data/nested_folders/first_nested_folder/1_first.csv
data/nested_folders/first_nested_folder/20_first.csv
data/nested_folders/first_nested_folder/5_first.csv
data/nested_folders/first_nested_folder/6_first.csv
data/nested_folders/first_nested_folder/7_first.csv
data/nested_folders/first_nested_folder/8_first.csv
data/nested_folders/first_nested_folder/9_first.csv

이 패턴은 조금 더 복잡합니다. 다시 말하지만, 숫자 2~4 다음에 밑줄이 있고 첫 번째 또는 두 번째 단어(세로 막대 |로 표시됨)를 찾습니다. 마침표는 정규 표현식에서 임의의 문자를 나타내므로 백슬래시 두 개(\\)로 끝내야 합니다. 마지막으로 파일은 달러 기호 $로 표시된 csv 문자로 끝나야 합니다.

csv_files_nested[str_detect(csv_files_nested, 
                            pattern = "[2-4]_first|second\\.csv$",
                            negate = TRUE)] |>
  map_dfr(~ read_csv(.x, show_col_types = FALSE) |>
            mutate(filename = .x)) |>
  glimpse()
Rows: 280
Columns: 12
$ manufacturer <chr> "hyundai", "subaru", "toyota", "honda", "jeep", "audi", "…
$ model        <chr> "sonata", "impreza awd", "corolla", "civic", "grand chero…
$ displ        <dbl> 2.5, 2.5, 1.8, 1.6, 3.7, 2.8, 3.1, 3.9, 4.0, 2.0, 5.2, 1.…
$ year         <dbl> 1999, 2008, 2008, 1999, 2008, 1999, 2008, 1999, 2008, 200…
$ cyl          <dbl> 6, 4, 4, 4, 6, 6, 6, 6, 6, 4, 8, 4, 6, 6, 4, 8, 4, 8, 6, …
$ trans        <chr> "manual(m5)", "manual(m5)", "manual(m5)", "auto(l4)", "au…
$ drv          <chr> "f", "4", "f", "f", "4", "4", "4", "4", "4", "4", "4", "f…
$ cty          <dbl> 18, 19, 28, 24, 15, 15, 17, 14, 16, 19, 11, 21, 15, 18, 3…
$ hwy          <dbl> 26, 25, 37, 32, 19, 24, 25, 17, 20, 27, 15, 29, 22, 26, 4…
$ fl           <chr> "r", "p", "r", "r", "r", "p", "p", "r", "r", "p", "r", "p…
$ class        <chr> "midsize", "compact", "compact", "subcompact", "suv", "mi…
$ filename     <fs::path> "data/nested_folders/first_nested_folder/10_first.cs…