26 반복
26.1 소개
이 장에서는 서로 다른 객체에 대해 동일한 작업을 반복적으로 수행하는 반복(iteration)을 위한 도구들을 배울 것입니다. R에서의 반복은 다른 프로그래밍 언어와는 다소 다르게 보이는 경향이 있는데, 이는 많은 부분이 암시적으로 처리되어 무료로 제공되기 때문입니다. 예를 들어, R에서 수치형 벡터 x를 두 배로 만들고 싶다면 그냥 2 * x라고 쓰면 됩니다. 대부분의 다른 언어에서는 for 루프 같은 것을 사용하여 x의 각 요소를 명시적으로 두 배로 만들어야 합니다.
이 책은 이미 여러 “것”들에 대해 동일한 작업을 수행하는 작지만 강력한 도구들을 몇 가지 소개했습니다:
-
facet_wrap()및facet_grid()는 각 하위 집합에 대해 플롯을 그립니다. -
group_by()와summarize()를 함께 사용하면 각 하위 집합에 대한 요약 통계를 계산합니다. -
unnest_wider()및unnest_longer()는 리스트 열의 각 요소에 대해 새로운 행과 열을 생성합니다.
이제 다른 함수를 입력으로 받는 함수를 중심으로 구축되었기 때문에 종종 함수형 프로그래밍(functional programming) 도구라고 불리는 좀 더 일반적인 도구들을 배울 때입니다. 함수형 프로그래밍을 배우는 것은 자칫 추상적으로 흐를 수 있지만, 이 장에서는 세 가지 일반적인 작업인 여러 열 수정하기, 여러 파일 읽기, 여러 객체 저장하기에 집중하여 구체적으로 다루겠습니다.
26.1.1 선수 지식
이 장에서는 tidyverse의 핵심 멤버인 dplyr과 purrr에서 제공하는 도구들에 집중하겠습니다. dplyr은 이미 보셨겠지만, purrr는 처음입니다. 이 장에서는 몇 가지 purrr 함수만 사용할 예정이지만, 프로그래밍 기술을 향상시키고 싶다면 탐색해 보기에 아주 좋은 패키지입니다.
26.2 여러 열 수정하기
다음과 같은 간단한 티블이 있고, 모든 열의 관측값 개수를 세고 중앙값을 계산하고 싶다고 가정해 봅시다.
복사해서 붙여넣기를 사용하여 할 수 있습니다:
하지만 이것은 코드 블록을 두 번 이상 복사해서 붙여넣지 말라는 우리의 경험 법칙을 어기는 것이며, 열이 수십 개 또는 수백 개라면 매우 지루해질 것임을 상상할 수 있습니다. 대신 across()를 사용할 수 있습니다:
across()에는 특히 중요한 세 가지 인수가 있으며, 다음 섹션들에서 자세히 다루겠습니다. across()를 사용할 때마다 처음 두 개를 사용하게 될 것입니다. 첫 번째 인수 .cols는 반복할 열을 지정하고, 두 번째 인수 .fns는 각 열에 대해 수행할 작업을 지정합니다. 출력 열의 이름을 추가로 제어해야 할 때는 .names 인수를 사용할 수 있으며, 이는 mutate()와 함께 across()를 사용할 때 특히 중요합니다. 또한 filter()와 함께 작동하는 두 가지 중요한 변형인 if_any()와 if_all()에 대해서도 논의할 것입니다.
26.2.1 .cols로 열 선택하기
across()의 첫 번째 인수 .cols는 변환할 열을 선택합니다. 이것은 select()와 동일한 사양을 사용하므로(Section 3.3.2), starts_with() 및 ends_with()와 같은 함수를 사용하여 이름에 따라 열을 선택할 수 있습니다.
across()에 특히 유용한 두 가지 추가 선택 기술이 있습니다: everything()과 where()입니다. everything()은 간단합니다. (그룹화되지 않은) 모든 열을 선택합니다:
set.seed(1014)
df <- tibble(
grp = sample(2, 10, replace = TRUE),
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
df |>
group_by(grp) |>
summarize(across(everything(), median))
#> # A tibble: 2 × 5
#> grp a b c d
#> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 1 -0.244 -0.522 -0.0974 -0.251
#> 2 2 -0.247 0.468 0.112 0.0700그룹화 열(여기서는 grp)은 summarize()에 의해 자동으로 보존되므로 across()에 포함되지 않습니다.
where()를 사용하면 유형에 따라 열을 선택할 수 있습니다:
-
where(is.numeric)은 모든 수치형 열을 선택합니다. -
where(is.character)는 모든 문자열 열을 선택합니다. -
where(is.Date)는 모든 날짜 열을 선택합니다. -
where(is.POSIXct)는 모든 날짜-시간 열을 선택합니다. -
where(is.logical)은 모든 논리형 열을 선택합니다.
다른 선택자들과 마찬가지로 부울 대수와 결합할 수 있습니다. 예를 들어 !where(is.numeric)은 수치형이 아닌 모든 열을 선택하고, starts_with("a") & where(is.logical)은 이름이 “a”로 시작하는 모든 논리형 열을 선택합니다.
26.2.2 단일 함수 호출하기
across()의 두 번째 인수는 각 열이 어떻게 변환될지를 정의합니다. 위의 간단한 경우처럼 단일 기존 함수가 될 수도 있습니다. 이것은 R의 꽤 특별한 기능입니다. 우리는 한 함수(median, mean, str_flatten, …)를 다른 함수(across)에 전달하고 있습니다. 이것이 R을 함수형 프로그래밍 언어로 만드는 기능 중 하나입니다.
여기서 중요한 점은 이 함수를 across()에 전달하여 across()가 호출할 수 있게 하는 것이지, 우리가 직접 호출하는 것이 아니라는 점입니다. 즉, 함수 이름 뒤에 ()가 오면 안 됩니다. 이를 잊어버리면 오류가 발생합니다:
df |>
group_by(grp) |>
summarize(across(everything(), median()))
#> Error in `summarize()`:
#> ℹ In argument: `across(everything(), median())`.
#> Caused by error in `median.default()`:
#> ! argument "x" is missing, with no default이 오류는 입력 없이 함수를 호출하려고 했기 때문에 발생합니다. 예:
median()
#> Error in median.default(): argument "x" is missing, with no default26.2.3 여러 함수 호출하기
더 복잡한 경우에는 추가 인수를 제공하거나 여러 변환을 수행하고 싶을 수 있습니다. 간단한 예로 이 문제의 동기를 부여해 보겠습니다. 데이터에 결측값이 있으면 어떻게 될까요? median()은 결측값을 전파하여 최적이 아닌 출력을 제공합니다:
set.seed(1014)
rnorm_na <- function(n, n_na, mean = 0, sd = 1) {
sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
}
df_miss <- tibble(
a = rnorm_na(5, 1),
b = rnorm_na(5, 1),
c = rnorm_na(5, 2),
d = rnorm(5)
)
df_miss |>
summarize(
across(a:d, median),
n = n()
)
#> # A tibble: 1 × 5
#> a b c d n
#> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 NA NA NA 0.413 5이 결측값들을 제거하기 위해 na.rm = TRUE를 median()에 전달할 수 있다면 좋을 것입니다. 그렇게 하려면 median()을 직접 호출하는 대신, 원하는 인수로 median()을 호출하는 새 함수를 만들어야 합니다:
이것은 약간 장황하므로, R에는 편리한 단축키가 있습니다. 이런 일회용 또는 익명(anonymous)1 함수의 경우 function을 \2로 바꿀 수 있습니다:
어느 경우든 across()는 실질적으로 다음 코드로 확장됩니다:
median()에서 결측값을 제거할 때, 얼마나 많은 값이 제거되었는지도 알 수 있으면 좋을 것입니다. across()에 두 개의 함수를 제공하여 이를 알아낼 수 있습니다. 하나는 중앙값을 계산하고 다른 하나는 결측값의 개수를 세는 것입니다. .fns에 명명된 리스트를 사용하여 여러 함수를 제공합니다:
df_miss |>
summarize(
across(a:d, list(
median = \(x) median(x, na.rm = TRUE),
n_miss = \(x) sum(is.na(x))
)),
n = n()
)
#> # A tibble: 1 × 9
#> a_median a_n_miss b_median b_n_miss c_median c_n_miss d_median d_n_miss
#> <dbl> <int> <dbl> <int> <dbl> <int> <dbl> <int>
#> 1 -0.703 1 -0.265 1 -0.522 2 0.413 0
#> # ℹ 1 more variable: n <int>자세히 보시면, 열 이름이 {.col}_{.fn}과 같은 glue 사양(Section 14.3.2)을 사용하여 지정되었다는 것을 직감할 수 있을 것입니다. 여기서 .col은 원래 열의 이름이고 .fn은 함수의 이름입니다. 이것은 우연이 아닙니다! 다음 섹션에서 배우게 되겠지만, .names 인수를 사용하여 자신만의 glue 사양을 제공할 수 있습니다.
26.2.4 열 이름
across()의 결과는 .names 인수에 제공된 사양에 따라 이름이 지정됩니다. 함수 이름이 먼저 오게 하고 싶다면 직접 지정할 수 있습니다3:
df_miss |>
summarize(
across(
a:d,
list(
median = \(x) median(x, na.rm = TRUE),
n_miss = \(x) sum(is.na(x))
),
.names = "{.fn}_{.col}"
),
n = n(),
)
#> # A tibble: 1 × 9
#> median_a n_miss_a median_b n_miss_b median_c n_miss_c median_d n_miss_d
#> <dbl> <int> <dbl> <int> <dbl> <int> <dbl> <int>
#> 1 -0.703 1 -0.265 1 -0.522 2 0.413 0
#> # ℹ 1 more variable: n <int>.names 인수는 mutate()와 함께 across()를 사용할 때 특히 중요합니다. 기본적으로 across()의 출력에는 입력과 동일한 이름이 지정됩니다. 즉, mutate() 내부의 across()는 기존 열을 대체합니다. 예를 들어, 여기서는 coalesce()를 사용하여 NA를 0으로 바꿉니다:
대신 새 열을 만들고 싶다면 .names 인수를 사용하여 출력에 새 이름을 지정할 수 있습니다:
df_miss |>
mutate(
across(a:d, \(x) coalesce(x, 0), .names = "{.col}_na_zero")
)
#> # A tibble: 5 × 8
#> a b c d a_na_zero b_na_zero c_na_zero d_na_zero
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 -0.00557 -0.283 -1.86 -0.783 -0.00557 -0.283 -1.86 -0.783
#> 2 0.255 -0.247 -0.522 -0.00289 0.255 -0.247 -0.522 -0.00289
#> 3 -1.40 -0.554 0.512 0.413 -1.40 -0.554 0.512 0.413
#> 4 -2.44 -0.244 NA 0.724 -2.44 -0.244 0 0.724
#> 5 NA NA NA 2.35 0 0 0 2.3526.2.5 필터링
across()는 summarize() 및 mutate()와 훌륭한 조화를 이루지만 filter()와 함께 사용하기에는 더 어색합니다. 일반적으로 여러 조건을 | 또는 &로 결합하기 때문입니다. across()가 여러 논리형 열을 만드는 데 도움이 될 수 있다는 것은 분명하지만, 그 다음에는 어떻게 해야 할까요? 그래서 dplyr은 if_any()와 if_all()이라는 across()의 두 가지 변형을 제공합니다:
# df_miss |> filter(is.na(a) | is.na(b) | is.na(c) | is.na(d)) 와 동일
df_miss |> filter(if_any(a:d, is.na))
#> # A tibble: 2 × 4
#> a b c d
#> <dbl> <dbl> <dbl> <dbl>
#> 1 -2.44 -0.244 NA 0.724
#> 2 NA NA NA 2.35
# df_miss |> filter(is.na(a) & is.na(b) & is.na(c) & is.na(d)) 와 동일
df_miss |> filter(if_all(a:d, is.na))
#> # A tibble: 0 × 4
#> # ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>
26.2.6 함수 내의 across()
across()는 여러 열에 대해 작업할 수 있게 해주기 때문에 프로그래밍하기에 특히 유용합니다. 예를 들어, Jacob Scott은 여러 lubridate 함수를 래핑하여 모든 날짜 열을 연, 월, 일 열로 확장하는 이 작은 도우미 함수를 사용합니다:
expand_dates <- function(df) {
df |>
mutate(
across(where(is.Date), list(year = year, month = month, day = mday))
)
}
df_date <- tibble(
name = c("Amy", "Bob"),
date = ymd(c("2009-08-03", "2010-01-16"))
)
df_date |>
expand_dates()
#> # A tibble: 2 × 5
#> name date date_year date_month date_day
#> <chr> <date> <dbl> <dbl> <int>
#> 1 Amy 2009-08-03 2009 8 3
#> 2 Bob 2010-01-16 2010 1 16across()는 또한 첫 번째 인수가 tidy-select를 사용하기 때문에 단일 인수에 여러 열을 쉽게 제공할 수 있습니다. Section 25.3.2 에서 논의한 대로 해당 인수를 감싸야 한다는 점만 기억하면 됩니다. 예를 들어, 이 함수는 기본적으로 수치형 열의 평균을 계산합니다. 하지만 두 번째 인수를 제공하여 선택한 열만 요약하도록 선택할 수 있습니다:
summarize_means <- function(df, summary_vars = where(is.numeric)) {
df |>
summarize(
across({{ summary_vars }}, \(x) mean(x, na.rm = TRUE)),
n = n(),
.groups = "drop"
)
}
diamonds |>
group_by(cut) |>
summarize_means()
#> # A tibble: 5 × 9
#> cut carat depth table price x y z n
#> <ord> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 Fair 1.05 64.0 59.1 4359. 6.25 6.18 3.98 1610
#> 2 Good 0.849 62.4 58.7 3929. 5.84 5.85 3.64 4906
#> 3 Very Good 0.806 61.8 58.0 3982. 5.74 5.77 3.56 12082
#> 4 Premium 0.892 61.3 58.7 4584. 5.97 5.94 3.65 13791
#> 5 Ideal 0.703 61.7 56.0 3458. 5.51 5.52 3.40 21551
diamonds |>
group_by(cut) |>
summarize_means(c(carat, x:z))
#> # A tibble: 5 × 6
#> cut carat x y z n
#> <ord> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 Fair 1.05 6.25 6.18 3.98 1610
#> 2 Good 0.849 5.84 5.85 3.64 4906
#> 3 Very Good 0.806 5.74 5.77 3.56 12082
#> 4 Premium 0.892 5.97 5.94 3.65 13791
#> 5 Ideal 0.703 5.51 5.52 3.40 21551
26.2.7 pivot_longer()와 비교
계속 진행하기 전에 across()와 pivot_longer()(Section 5.3) 사이의 흥미로운 연결 고리를 짚고 넘어갈 가치가 있습니다. 많은 경우, 먼저 데이터를 피벗한 다음 열이 아닌 그룹별로 연산을 수행하여 동일한 계산을 수행할 수 있습니다. 예를 들어, 이 다중 함수 요약을 보세요:
피벗을 더 길게(longer) 한 다음 요약하여 동일한 값을 계산할 수 있습니다:
long <- df |>
pivot_longer(a:d) |>
group_by(name) |>
summarize(
median = median(value),
mean = mean(value)
)
long
#> # A tibble: 4 × 3
#> name median mean
#> <chr> <dbl> <dbl>
#> 1 a -0.246 -0.0426
#> 2 b 0.155 -0.0656
#> 3 c 0.0480 -0.0297
#> 4 d -0.193 -0.200그리고 across()와 동일한 구조를 원한다면 다시 피벗할 수 있습니다:
long |>
pivot_wider(
names_from = name,
values_from = c(median, mean),
names_vary = "slowest",
names_glue = "{name}_{.value}"
)
#> # A tibble: 1 × 8
#> a_median a_mean b_median b_mean c_median c_mean d_median d_mean
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 -0.246 -0.0426 0.155 -0.0656 0.0480 -0.0297 -0.193 -0.200이것은 가끔 현재 across()로는 해결할 수 없는 문제에 부딪힐 때 유용한 기술입니다. 즉, 동시에 계산하려는 열 그룹이 있는 경우입니다. 예를 들어, 데이터 프레임에 값과 가중치가 모두 포함되어 있고 가중 평균을 계산하고 싶다고 가정해 봅시다:
현재로서는 across()를 사용하여 이 작업을 수행할 수 있는 방법이 없지만4, pivot_longer()를 사용하면 비교적 간단합니다:
df_long <- df_paired |>
pivot_longer(
everything(),
names_to = c("group", ".value"),
names_sep = "_"
)
df_long
#> # A tibble: 40 × 3
#> group val wts
#> <chr> <dbl> <dbl>
#> 1 a -1.40 0.290
#> 2 b -1.86 0.461
#> 3 c 0.935 0.528
#> 4 d 2.76 0.709
#> 5 a 0.255 0.678
#> 6 b -0.522 0.315
#> # ℹ 34 more rows
df_long |>
group_by(group) |>
summarize(mean = weighted.mean(val, wts))
#> # A tibble: 4 × 2
#> group mean
#> <chr> <dbl>
#> 1 a -0.207
#> 2 b -0.237
#> 3 c 0.0208
#> 4 d 0.0655필요한 경우 pivot_wider()를 사용하여 이를 다시 원래 형태로 되돌릴 수 있습니다.
26.2.8 연습문제
-
다음을 수행하여
across()기술을 연습하세요:palmerpenguins::penguins의 각 열에 있는 고유 값의 개수를 계산합니다.mtcars의 모든 열의 평균을 계산합니다.diamonds를cut,clarity,color로 그룹화한 다음 관측값 개수를 세고 각 수치형 열의 평균을 계산합니다.
across()에 함수 리스트를 사용하면서 이름을 지정하지 않으면 어떻게 됩니까? 출력 이름은 어떻게 지정됩니까?날짜 열이 확장된 후 자동으로 제거되도록
expand_dates()를 조정하세요. 인수를 감싸야(embrace) 합니까?-
이 함수 파이프라인의 각 단계가 무엇을 하는지 설명하세요.
where()의 어떤 특별한 기능을 활용하고 있습니까?
26.3 여러 파일 읽기
이전 섹션에서는 dplyr::across()를 사용하여 여러 열에 대해 변환을 반복하는 방법을 배웠습니다. 이 섹션에서는 purrr::map()을 사용하여 디렉터리의 모든 파일에 대해 작업을 수행하는 방법을 배울 것입니다. 간단한 동기 부여로 시작하겠습니다. 읽고 싶은 엑셀 스프레드시트로 가득 찬 디렉터리가 있다고 가정해 봅시다5. 복사해서 붙여넣기를 사용하여 할 수 있습니다:
data2019 <- readxl::read_excel("data/y2019.xlsx")
data2020 <- readxl::read_excel("data/y2020.xlsx")
data2021 <- readxl::read_excel("data/y2021.xlsx")
data2022 <- readxl::read_excel("data/y2022.xlsx")그런 다음 dplyr::bind_rows()를 사용하여 모두 결합합니다:
data <- bind_rows(data2019, data2020, data2021, data2022)이 작업은 파일이 4개가 아니라 수백 개라면 금방 지루해질 것임을 상상할 수 있습니다. 다음 섹션들에서는 이러한 종류의 작업을 자동화하는 방법을 보여줍니다. 세 가지 기본 단계가 있습니다: list.files()를 사용하여 디렉터리의 모든 파일을 나열한 다음, purrr::map()을 사용하여 각 파일을 리스트로 읽어 들인 다음, purrr::list_rbind()를 사용하여 이를 단일 데이터 프레임으로 결합합니다. 그런 다음 모든 파일에 대해 정확히 동일한 작업을 수행할 수 없는, 이질성이 증가하는 상황을 처리하는 방법에 대해 논의하겠습니다.
26.3.1 디렉터리의 파일 나열하기
이름에서 알 수 있듯이 list.files()는 디렉터리의 파일을 나열합니다. 거의 항상 다음 세 가지 인수를 사용하게 될 것입니다:
첫 번째 인수
path는 살펴볼 디렉터리입니다.pattern은 파일 이름을 필터링하는 데 사용되는 정규 표현식입니다. 가장 일반적인 패턴은 지정된 확장자를 가진 모든 파일을 찾기 위한[.]xlsx$또는[.]csv$와 같은 것입니다.full.names는 디렉터리 이름을 출력에 포함할지 여부를 결정합니다. 거의 항상 이것을TRUE로 설정하고 싶을 것입니다.
우리의 동기 부여 예제를 구체화하기 위해, 이 책에는 gapminder 패키지의 데이터가 포함된 12개의 엑셀 스프레드시트가 있는 폴더가 있습니다. 이 폴더는 https://github.com/hadley/r4ds/tree/main/data/gapminder에서 찾을 수 있습니다. 각 파일에는 142개 국가에 대한 1년 치 데이터가 들어 있습니다. 적절한 list.files() 호출로 이를 모두 나열할 수 있습니다:
paths <- list.files("data/gapminder", pattern = "[.]xlsx$", full.names = TRUE)
paths
#> [1] "data/gapminder/1952.xlsx" "data/gapminder/1957.xlsx"
#> [3] "data/gapminder/1962.xlsx" "data/gapminder/1967.xlsx"
#> [5] "data/gapminder/1972.xlsx" "data/gapminder/1977.xlsx"
#> [7] "data/gapminder/1982.xlsx" "data/gapminder/1987.xlsx"
#> [9] "data/gapminder/1992.xlsx" "data/gapminder/1997.xlsx"
#> [11] "data/gapminder/2002.xlsx" "data/gapminder/2007.xlsx"26.3.2 리스트
이제 이 12개의 경로가 있으므로 read_excel()을 12번 호출하여 12개의 데이터 프레임을 얻을 수 있습니다:
gapminder_1952 <- readxl::read_excel("data/gapminder/1952.xlsx")
gapminder_1957 <- readxl::read_excel("data/gapminder/1957.xlsx")
gapminder_1962 <- readxl::read_excel("data/gapminder/1962.xlsx")
...,
gapminder_2007 <- readxl::read_excel("data/gapminder/2007.xlsx")하지만 각 시트를 자체 변수에 넣으면 몇 단계 후에 작업하기가 매우 어려워질 것입니다. 대신 단일 객체에 넣는 것이 작업하기 더 쉬울 것입니다. 리스트는 이 작업에 완벽한 도구입니다:
files <- list(
readxl::read_excel("data/gapminder/1952.xlsx"),
readxl::read_excel("data/gapminder/1957.xlsx"),
readxl::read_excel("data/gapminder/1962.xlsx"),
...,
readxl::read_excel("data/gapminder/2007.xlsx")
)이제 리스트에 이 데이터 프레임들이 있으니, 어떻게 하나를 꺼낼까요? files[[i]]를 사용하여 i번째 요소를 추출할 수 있습니다:
files[[3]]
#> # A tibble: 142 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 32.0 10267083 853.
#> 2 Albania Europe 64.8 1728137 2313.
#> 3 Algeria Africa 48.3 11000948 2551.
#> 4 Angola Africa 34 4826015 4269.
#> 5 Argentina Americas 65.1 21283783 7133.
#> 6 Australia Oceania 70.9 10794968 12217.
#> # ℹ 136 more rows[[에 대해서는 Section 27.3 에서 더 자세히 다루겠습니다.
26.3.3 purrr::map()과 list_rbind()
해당 데이터 프레임들을 리스트에 “수동으로” 모으는 코드는 파일을 하나씩 읽는 코드만큼이나 입력하기 지루합니다. 다행히도 purrr::map()을 사용하여 paths 벡터를 훨씬 더 잘 활용할 수 있습니다. map()은 across()와 비슷하지만, 데이터 프레임의 각 열에 대해 작업을 수행하는 대신 벡터의 각 요소에 대해 작업을 수행합니다. map(x, f)는 다음의 단축 표현입니다:
list(
f(x[[1]]),
f(x[[2]]),
...,
f(x[[n]])
)따라서 map()을 사용하여 12개의 데이터 프레임 리스트를 얻을 수 있습니다:
files <- map(paths, readxl::read_excel)
length(files)
#> [1] 12
files[[1]]
#> # A tibble: 142 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 28.8 8425333 779.
#> 2 Albania Europe 55.2 1282697 1601.
#> 3 Algeria Africa 43.1 9279525 2449.
#> 4 Angola Africa 30.0 4232095 3521.
#> 5 Argentina Americas 62.5 17876956 5911.
#> 6 Australia Oceania 69.1 8691212 10040.
#> # ℹ 136 more rows(이것은 str()로 특히 간결하게 표시되지 않는 또 다른 데이터 구조이므로, RStudio에 로드하고 View()로 검사하고 싶을 수 있습니다.)
이제 purrr::list_rbind()를 사용하여 해당 데이터 프레임 리스트를 단일 데이터 프레임으로 결합할 수 있습니다:
list_rbind(files)
#> # A tibble: 1,704 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 28.8 8425333 779.
#> 2 Albania Europe 55.2 1282697 1601.
#> 3 Algeria Africa 43.1 9279525 2449.
#> 4 Angola Africa 30.0 4232095 3521.
#> 5 Argentina Americas 62.5 17876956 5911.
#> 6 Australia Oceania 69.1 8691212 10040.
#> # ℹ 1,698 more rows또는 파이프라인에서 두 단계를 한 번에 수행할 수도 있습니다:
paths |>
map(readxl::read_excel) |>
list_rbind()read_excel()에 추가 인수를 전달하고 싶다면 어떻게 할까요? across()와 동일한 기술을 사용합니다. 예를 들어, n_max = 1을 사용하여 데이터의 처음 몇 행만 엿보는 것이 종종 유용합니다:
paths |>
map(\(path) readxl::read_excel(path, n_max = 1)) |>
list_rbind()
#> # A tibble: 12 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 28.8 8425333 779.
#> 2 Afghanistan Asia 30.3 9240934 821.
#> 3 Afghanistan Asia 32.0 10267083 853.
#> 4 Afghanistan Asia 34.0 11537966 836.
#> 5 Afghanistan Asia 36.1 13079460 740.
#> 6 Afghanistan Asia 38.4 14880372 786.
#> # ℹ 6 more rows이것은 무언가 빠져 있다는 것을 명확히 해줍니다. year 열이 없는데, 해당 값은 개별 파일이 아니라 경로에 기록되어 있기 때문입니다. 다음으로 이 문제를 해결해 보겠습니다.
26.3.4 경로에 있는 데이터
때로는 파일 이름 자체가 데이터이기도 합니다. 이 예제에서 파일 이름에는 연도가 포함되어 있는데, 이는 개별 파일 내부에는 기록되지 않습니다. 해당 열을 최종 데이터 프레임에 넣으려면 두 가지 작업을 수행해야 합니다:
먼저, 경로 벡터의 이름을 지정합니다. 가장 쉬운 방법은 함수를 인수로 받을 수 있는 set_names() 함수를 사용하는 것입니다. 여기서는 basename()을 사용하여 전체 경로에서 파일 이름만 추출합니다:
paths |> set_names(basename)
#> 1952.xlsx 1957.xlsx
#> "data/gapminder/1952.xlsx" "data/gapminder/1957.xlsx"
#> 1962.xlsx 1967.xlsx
#> "data/gapminder/1962.xlsx" "data/gapminder/1967.xlsx"
#> 1972.xlsx 1977.xlsx
#> "data/gapminder/1972.xlsx" "data/gapminder/1977.xlsx"
#> 1982.xlsx 1987.xlsx
#> "data/gapminder/1982.xlsx" "data/gapminder/1987.xlsx"
#> 1992.xlsx 1997.xlsx
#> "data/gapminder/1992.xlsx" "data/gapminder/1997.xlsx"
#> 2002.xlsx 2007.xlsx
#> "data/gapminder/2002.xlsx" "data/gapminder/2007.xlsx"이 이름들은 모든 map 함수에 의해 자동으로 전달되므로, 데이터 프레임 리스트도 동일한 이름을 갖게 됩니다:
files <- paths |>
set_names(basename) |>
map(readxl::read_excel)이렇게 하면 map() 호출은 다음의 단축 표현이 됩니다:
files <- list(
"1952.xlsx" = readxl::read_excel("data/gapminder/1952.xlsx"),
"1957.xlsx" = readxl::read_excel("data/gapminder/1957.xlsx"),
"1962.xlsx" = readxl::read_excel("data/gapminder/1962.xlsx"),
...,
"2007.xlsx" = readxl::read_excel("data/gapminder/2007.xlsx")
)또한 [[를 사용하여 이름으로 요소를 추출할 수 있습니다:
files[["1962.xlsx"]]
#> # A tibble: 142 × 5
#> country continent lifeExp pop gdpPercap
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 32.0 10267083 853.
#> 2 Albania Europe 64.8 1728137 2313.
#> 3 Algeria Africa 48.3 11000948 2551.
#> 4 Angola Africa 34 4826015 4269.
#> 5 Argentina Americas 65.1 21283783 7133.
#> 6 Australia Oceania 70.9 10794968 12217.
#> # ℹ 136 more rows그런 다음 list_rbind()의 names_to 인수를 사용하여 이름을 year라는 새 열에 저장하도록 지시한 다음, readr::parse_number()를 사용하여 문자열에서 숫자를 추출합니다.
paths |>
set_names(basename) |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
mutate(year = parse_number(year))
#> # A tibble: 1,704 × 6
#> year country continent lifeExp pop gdpPercap
#> <dbl> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 1952 Afghanistan Asia 28.8 8425333 779.
#> 2 1952 Albania Europe 55.2 1282697 1601.
#> 3 1952 Algeria Africa 43.1 9279525 2449.
#> 4 1952 Angola Africa 30.0 4232095 3521.
#> 5 1952 Argentina Americas 62.5 17876956 5911.
#> 6 1952 Australia Oceania 69.1 8691212 10040.
#> # ℹ 1,698 more rows더 복잡한 경우에는 디렉터리 이름에 다른 변수가 저장되어 있거나, 파일 이름에 여러 데이터 비트가 포함되어 있을 수 있습니다. 이 경우 set_names()(인수 없이)를 사용하여 전체 경로를 기록한 다음, tidyr::separate_wider_delim()과 그 친구들을 사용하여 유용한 열로 변환하세요.
paths |>
set_names() |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
separate_wider_delim(year, delim = "/", names = c(NA, "dir", "file")) |>
separate_wider_delim(file, delim = ".", names = c("file", "ext"))
#> # A tibble: 1,704 × 8
#> dir file ext country continent lifeExp pop gdpPercap
#> <chr> <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 gapminder 1952 xlsx Afghanistan Asia 28.8 8425333 779.
#> 2 gapminder 1952 xlsx Albania Europe 55.2 1282697 1601.
#> 3 gapminder 1952 xlsx Algeria Africa 43.1 9279525 2449.
#> 4 gapminder 1952 xlsx Angola Africa 30.0 4232095 3521.
#> 5 gapminder 1952 xlsx Argentina Americas 62.5 17876956 5911.
#> 6 gapminder 1952 xlsx Australia Oceania 69.1 8691212 10040.
#> # ℹ 1,698 more rows26.3.5 작업 저장
이제 멋지고 깔끔한 데이터 프레임을 얻기 위해 이 모든 노력을 기울였으니, 작업을 저장하기에 아주 좋은 시간입니다:
gapminder <- paths |>
set_names(basename) |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
mutate(year = parse_number(year))
write_csv(gapminder, "gapminder.csv")이제 나중에 이 문제로 돌아올 때 단일 csv 파일을 읽어 들일 수 있습니다. 대규모의 풍부한 데이터셋의 경우 Section 22.4 에서 논의한 대로 .csv보다는 parquet를 사용하는 것이 더 나은 선택일 수 있습니다.
프로젝트에서 작업하는 경우, 이런 종류의 데이터 준비 작업을 수행하는 파일을 0-cleanup.R과 같은 이름으로 부르는 것이 좋습니다. 파일 이름의 0은 이 파일이 다른 무엇보다 먼저 실행되어야 함을 암시합니다.
입력 데이터 파일이 시간이 지남에 따라 변경된다면, 이런 종류의 데이터 정리 코드가 입력 파일 중 하나가 수정될 때마다 자동으로 다시 실행되도록 설정하는 targets와 같은 도구를 배워서 사용하는 것을 고려해 보세요.
26.3.6 여러 번의 단순한 반복
여기서는 데이터를 디스크에서 직접 로드했고, 운 좋게도 깔끔한 데이터셋을 얻었습니다. 대부분의 경우 추가적인 정리가 필요하며, 두 가지 기본 옵션이 있습니다. 복잡한 함수로 한 번의 반복을 수행하거나, 단순한 함수로 여러 번의 반복을 수행하는 것입니다. 우리의 경험상 대부분의 사람들은 먼저 한 번의 복잡한 반복을 시도하지만, 여러 번의 단순한 반복을 수행하는 것이 종종 더 좋습니다.
예를 들어 여러 파일을 읽어 들이고, 결측값을 필터링하고, 피벗한 다음, 결합하고 싶다고 가정해 봅시다. 문제에 접근하는 한 가지 방법은 파일을 받아서 그 모든 단계를 수행하는 함수를 작성한 다음 map()을 한 번 호출하는 것입니다:
process_file <- function(path) {
df <- read_csv(path)
df |>
filter(!is.na(id)) |>
mutate(id = tolower(id)) |>
pivot_longer(jan:dec, names_to = "month")
}
paths |>
map(process_file) |>
list_rbind()또는 process_file()의 각 단계를 모든 파일에 대해 수행할 수도 있습니다:
paths |>
map(read_csv) |>
map(\(df) df |> filter(!is.na(id))) |>
map(\(df) df |> mutate(id = tolower(id))) |>
map(\(df) df |> pivot_longer(jan:dec, names_to = "month")) |>
list_rbind()이 접근 방식을 권장하는 이유는 첫 번째 파일을 올바르게 만드는 데 집착하지 않고 나머지로 넘어갈 수 있기 때문입니다. 정리 및 청소 작업을 할 때 모든 데이터를 고려함으로써 전체적으로 생각하고 더 높은 품질의 결과를 얻을 가능성이 큽니다.
이 특정 예제에서는 데이터 프레임들을 더 일찍 결합하여 또 다른 최적화를 할 수 있습니다. 그러면 일반적인 dplyr 동작에 의존할 수 있습니다:
paths |>
map(read_csv) |>
list_rbind() |>
filter(!is.na(id)) |>
mutate(id = tolower(id)) |>
pivot_longer(jan:dec, names_to = "month")26.3.7 이질적인 데이터
불행히도 때로는 map()에서 list_rbind()로 바로 넘어갈 수 없는 경우가 있습니다. 데이터 프레임들이 너무 이질적이어서 list_rbind()가 실패하거나 별로 유용하지 않은 데이터 프레임을 생성하기 때문입니다. 이럴 때는 여전히 모든 파일을 로드하는 것부터 시작하는 것이 유용합니다:
files <- paths |>
map(readxl::read_excel) 그런 다음 데이터 프레임의 구조를 캡처하여 데이터 과학 기술로 탐색할 수 있게 하는 전략이 매우 유용합니다. 그렇게 하는 한 가지 방법은 각 열에 대해 한 행씩 있는 티블을 반환하는 이 편리한 df_types 함수6를 사용하는 것입니다:
df_types <- function(df) {
tibble(
col_name = names(df),
col_type = map_chr(df, vctrs::vec_ptype_full),
n_miss = map_int(df, \(x) sum(is.na(x)))
)
}
df_types(gapminder)
#> # A tibble: 6 × 3
#> col_name col_type n_miss
#> <chr> <chr> <int>
#> 1 year double 0
#> 2 country character 0
#> 3 continent character 0
#> 4 lifeExp double 0
#> 5 pop double 0
#> 6 gdpPercap double 0그런 다음 이 함수를 모든 파일에 적용하고, 차이점이 어디에 있는지 더 쉽게 볼 수 있도록 피벗을 수행할 수 있습니다. 예를 들어, 이를 통해 우리가 작업해 온 gapminder 스프레드시트가 모두 상당히 동질적임을 쉽게 확인할 수 있습니다:
files |>
map(df_types) |>
list_rbind(names_to = "file_name") |>
select(-n_miss) |>
pivot_wider(names_from = col_name, values_from = col_type)
#> # A tibble: 12 × 6
#> file_name country continent lifeExp pop gdpPercap
#> <chr> <chr> <chr> <chr> <chr> <chr>
#> 1 1952.xlsx character character double double double
#> 2 1957.xlsx character character double double double
#> 3 1962.xlsx character character double double double
#> 4 1967.xlsx character character double double double
#> 5 1972.xlsx character character double double double
#> 6 1977.xlsx character character double double double
#> # ℹ 6 more rows파일 형식이 이질적이라면 성공적으로 병합하기 전에 추가 처리를 수행해야 할 수도 있습니다. 불행히도 이 부분은 여러분이 스스로 알아내도록 남겨두겠지만, map_if() 및 map_at()에 대해 읽어보는 것이 좋습니다. map_if()를 사용하면 값에 따라 리스트의 요소를 선택적으로 수정할 수 있고, map_at()을 사용하면 이름에 따라 요소를 선택적으로 수정할 수 있습니다.
26.3.8 실패 처리
때로는 데이터의 구조가 너무 엉망이라 단일 명령으로 모든 파일을 읽을 수조차 없을 때가 있습니다. 그러면 map()의 단점 중 하나를 만나게 됩니다. 즉, 전체가 성공하거나 실패한다는 것입니다. map()은 디렉터리의 모든 파일을 성공적으로 읽거나, 아니면 오류와 함께 실패하여 파일을 하나도 읽지 못합니다. 이것은 짜증 나는 일입니다. 왜 단 하나의 실패가 다른 모든 성공에 접근하는 것을 막아야 할까요?
다행히 purrr에는 이 문제를 해결하기 위한 도우미인 possibly()가 있습니다. possibly()는 함수 연산자(function operator)로 알려져 있습니다. 함수를 인수로 받아 수정된 동작을 가진 함수를 반환합니다. 특히 possibly()는 함수가 오류를 발생시키는 대신 지정된 값을 반환하도록 변경합니다:
files <- paths |>
map(possibly(\(path) readxl::read_excel(path), NULL))
data <- files |> list_rbind()이것은 여기서 특히 잘 작동하는데, list_rbind()가 많은 tidyverse 함수와 마찬가지로 NULL을 자동으로 무시하기 때문입니다.
이제 쉽게 읽을 수 있는 모든 데이터를 확보했으니, 일부 파일 로드에 실패한 이유와 그에 대해 무엇을 할지 알아내는 어려운 부분을 해결할 차례입니다. 먼저 실패한 경로들을 가져옵니다:
failed <- map_vec(files, is.null)
paths[failed]
#> character(0)그런 다음 실패한 각 파일에 대해 가져오기 함수를 다시 호출하여 무엇이 잘못되었는지 확인합니다.
26.4 여러 출력 저장하기
지난 섹션에서는 여러 파일을 단일 객체로 읽어 들이는 데 유용한 map()에 대해 배웠습니다. 이 섹션에서는 이제 그 반대 문제, 즉 하나 이상의 R 객체를 가져와서 하나 이상의 파일에 저장하는 방법을 살펴보겠습니다. 다음 세 가지 예제를 사용하여 이 과제를 탐구하겠습니다:
- 여러 데이터 프레임을 하나의 데이터베이스에 저장하기.
- 여러 데이터 프레임을 여러
.csv파일에 저장하기. - 여러 플롯을 여러
.png파일에 저장하기.
26.4.1 데이터베이스에 쓰기
때로는 한꺼번에 많은 파일로 작업할 때 모든 데이터를 메모리에 한 번에 올릴 수 없어서 map(files, read_csv)를 수행할 수 없는 경우가 있습니다. 이 문제를 처리하는 한 가지 접근 방식은 dbplyr을 사용하여 필요한 부분만 액세스할 수 있도록 데이터를 데이터베이스에 로드하는 것입니다.
운이 좋다면 사용 중인 데이터베이스 패키지가 경로 벡터를 받아서 모두 데이터베이스에 로드하는 편리한 함수를 제공할 것입니다. duckdb의 duckdb_read_csv()가 그런 사례입니다:
con <- DBI::dbConnect(duckdb::duckdb())
duckdb::duckdb_read_csv(con, "gapminder", paths)이것은 여기서 잘 작동하겠지만, 우리는 csv 파일이 아니라 엑셀 스프레드시트를 가지고 있습니다. 그래서 “수동으로” 해야 할 것입니다. 수동으로 하는 법을 배우면 csv 파일 뭉치가 있고, 작업 중인 데이터베이스에 이를 모두 로드하는 단일 함수가 없는 경우에도 도움이 될 것입니다.
먼저 데이터를 채울 테이블을 만드는 것부터 시작해야 합니다. 가장 쉬운 방법은 원하는 모든 열을 포함하지만 데이터는 샘플링된 정도만 포함하는 더미 데이터 프레임인 템플릿을 만드는 것입니다. gapminder 데이터의 경우, 단일 파일을 읽고 연도를 추가하여 해당 템플릿을 만들 수 있습니다:
template <- readxl::read_excel(paths[[1]])
template$year <- 1952
template
#> # A tibble: 142 × 6
#> country continent lifeExp pop gdpPercap year
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Afghanistan Asia 28.8 8425333 779. 1952
#> 2 Albania Europe 55.2 1282697 1601. 1952
#> 3 Algeria Africa 43.1 9279525 2449. 1952
#> 4 Angola Africa 30.0 4232095 3521. 1952
#> 5 Argentina Americas 62.5 17876956 5911. 1952
#> 6 Australia Oceania 69.1 8691212 10040. 1952
#> # ℹ 136 more rows이제 데이터베이스에 연결하고 DBI::dbCreateTable()을 사용하여 템플릿을 데이터베이스 테이블로 바꿀 수 있습니다:
con <- DBI::dbConnect(duckdb::duckdb())
DBI::dbCreateTable(con, "gapminder", template)dbCreateTable()은 template의 데이터를 사용하지 않고 변수 이름과 유형만 사용합니다. 따라서 지금 gapminder 테이블을 검사해 보면 비어 있지만, 우리가 예상하는 유형을 가진 필요한 변수들이 있는 것을 볼 수 있습니다:
con |> tbl("gapminder")
#> # Source: table<gapminder> [?? x 6]
#> # Database: DuckDB 1.4.3 [root@Darwin 25.1.0:R 4.5.0/:memory:]
#> # ℹ 6 variables: country <chr>, continent <chr>, lifeExp <dbl>, pop <dbl>,
#> # gdpPercap <dbl>, year <dbl>다음으로, 단일 파일 경로를 받아서 R로 읽어 들이고 그 결과를 gapminder 테이블에 추가하는 함수가 필요합니다. read_excel()을 DBI::dbAppendTable()과 결합하여 이를 수행할 수 있습니다:
append_file <- function(path) {
df <- readxl::read_excel(path)
df$year <- parse_number(basename(path))
DBI::dbAppendTable(con, "gapminder", df)
}이제 paths의 각 요소에 대해 append_file()을 한 번씩 호출해야 합니다. map()으로도 가능합니다:
paths |> map(append_file)하지만 우리는 append_file()의 출력 결과에는 관심이 없으므로, map() 대신 walk()를 사용하는 것이 약간 더 좋습니다. walk()는 map()과 정확히 동일한 작업을 수행하지만 출력 결과는 버립니다:
paths |> walk(append_file)이제 테이블에 모든 데이터가 들어 있는지 확인할 수 있습니다:
26.4.2 csv 파일 쓰기
각 그룹에 대해 하나씩 여러 csv 파일을 쓰려는 경우에도 동일한 기본 원칙이 적용됩니다. ggplot2::diamonds 데이터를 가져와서 각 clarity(투명도)별로 하나의 csv 파일을 저장하고 싶다고 가정해 봅시다. 먼저 그 개별 데이터셋들을 만들어야 합니다. 여러 가지 방법으로 할 수 있지만, 우리가 특히 좋아하는 방법은 group_nest()입니다.
by_clarity <- diamonds |>
group_nest(clarity)
by_clarity
#> # A tibble: 8 × 2
#> clarity data
#> <ord> <list<tibble[,9]>>
#> 1 I1 [741 × 9]
#> 2 SI2 [9,194 × 9]
#> 3 SI1 [13,065 × 9]
#> 4 VS2 [12,258 × 9]
#> 5 VS1 [8,171 × 9]
#> 6 VVS2 [5,066 × 9]
#> # ℹ 2 more rows이것은 8개의 행과 2개의 열이 있는 새로운 티블을 제공합니다. clarity는 그룹화 변수이고, data는 각 고유한 clarity 값에 대해 하나의 티블을 포함하는 리스트 열입니다:
by_clarity$data[[1]]
#> # A tibble: 741 × 9
#> carat cut color depth table price x y z
#> <dbl> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#> 1 0.32 Premium E 60.9 58 345 4.38 4.42 2.68
#> 2 1.17 Very Good J 60.2 61 2774 6.83 6.9 4.13
#> 3 1.01 Premium F 61.8 60 2781 6.39 6.36 3.94
#> 4 1.01 Fair E 64.5 58 2788 6.29 6.21 4.03
#> 5 0.96 Ideal F 60.7 55 2801 6.37 6.41 3.88
#> 6 1.04 Premium G 62.2 58 2801 6.46 6.41 4
#> # ℹ 735 more rows여기서 mutate()와 str_glue()를 사용하여 출력 파일의 이름을 제공하는 열을 만들어 보겠습니다:
by_clarity <- by_clarity |>
mutate(path = str_glue("diamonds-{clarity}.csv"))
by_clarity
#> # A tibble: 8 × 3
#> clarity data path
#> <ord> <list<tibble[,9]>> <glue>
#> 1 I1 [741 × 9] diamonds-I1.csv
#> 2 SI2 [9,194 × 9] diamonds-SI2.csv
#> 3 SI1 [13,065 × 9] diamonds-SI1.csv
#> 4 VS2 [12,258 × 9] diamonds-VS2.csv
#> 5 VS1 [8,171 × 9] diamonds-VS1.csv
#> 6 VVS2 [5,066 × 9] diamonds-VVS2.csv
#> # ℹ 2 more rows따라서 이 데이터 프레임들을 수동으로 저장한다면 다음과 같이 쓸 수 있을 것입니다:
이것은 변하는 인수가 하나가 아니라 두 개이므로 이전의 map() 사용과는 조금 다릅니다. 즉, 첫 번째 인수와 두 번째 인수를 모두 변경하는 map2()라는 새로운 함수가 필요합니다. 그리고 이번에도 출력 결과에는 관심이 없으므로 map2() 대신 walk2()를 원합니다. 그러면 다음과 같습니다:
walk2(by_clarity$data, by_clarity$path, write_csv)26.4.3 플롯 저장하기
동일한 기본 접근 방식을 사용하여 많은 플롯을 만들 수 있습니다. 먼저 원하는 플롯을 그리는 함수를 만들어 보겠습니다:
carat_histogram <- function(df) {
ggplot(df, aes(x = carat)) + geom_histogram(binwidth = 0.1)
}
carat_histogram(by_clarity$data[[1]])이제 map()을 사용하여 많은 플롯7과 최종 파일 경로의 리스트를 만들 수 있습니다:
그런 다음 walk2()를 ggsave()와 함께 사용하여 각 플롯을 저장합니다:
이것은 다음의 단축 표현입니다:
26.5 요약
이 장에서는 데이터 과학을 할 때 자주 발생하는 세 가지 문제, 즉 여러 열 조작하기, 여러 파일 읽기, 여러 출력 저장하기를 해결하기 위해 명시적 반복을 사용하는 방법을 살펴보았습니다. 하지만 일반적으로 반복은 슈퍼 파워입니다. 올바른 반복 기술을 알고 있다면 하나의 문제를 해결하는 것에서 모든 문제를 해결하는 것으로 쉽게 나아갈 수 있습니다. 이 장의 기술들을 마스터했다면, Advanced R의 함수형(Functionals) 장을 읽고 purrr 웹사이트를 참고하여 더 많은 내용을 배워보시기를 강력히 추천합니다.
다른 언어의 반복문에 대해 많이 알고 있다면, 우리가 for 루프에 대해 논의하지 않은 것에 놀랐을 수도 있습니다. 그 이유는 R의 데이터 분석 지향적 특성이 반복 방식을 바꾸기 때문입니다. 대부분의 경우 기존의 관용구에 의존하여 각 열이나 각 그룹에 작업을 수행할 수 있습니다. 그럴 수 없는 경우에는 리스트의 각 요소에 작업을 수행하는 map()과 같은 함수형 프로그래밍 도구를 종종 사용할 수 있습니다. 하지만 야생에서 마주치는 코드에서는 for 루프를 보게 될 것이므로, 몇 가지 중요한 기본(base) R 도구를 논의하는 다음 장에서 이에 대해 배울 것입니다.
익명인 이유는
<-를 사용하여 명시적으로 이름을 지정하지 않았기 때문입니다. 프로그래머들은 이를 “람다 함수”라고도 부릅니다.↩︎오래된 코드에서는
~ .x + 1과 같은 구문을 볼 수 있습니다. 이것은 익명 함수를 작성하는 또 다른 방법이지만 tidyverse 함수 내에서만 작동하며 항상 변수 이름.x를 사용합니다. 우리는 이제 기본 구문인\(x) x + 1을 권장합니다.↩︎현재로서는 열의 순서를 변경할 수는 없지만,
relocate()등을 사용하여 나중에 재정렬할 수 있습니다.↩︎언젠가는 가능할 수도 있겠지만, 현재로서는 방법이 보이지 않습니다.↩︎
대신 동일한 형식의 csv 파일 디렉터리가 있다면 Section 7.4 의 기술을 사용할 수 있습니다.↩︎
이 함수의 작동 방식은 설명하지 않겠지만, 사용된 함수들의 문서를 찾아보면 알아낼 수 있을 것입니다.↩︎
by_clarity$plot을 인쇄하여 조잡한 애니메이션을 얻을 수 있습니다.plots의 각 요소에 대해 하나의 플롯을 얻게 됩니다. 참고: 저에게는 이런 일이 일어나지 않았습니다.↩︎