26 Lặp
26.1 Giới thiệu
Trong chương này, bạn sẽ học các công cụ để iterate (iteration), tức là thực hiện iterate đi iterate lại cùng một hành động trên các đối tượng khác nhau. Lặp trong R nhìn chung có xu hướng khá khác biệt so với các ngôn ngữ lập trình khác vì phần lớn nó được thực hiện ngầm và chúng ta có được nó miễn phí. Ví dụ, nếu bạn muốn nhân đôi một vector số x trong R, bạn chỉ cần viết 2 * x. Trong hầu hết các ngôn ngữ khác, bạn cần phải nhân đôi từng phần tử của x một cách tường minh bằng một loop for nào đó.
Cuốn sách này đã cung cấp cho bạn một số ít nhưng mạnh mẽ các công cụ thực hiện cùng một hành động cho nhiều “thứ”:
-
facet_wrap()vàfacet_grid()vẽ một biểu đồ cho mỗi tập con. -
group_by()kết hợp vớisummarize()tính các thống kê tóm tắt cho mỗi tập con. -
unnest_wider()vàunnest_longer()tạo các row và column mới cho mỗi phần tử của một column list (list-column).
Bây giờ là lúc học thêm một số công cụ tổng quát hơn, thường được gọi là các công cụ lập trình hàm (functional programming) vì chúng được xây dựng xung quanh các function nhận các function khác làm đầu vào. Học lập trình function có thể dễ dàng đi vào hướng trừu tượng, nhưng trong chương này chúng ta sẽ giữ mọi thứ cụ thể bằng cách tập trung vào ba tác vụ phổ biến: chỉnh sửa nhiều column, đọc nhiều tệp và lưu nhiều đối tượng.
26.1.1 Điều kiện tiên quyết
Trong chương này, chúng ta sẽ tập trung vào các công cụ được cung cấp bởi dplyr và purrr, cả hai đều là thành viên cốt lõi của tidyverse. Bạn đã thấy dplyr trước đó, nhưng purrr thì mới. Chúng ta chỉ sử dụng một vài function của purrr trong chương này, nhưng đây là một package tuyệt vời để khám phá khi bạn cải thiện kỹ năng lập trình của mình.
26.2 Chỉnh sửa nhiều column
Hãy tưởng tượng bạn có tibble đơn giản này và muốn đếm số quan sát cũng như tính trung vị của mỗi column.
Bạn có thể làm điều đó bằng cách sao chép và dán:
Điều đó vi phạm quy tắc ngón tay cái của chúng ta là không bao giờ sao chép và dán quá hai lần, và bạn có thể tưởng tượng rằng điều này sẽ trở nên rất nhàm chán nếu bạn có row chục hoặc thậm chí row trăm column. Thay vào đó, bạn có thể sử dụng across():
across() có ba argument đặc biệt quan trọng, mà chúng ta sẽ thảo luận chi tiết trong các phần sau. Bạn sẽ sử dụng hai argument đầu tiên mỗi khi dùng across(): argument đầu tiên, .cols, chỉ định những column nào bạn muốn iterate qua, và argument thứ hai, .fns, chỉ định việc cần làm với mỗi column. Bạn có thể sử dụng argument .names khi cần kiểm soát thêm tên của các column đầu ra, điều này đặc biệt quan trọng khi bạn sử dụng across() với mutate(). Chúng ta cũng sẽ thảo luận về hai biến thể quan trọng, if_any() và if_all(), hoạt động với filter().
26.2.1 Chọn column với .cols
Đối số đầu tiên của across(), .cols, chọn các column để biến đổi. Nó sử dụng cùng các đặc tả như select(), Phần 3.3.2, nên bạn có thể sử dụng các function như starts_with() và ends_with() để chọn column dựa trên tên của chúng.
Có hai kỹ thuật chọn bổ sung đặc biệt hữu ích cho across(): everything() và where(). everything() thì đơn giản: nó chọn mọi column (không phải column nhóm):
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.0700Lưu ý các column nhóm (grp ở đây) không được bao gồm trong across(), vì chúng được tự động giữ lại bởi summarize().
where() cho phép bạn chọn column dựa trên kiểu dữ liệu của chúng:
-
where(is.numeric)chọn tất cả các column số. -
where(is.character)chọn tất cả các column string. -
where(is.Date)chọn tất cả các column ngày. -
where(is.POSIXct)chọn tất cả các column ngày-giờ. -
where(is.logical)chọn tất cả các column logic.
Giống như các bộ chọn khác, bạn có thể kết hợp chúng với đại số Boole. Ví dụ, !where(is.numeric) chọn tất cả các column không phải số, và starts_with("a") & where(is.logical) chọn tất cả các column logic có tên bắt đầu bằng “a”.
26.2.2 Gọi một function đơn
Đối số thứ hai của across() định nghĩa cách mỗi column sẽ được biến đổi. Trong các trường hợp đơn giản, như ở trên, đây sẽ là một function có sẵn. Đây là một tính năng khá đặc biệt của R: chúng ta đang truyền một function (median, mean, str_flatten, …) cho một function khác (across). Đây là một trong những tính năng khiến R trở thành một ngôn ngữ lập trình function.
Điều quan trọng cần lưu ý là chúng ta đang truyền function này cho across(), để across() có thể gọi nó; chúng ta không tự gọi nó. Điều đó có nghĩa là tên function không bao giờ được theo sau bởi (). Nếu bạn quên, bạn sẽ nhận được lỗi:
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 defaultLỗi này phát sinh vì bạn đang gọi function mà không có đầu vào, ví dụ:
median()
#> Error in `median.default()`:
#> ! argument "x" is missing, with no default26.2.3 Gọi nhiều hàm
Trong các trường hợp phức tạp hơn, bạn có thể muốn cung cấp thêm argument hoặc thực hiện nhiều phép biến đổi. Hãy tạo động lực cho vấn đề này bằng một ví dụ đơn giản: điều gì xảy ra nếu chúng ta có một số missing value (missing value) trong dữ liệu? median() lan truyền những missing value đó, cho chúng ta một đầu ra không tối ưu:
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 5Sẽ tốt nếu chúng ta có thể truyền na.rm = TRUE cho median() để loại bỏ các missing value này. Để làm vậy, thay vì gọi median() trực tiếp, chúng ta cần tạo một function mới gọi median() với các argument mong muốn:
Điều này hơi dài dòng, nên R đi kèm một lối tắt tiện lợi: đối với loại function dùng một lần, hay còn gọi là function vô danh (anonymous)1, bạn có thể thay function bằng \2:
Trong cả hai trường hợp, across() thực tế mở rộng thành đoạn code sau:
Khi chúng ta loại bỏ các missing value khỏi median(), sẽ tốt nếu biết chính xác bao nhiêu giá trị đã bị loại bỏ. Chúng ta có thể tìm ra điều đó bằng cách cung cấp hai function cho across(): một function tính trung vị và function kia đếm các missing value. Bạn cung cấp nhiều function bằng cách sử dụng một list có tên cho .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>Nếu quan sát kỹ, bạn có thể suy ra rằng các column được đặt tên bằng một đặc tả glue (Phần 14.3.2) như {.col}_{.fn} trong đó .col là tên của column gốc và .fn là tên của function. Đó không phải là sự trùng hợp! Như bạn sẽ học trong phần tiếp theo, bạn có thể sử dụng argument .names để cung cấp đặc tả glue của riêng mình.
26.2.4 Tên cột
Kết quả của across() được đặt tên theo đặc tả được cung cấp trong argument .names. Chúng ta có thể chỉ định tên riêng nếu muốn tên function xuất hiện trước3:
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>Đối số .names đặc biệt quan trọng khi bạn sử dụng across() với mutate(). Mặc định, đầu ra của across() được đặt cùng tên với đầu vào. Điều này có nghĩa là across() bên trong mutate() sẽ thay thế các column hiện có. Ví dụ, ở đây chúng ta sử dụng coalesce() để thay thế các NA bằng 0:
Nếu bạn muốn tạo các column mới thay vào đó, bạn có thể sử dụng argument .names để đặt tên mới cho đầu ra:
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 Lọc
across() là một sự kết hợp tuyệt vời cho summarize() và mutate() nhưng nó hơi khó sử dụng với filter(), vì bạn thường kết hợp nhiều điều kiện với | hoặc &. Rõ ràng là across() có thể giúp tạo nhiều column logic, nhưng rồi sao? Vì vậy dplyr cung cấp hai biến thể của across() gọi là if_any() và if_all():
# same as 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
# same as 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() trong các function
across() đặc biệt hữu ích khi lập trình vì nó cho phép bạn thao tác trên nhiều column. Ví dụ, Jacob Scott sử dụng function trợ giúp nhỏ này để bọc một loạt các function lubridate nhằm mở rộng tất cả các column ngày thành các column năm, tháng và ngày:
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() cũng giúp dễ dàng cung cấp nhiều column trong một argument đơn vì argument đầu tiên sử dụng tidy-select; bạn chỉ cần nhớ embrace argument đó, như chúng ta đã thảo luận trong Phần 25.3.2. Ví dụ, function này sẽ tính trung bình của các column số theo mặc định. Nhưng bằng cách cung cấp argument thứ hai, bạn có thể chọn chỉ tóm tắt các column được chọn:
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 So sánh với pivot_longer()
Trước khi tiếp tục, cần chỉ ra một mối liên hệ thú vị giữa across() và pivot_longer() (Phần 5.3). Trong nhiều trường hợp, bạn thực hiện cùng các phép tính bằng cách trước tiên xoay dữ liệu rồi thực hiện các thao tác theo nhóm thay vì theo column. Ví dụ, hãy xem phép tóm tắt đa function này:
Chúng ta có thể tính cùng các giá trị bằng cách xoay dài hơn rồi tóm tắt:
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.200Và nếu bạn muốn cùng cấu trúc như across(), bạn có thể xoay lại:
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Đây là một kỹ thuật hữu ích cần biết vì đôi khi bạn sẽ gặp vấn đề hiện không thể giải quyết được với across(): khi bạn có các nhóm column mà bạn muốn tính toán đồng thời. Ví dụ, hãy tưởng tượng rằng data frame của chúng ta chứa cả giá trị và trọng số và chúng ta muốn tính trung bình có trọng số:
Hiện tại không có cách nào để làm điều này với across()4, nhưng nó tương đối đơn giản với 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.0655Nếu cần, bạn có thể pivot_wider() kết quả này trở lại dạng ban đầu.
26.2.8 Bài tập
-
Thực hành kỹ năng
across()của bạn bằng cách:Tính số giá trị duy nhất trong mỗi column của
palmerpenguins::penguins.Tính trung bình của mỗi column trong
mtcars.Nhóm
diamondstheocut,clarityvàcolorrồi đếm số quan sát và tính trung bình của mỗi column số.
Điều gì xảy ra nếu bạn sử dụng một list các function trong
across(), nhưng không đặt tên cho chúng? Đầu ra được đặt tên như thế nào?Điều chỉnh
expand_dates()để tự động loại bỏ các column ngày sau khi chúng đã được mở rộng. Bạn có cần embrace bất kỳ argument nào không?-
Giải thích mỗi bước trong pipeline của function này làm gì. Chúng ta đang tận dụng tính năng đặc biệt nào của
where()?
26.3 Đọc nhiều tệp
Trong phần trước, bạn đã học cách sử dụng dplyr::across() để iterate lại một phép biến đổi trên nhiều column. Trong phần này, bạn sẽ học cách sử dụng purrr::map() để làm gì đó với mỗi tệp trong một thư mục. Hãy bắt đầu với một chút động lực: hãy tưởng tượng bạn có một thư mục đầy các spreadsheet excel5 mà bạn muốn đọc. Bạn có thể làm điều đó bằng cách sao chép và dán:
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")Và sau đó sử dụng dplyr::bind_rows() để kết hợp tất cả lại với nhau:
data <- bind_rows(data2019, data2020, data2021, data2022)Bạn có thể tưởng tượng rằng điều này sẽ nhanh chóng trở nên nhàm chán, đặc biệt nếu bạn có row trăm tệp, không chỉ bốn. Các phần sau đây cho bạn thấy cách tự động hóa loại tác vụ này. Có ba bước cơ bản: sử dụng list.files() để liệt kê tất cả các tệp trong một thư mục, sau đó sử dụng purrr::map() để đọc từng tệp vào một list, rồi sử dụng purrr::list_rbind() để kết hợp chúng thành một data frame duy nhất. Sau đó chúng ta sẽ thảo luận cách xử lý các tình huống ngày càng không đồng nhất, khi bạn không thể làm chính xác cùng một việc cho mỗi tệp.
26.3.1 Liệt kê các tệp trong một thư mục
Đúng như tên gọi, list.files() liệt kê các tệp trong một thư mục. Bạn hầu như luôn sử dụng ba argument:
Đối số đầu tiên,
path, là thư mục cần tìm.patternlà một regular expression (regular expression) dùng để lọc tên tệp. Mẫu phổ biến nhất là thứ gì đó như[.]xlsx$hoặc[.]csv$để tìm tất cả các tệp có phần mở rộng được chỉ định.full.namesxác định liệu tên thư mục có nên được bao gồm trong đầu ra hay không. Bạn hầu như luôn muốn giá trị này làTRUE.
Để làm cho ví dụ động lực của chúng ta trở nên cụ thể, cuốn sách này chứa một thư mục với 12 spreadsheet excel chứa dữ liệu từ package gapminder. Thư mục này có thể được tìm thấy tại https://github.com/hadley/r4ds/tree/main/data/gapminder. Mỗi tệp chứa dữ liệu một năm cho 142 quốc gia. Chúng ta có thể liệt kê tất cả chúng với lệnh gọi thích hợp đến 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 Danh sách
Bây giờ chúng ta có 12 đường dẫn này, chúng ta có thể gọi read_excel() 12 lần để có 12 data frame:
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")Nhưng việc đặt mỗi spreadsheet vào biến riêng sẽ khiến việc làm việc với chúng trở nên khó khăn ở một vài bước sau. Thay vào đó, sẽ dễ dàng hơn khi đặt chúng vào một đối tượng duy nhất. Một list (list) là công cụ hoàn hảo cho công việc này:
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")
)Bây giờ khi bạn có các data frame này trong một list, làm thế nào để lấy một cái ra? Bạn có thể sử dụng files[[i]] để trích xuất phần tử thứ 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 rowsChúng ta sẽ quay lại [[ chi tiết hơn trong Phần 27.3.
26.3.3 purrr::map() và list_rbind()
Đoạn code để thu thập các data frame đó vào một list “bằng tay” về cơ bản cũng nhàm chán để gõ như code đọc từng tệp một. May mắn thay, chúng ta có thể sử dụng purrr::map() để tận dụng tốt hơn vector paths của mình. map() tương tự như across(), nhưng thay vì làm gì đó với mỗi column trong một data frame, nó làm gì đó với mỗi phần tử của một vector. map(x, f) là viết tắt của:
list(
f(x[[1]]),
f(x[[2]]),
...,
f(x[[n]])
)Vì vậy chúng ta có thể sử dụng map() để lấy một list gồm 12 data frame:
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(Đây là một cấu trúc dữ liệu khác không hiển thị đặc biệt gọn gàng với str() nên bạn có thể muốn tải nó vào RStudio và kiểm tra bằng View()).
Bây giờ chúng ta có thể sử dụng purrr::list_rbind() để kết hợp list các data frame đó thành một data frame duy nhất:
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 rowsHoặc chúng ta có thể thực hiện cả hai bước cùng lúc trong một pipeline:
paths |>
map(readxl::read_excel) |>
list_rbind()Điều gì nếu chúng ta muốn truyền thêm argument cho read_excel()? Chúng ta sử dụng cùng kỹ thuật đã dùng với across(). Ví dụ, thường hữu ích khi xem qua vài row đầu tiên của dữ liệu với 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Điều này cho thấy rõ rằng có gì đó đang thiếu: không có column year vì giá trị đó được ghi trong đường dẫn, không phải trong các tệp riêng lẻ. Chúng ta sẽ giải quyết vấn đề đó tiếp theo.
26.3.4 Dữ liệu trong đường dẫn
Đôi khi tên tệp chính là dữ liệu. Trong ví dụ này, tên tệp chứa năm, thông tin không được ghi ở nơi khác trong các tệp riêng lẻ. Để đưa column đó vào data frame cuối cùng, chúng ta cần làm hai việc:
Đầu tiên, chúng ta đặt tên cho vector các đường dẫn. Cách dễ nhất để làm điều này là với function set_names(), có thể nhận một function. Ở đây chúng ta sử dụng basename() để trích xuất chỉ tên tệp từ đường dẫn đầy đủ:
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"Những tên đó được tự động mang theo bởi tất cả các function map, nên list các data frame sẽ có cùng những tên đó:
files <- paths |>
set_names(basename) |>
map(readxl::read_excel)Điều đó khiến lệnh gọi map() này là viết tắt của:
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")
)Bạn cũng có thể sử dụng [[ để trích xuất phần tử theo tên:
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 rowsSau đó chúng ta sử dụng argument names_to của list_rbind() để yêu cầu nó lưu các tên vào một column mới gọi là year rồi sử dụng readr::parse_number() để trích xuất số từ string.
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 rowsTrong các trường hợp phức tạp hơn, có thể có các biến khác được lưu trong tên thư mục, hoặc có thể tên tệp chứa nhiều mẩu dữ liệu. Trong trường hợp đó, hãy sử dụng set_names() (không có argument nào) để ghi lại đường dẫn đầy đủ, và sau đó sử dụng tidyr::separate_wider_delim() và các function liên quan để biến chúng thành các column hữu ích.
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 Lưu công việc của bạn
Bây giờ khi bạn đã làm tất cả công việc khó khăn này để có được một data frame gọn gàng (tidy), đây là thời điểm tuyệt vời để lưu công việc của bạn:
gapminder <- paths |>
set_names(basename) |>
map(readxl::read_excel) |>
list_rbind(names_to = "year") |>
mutate(year = parse_number(year))
write_csv(gapminder, "gapminder.csv")Bây giờ khi bạn quay lại vấn đề này trong tương lai, bạn có thể đọc vào một tệp csv duy nhất. Đối với các tập dữ liệu lớn và phong phú hơn, sử dụng parquet có thể là lựa chọn tốt hơn .csv, như đã thảo luận trong Phần 22.4.
Nếu bạn đang làm việc trong một dự án, chúng tôi gợi ý đặt tên tệp thực hiện công việc chuẩn bị dữ liệu kiểu này là 0-cleanup.R. Số 0 trong tên tệp gợi ý rằng nó nên được chạy trước bất kỳ thứ gì khác.
Nếu các tệp dữ liệu đầu vào của bạn thay đổi theo thời gian, bạn có thể cân nhắc học một công cụ như targets để thiết lập code dọn dẹp dữ liệu tự động chạy lại mỗi khi một trong các tệp đầu vào bị thay đổi.
26.3.6 Nhiều loop đơn giản
Ở đây chúng ta chỉ tải dữ liệu trực tiếp từ đĩa, và may mắn có được một tập tidy data. Trong hầu hết các trường hợp, bạn sẽ cần thực hiện thêm một số thao tác dọn dẹp, và bạn có hai lựa chọn cơ bản: bạn có thể thực hiện một loop với một function phức tạp, hoặc thực hiện nhiều loop với các function đơn giản. Theo kinh nghiệm của chúng tôi, hầu hết mọi người thường chọn một loop phức tạp trước, nhưng bạn thường tốt hơn khi thực hiện nhiều loop đơn giản.
Ví dụ, hãy tưởng tượng rằng bạn muốn đọc một loạt tệp, lọc bỏ các missing value, xoay dữ liệu, rồi kết hợp chúng. Một cách tiếp cận vấn đề là viết một function nhận một tệp và thực hiện tất cả các bước đó rồi gọi map() một lần:
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()Ngoài ra, bạn có thể thực hiện từng bước của process_file() cho mỗi tệp:
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()Chúng tôi khuyến nghị cách tiếp cận này vì nó ngăn bạn bị ám ảnh với việc làm đúng tệp đầu tiên trước khi chuyển sang các tệp còn lại. Bằng cách xem xét tất cả dữ liệu khi thực hiện dọn dẹp, bạn có nhiều khả năng suy nghĩ toàn diện hơn và đạt được kết quả chất lượng cao hơn.
Trong ví dụ cụ thể này, có một cách tối ưu hóa khác bạn có thể thực hiện, bằng cách kết nối tất cả các data frame lại sớm hơn. Sau đó bạn có thể dựa vào hành vi thông thường của 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 Dữ liệu không đồng nhất
Thật không may, đôi khi không thể đi từ map() thẳng đến list_rbind() vì các data frame quá không đồng nhất khiến list_rbind() hoặc thất bại hoặc cho ra một data frame không hữu ích lắm. Trong trường hợp đó, vẫn hữu ích khi bắt đầu bằng việc tải tất cả các tệp:
files <- paths |>
map(readxl::read_excel)Sau đó một chiến lược rất hữu ích là nắm bắt cấu trúc của các data frame để bạn có thể khám phá nó bằng các kỹ năng khoa học dữ liệu của mình. Một cách để làm điều đó là với function df_types tiện lợi này6 trả về một tibble với một row cho mỗi column:
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 0Sau đó bạn có thể áp dụng function này cho tất cả các tệp, và có thể thực hiện một số thao tác xoay để dễ dàng thấy sự khác biệt ở đâu. Ví dụ, điều này giúp dễ dàng xác minh rằng các spreadsheet gapminder mà chúng ta đã làm việc đều khá đồng nhất:
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 rowsNếu các tệp có định dạng không đồng nhất, bạn có thể cần xử lý thêm trước khi có thể hợp nhất chúng thành công. Thật không may, chúng tôi sẽ để bạn tự tìm hiểu điều đó, nhưng bạn có thể muốn đọc về map_if() và map_at(). map_if() cho phép bạn chọn lọc sửa đổi các phần tử của một list dựa trên giá trị của chúng; map_at() cho phép bạn chọn lọc sửa đổi các phần tử dựa trên tên của chúng.
26.3.8 Xử lý lỗi
Đôi khi cấu trúc dữ liệu của bạn có thể đủ lộn xộn đến mức bạn thậm chí không thể đọc tất cả các tệp bằng một lệnh duy nhất. Và rồi bạn sẽ gặp một trong những nhược điểm của map(): nó thành công hoặc thất bại toàn bộ. map() sẽ hoặc đọc thành công tất cả các tệp trong một thư mục hoặc thất bại với một lỗi, đọc được không tệp nào. Điều này thật phiền: tại sao một lỗi lại ngăn bạn truy cập tất cả các thành công khác?
May mắn thay, purrr đi kèm với một function trợ giúp để giải quyết vấn đề này: possibly(). possibly() là cái được gọi là toán tử function (function operator): nó nhận một function và trả về một function với hành vi đã được sửa đổi. Cụ thể, possibly() thay đổi một function từ việc báo lỗi thành trả về một giá trị mà bạn chỉ định:
files <- paths |>
map(possibly(\(path) readxl::read_excel(path), NULL))
data <- files |> list_rbind()Điều này hoạt động đặc biệt tốt ở đây vì list_rbind(), giống như nhiều function tidyverse, tự động bỏ qua các NULL.
Bây giờ bạn có tất cả dữ liệu có thể đọc được dễ dàng, và đây là lúc giải quyết phần khó là tìm ra tại sao một số tệp không tải được và phải làm gì với chúng. Bắt đầu bằng cách lấy các đường dẫn bị lỗi:
failed <- map_vec(files, is.null)
paths[failed]
#> character(0)Sau đó gọi lại function nhập cho từng lỗi và tìm hiểu xem có vấn đề gì.
26.4 Lưu nhiều đầu ra
Trong phần trước, bạn đã học về map(), hữu ích cho việc đọc nhiều tệp vào một đối tượng duy nhất. Trong phần này, chúng ta sẽ khám phá vấn đề ngược lại: làm thế nào bạn có thể lấy một hoặc nhiều đối tượng R và lưu chúng vào một hoặc nhiều tệp? Chúng ta sẽ khám phá thách thức này bằng ba ví dụ:
- Lưu nhiều data frame vào một database.
- Lưu nhiều data frame vào nhiều tệp
.csv. - Lưu nhiều biểu đồ vào nhiều tệp
.png.
26.4.1 Ghi vào database
Đôi khi khi làm việc với nhiều tệp cùng lúc, không thể đưa tất cả dữ liệu của bạn vào bộ nhớ cùng lúc, và bạn không thể dùng map(files, read_csv). Một cách tiếp cận để giải quyết vấn đề này là tải dữ liệu vào một database để bạn có thể truy cập chỉ những phần bạn cần với dbplyr.
Nếu may mắn, package database bạn đang sử dụng sẽ cung cấp một function tiện lợi nhận một vector các đường dẫn và tải tất cả chúng vào database. Đây là trường hợp với duckdb_read_csv() của duckdb:
con <- DBI::dbConnect(duckdb::duckdb())
duckdb::duckdb_read_csv(con, "gapminder", paths)Điều này sẽ hoạt động tốt ở đây, nhưng chúng ta không có các tệp csv, thay vào đó là các spreadsheet excel. Vì vậy chúng ta sẽ phải làm “thủ công”. Học cách làm thủ công cũng sẽ giúp bạn khi bạn có một loạt csv và database mà bạn đang làm việc không có một function nào tải tất cả chúng vào.
Chúng ta cần bắt đầu bằng cách tạo một bảng mà chúng ta sẽ điền dữ liệu vào. Cách dễ nhất để làm điều này là tạo một mẫu (template), một data frame giả chứa tất cả các column chúng ta muốn, nhưng chỉ một phần nhỏ dữ liệu. Đối với dữ liệu gapminder, chúng ta có thể tạo mẫu đó bằng cách đọc một tệp duy nhất và thêm năm vào:
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 rowsBây giờ chúng ta có thể kết nối đến database, và sử dụng DBI::dbCreateTable() để biến mẫu thành một bảng database:
con <- DBI::dbConnect(duckdb::duckdb())
DBI::dbCreateTable(con, "gapminder", template)dbCreateTable() không sử dụng dữ liệu trong template, chỉ tên biến và kiểu dữ liệu. Vì vậy nếu chúng ta kiểm tra bảng gapminder bây giờ, bạn sẽ thấy nó trống nhưng có các biến cần thiết với các kiểu mong đợi:
con |> tbl("gapminder")
#> # Source: table<gapminder> [?? x 6]
#> # Database: DuckDB 1.5.1 [root@Darwin 25.3.0:R 4.5.3/:memory:]
#> # ℹ 6 variables: country <chr>, continent <chr>, lifeExp <dbl>, pop <dbl>,
#> # gdpPercap <dbl>, year <dbl>Tiếp theo, chúng ta cần một function nhận một đường dẫn tệp đơn, đọc nó vào R, và thêm kết quả vào bảng gapminder. Chúng ta có thể làm điều đó bằng cách kết hợp read_excel() với DBI::dbAppendTable():
append_file <- function(path) {
df <- readxl::read_excel(path)
df$year <- parse_number(basename(path))
DBI::dbAppendTable(con, "gapminder", df)
}Bây giờ chúng ta cần gọi append_file() một lần cho mỗi phần tử của paths. Điều đó chắc chắn có thể làm được với map():
paths |> map(append_file)Nhưng chúng ta không quan tâm đến đầu ra của append_file(), nên thay vì map(), sử dụng walk() sẽ gọn gàng hơn một chút. walk() làm chính xác như map() nhưng bỏ đi đầu ra:
paths |> walk(append_file)Bây giờ chúng ta có thể xem liệu chúng ta có tất cả dữ liệu trong bảng không:
26.4.2 Ghi tệp csv
Cùng nguyên tắc cơ bản áp dụng nếu chúng ta muốn ghi nhiều tệp csv, mỗi tệp cho một nhóm. Hãy tưởng tượng rằng chúng ta muốn lấy dữ liệu ggplot2::diamonds và lưu một tệp csv cho mỗi clarity. Đầu tiên chúng ta cần tạo các tập dữ liệu riêng lẻ đó. Có nhiều cách để làm điều đó, nhưng có một cách chúng tôi đặc biệt thích: 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Điều này cho chúng ta một tibble mới với tám row và hai column. clarity là biến nhóm của chúng ta và data là một column list chứa một tibble cho mỗi giá trị duy nhất của 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 rowsTrong khi đang ở đây, hãy tạo một column chứa tên tệp đầu ra, sử dụng mutate() và 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 rowsVì vậy nếu chúng ta lưu các data frame này bằng tay, chúng ta có thể viết thứ gì đó như:
Điều này hơi khác so với các lần sử dụng map() trước đây vì có hai argument đang thay đổi, không chỉ một. Điều đó có nghĩa là chúng ta cần một function mới: map2(), thay đổi cả argument thứ nhất và thứ hai. Và vì chúng ta lại không quan tâm đến đầu ra, chúng ta muốn walk2() thay vì map2(). Điều đó cho chúng ta:
walk2(by_clarity$data, by_clarity$path, write_csv)26.4.3 Lưu biểu đồ
Chúng ta có thể áp dụng cùng cách tiếp cận cơ bản để tạo nhiều biểu đồ. Hãy bắt đầu bằng cách tạo một function vẽ biểu đồ mà chúng ta muốn:
carat_histogram <- function(df) {
ggplot(df, aes(x = carat)) + geom_histogram(binwidth = 0.1)
}
carat_histogram(by_clarity$data[[1]])
Bây giờ chúng ta có thể sử dụng map() để tạo một list nhiều biểu đồ7 và các đường dẫn tệp cuối cùng của chúng:
Sau đó sử dụng walk2() với ggsave() để lưu mỗi biểu đồ:
Đây là viết tắt của:
26.5 Tóm tắt
Trong chương này, bạn đã thấy cách sử dụng iterate tường minh để giải quyết ba vấn đề thường gặp khi làm khoa học dữ liệu: thao tác nhiều column, đọc nhiều tệp và lưu nhiều đầu ra. Nhưng nói chung, iterate là một siêu năng lực: nếu bạn biết đúng kỹ thuật iterate, bạn có thể dễ dàng đi từ việc sửa một vấn đề sang sửa tất cả các vấn đề. Khi bạn đã thành thạo các kỹ thuật trong chương này, chúng tôi rất khuyến khích bạn tìm hiểu thêm bằng cách đọc chương Functionals của Advanced R và tham khảo trang web purrr.
Nếu bạn biết nhiều về iterate trong các ngôn ngữ khác, bạn có thể ngạc nhiên rằng chúng tôi không thảo luận về loop for. Đó là vì hướng phân tích dữ liệu của R thay đổi cách chúng ta lặp: trong hầu hết các trường hợp bạn có thể dựa vào một thành ngữ có sẵn để làm gì đó cho mỗi column hoặc mỗi nhóm. Và khi bạn không thể, bạn thường có thể sử dụng một công cụ lập trình function như map() để làm gì đó với mỗi phần tử của một list. Tuy nhiên, bạn sẽ thấy loop for trong code ngoài tự nhiên, nên bạn sẽ học về chúng trong chương tiếp theo nơi chúng ta thảo luận một số công cụ base R quan trọng.
Vô danh, vì chúng ta chưa bao giờ đặt tên cho nó một cách tường minh bằng
<-. Một thuật ngữ khác mà lập trình viên sử dụng cho điều này là “hàm lambda”.↩︎Trong code cũ hơn, bạn có thể thấy cú pháp trông giống
~ .x + 1. Đây là một cách khác để viết function vô danh nhưng nó chỉ hoạt động bên trong các function tidyverse và luôn sử dụng tên biến.x. Chúng tôi hiện khuyến nghị sử dụng cú pháp cơ bản,\(x) x + 1.↩︎Hiện tại bạn không thể thay đổi thứ tự các column, nhưng bạn có thể sắp xếp lại chúng sau đó bằng
relocate()hoặc tương tự.↩︎Có thể một ngày nào đó sẽ có, nhưng hiện tại chúng tôi chưa thấy cách nào.↩︎
Nếu thay vào đó bạn có một thư mục các tệp csv với cùng định dạng, bạn có thể sử dụng kỹ thuật từ Phần 7.4.↩︎
Chúng tôi sẽ không giải thích cách nó hoạt động, nhưng nếu bạn xem tài liệu của các function được sử dụng, bạn sẽ có thể tự suy ra.↩︎
Bạn có thể in
by_clarity$plotđể có một hoạt ảnh thô — bạn sẽ nhận được một biểu đồ cho mỗi phần tử củaplots. LƯU Ý: điều này không xảy ra với tôi.↩︎