Table of Contents
引言
可视化是生成洞见的重要工具,但你很少能拿到正好符合绘图需求的数据形式。通常你需要创建一些新的变量或汇总,以便用数据回答你的问题;或者你只是想重命名变量、重新排列观测值,让数据更易于处理。接下来,你将学习如何完成这些操作(以及更多内容)。通过 dplyr 包介绍数据转换,并使用一个关于 2013 年从纽约市起飞航班的新数据集。
本章的目标是为你概述用于转换数据框的所有关键工具。我们将从对数据框的行进行操作的函数开始,然后介绍对列进行操作的函数,接着回过头来进一步讨论管道(pipe),这是一个用于组合各种动词的重要工具。随后我们将介绍如何按分组进行操作。最后,本章将以一个案例研究收尾,展示这些函数的实际应用。
先决条件
在本章中,我们将重点介绍 dplyr 包。我们将使用 nycflights13 包中的数据来说明关键思想,并使用 ggplot2 帮助我们理解这些数据。
# install.packages("nycflights13")library(nycflights13)# install.packages("dplyr")library(dplyr)请仔细留意在加载 包时打印的冲突信息,如果有的话。它会告诉你 dplyr 覆盖了 base R 中的一些函数。如果在加载 dplyr 之后你仍想使用这些函数的 base 版本,就需要使用它们的完整名称:stats::filter() 和 stats::lag()。到目前为止,我们大多忽略了函数来自哪个包,因为通常这并不重要。然而,了解函数所属的包可以帮助你查找帮助文档以及发现相关函数,因此当我们需要明确函数来源时,会使用与 R 相同的语法:packagename::functionname()。
nycflights13
为了探索 dplyr 的基本动词,我们将使用 nycflights13::flights。该数据集包含了 2013 年从纽约市出发的全部 336,776 趟航班。数据来源于美国运输统计局(US Bureau of Transportation Statistics),并在 ?flights 中有文档说明。
> flights# A tibble: 336,776 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 517 515 2 830 819 2 2013 1 1 533 529 4 850 830 3 2013 1 1 542 540 2 923 850 4 2013 1 1 544 545 -1 1004 1022 5 2013 1 1 554 600 -6 812 837 6 2013 1 1 554 558 -4 740 728 7 2013 1 1 555 600 -5 913 854 8 2013 1 1 557 600 -3 709 723 9 2013 1 1 557 600 -3 838 84610 2013 1 1 558 600 -2 753 745# ℹ 336,766 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rowsflights 是一个 tibble(tibble,一种数据框的特殊类型)。tidyverse 使用 tibble 来避免一些常见的“坑”。tibbles 和数据框之间最重要的区别在于它们的打印方式:tibble 是为大型数据集设计的,所以它们只显示前几行,以及能够在一屏中放下的列。要查看全部内容,有几种选项。如果你在使用 RStudio,那么最方便的可能是 View(flights),它会打开一个交互式、可滚动且可筛选的视图。否则你也可以使用 print(flights, width = Inf) 来显示所有列,或者使用 glimpse():
> glimpse(flights)Rows: 336,776Columns: 19$ year <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…$ month <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…$ day <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…$ dep_time <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 558, …$ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 600, …$ dep_delay <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2, -1…$ arr_time <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 849,…$ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 851,…$ arr_delay <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7, -1…$ carrier <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6", "…$ flight <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301, 4…$ tailnum <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N394…$ origin <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LGA",…$ dest <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IAD",…$ air_time <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149, 1…$ distance <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 733, …$ hour <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6…$ minute <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0…$ time_hour <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-01 0…在这两种视图中,变量名后面会跟着一些缩写,用来告诉你每个变量的类型:<int> 表示 integer(整数),<dbl> 表示 double(也就是实数),<chr> 表示 character(也就是字符串),而 <dttm> 表示 date-time(日期时间)。这些信息很重要,因为你能对某一列执行的操作在很大程度上取决于它的“类型”。
dplyr 基础知识
你即将学习 dplyr 的主要动词(函数)。它们将使你能够解决绝大多数数据处理方面的挑战。但在讨论它们各自的差异之前,先说明它们的共同点:
- 第一个参数始终是一个数据框(data frame)。
- 后续参数通常会用变量名(不加引号)来说明要对哪些列进行操作。
- 输出总是一个新的数据框。
- 因为每个动词只擅长做一件事,所以要解决复杂问题通常需要把多个动词组合起来,而我们将用管道
|>来完成。先简要说明:管道会把左边的内容传递给右边的函数,因此 等价于 ,而 等价于 。最容易读出管道的方式是“then”(然后)。即使你还没有学到具体细节,你也依然能对下面的代码有一个大致的理解:
dplyr 的动词根据它们作用的对象被分为四组:行(rows)、列(columns)、分组(groups)或表(tables)。接下来,你将学习针对行、列和分组最重要的动词。让我们开始吧!
行
作用于数据集“行”的最重要动词是 filter() 和 arrange():filter() 会改变哪些行被保留(但不改变它们的顺序),而 arrange() 会改变行的顺序(但不改变哪些行被保留)。这两个函数都只会影响行,列保持不变。我们还会讨论 distinct(),它用于查找具有唯一值的行。与 arrange() 和 filter() 不同,distinct() 除了可以可选地修改列之外,还能实现其功能。
filter()
filter() 允许你根据各列的取值来保留行。第一个参数是数据框(data frame)。第二个及后续参数是必须为真(成立)才能保留该行的条件。比如,我们可以找到所有延误超过 120 分钟(两个小时)的航班:
> flights |> + filter(dep_delay > 120)# A tibble: 9,723 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 848 1835 853 1001 1950 2 2013 1 1 957 733 144 1056 853 3 2013 1 1 1114 900 134 1447 1222 4 2013 1 1 1540 1338 122 2020 1825 5 2013 1 1 1815 1325 290 2120 1542 6 2013 1 1 1842 1422 260 1958 1535 7 2013 1 1 1856 1645 131 2212 2005 8 2013 1 1 1934 1725 129 2126 1855 9 2013 1 1 1938 1703 155 2109 182310 2013 1 1 1942 1705 157 2124 1830# ℹ 9,713 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows除了 >(大于),你还可以使用 >=(大于等于)、<(小于)、<=(小于等于)、==(等于)和 !=(不等于)。你也可以用 & 或 , 来组合条件,表示“and”(同时满足两个条件);或者用 | 来表示“or”(满足任一条件):
> flights |> + filter(month == 1 & day == 1)# A tibble: 842 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 517 515 2 830 819 2 2013 1 1 533 529 4 850 830 3 2013 1 1 542 540 2 923 850 4 2013 1 1 544 545 -1 1004 1022 5 2013 1 1 554 600 -6 812 837 6 2013 1 1 554 558 -4 740 728 7 2013 1 1 555 600 -5 913 854 8 2013 1 1 557 600 -3 709 723 9 2013 1 1 557 600 -3 838 84610 2013 1 1 558 600 -2 753 745# ℹ 832 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows当你把 | 和 == 结合使用时,有一个很有用的快捷方式:%in%。它会保留那些变量等于右侧任意一个取值的行:
> flights |> + filter(month %in% c(1, 2))# A tibble: 51,955 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 517 515 2 830 819 2 2013 1 1 533 529 4 850 830 3 2013 1 1 542 540 2 923 850 4 2013 1 1 544 545 -1 1004 1022 5 2013 1 1 554 600 -6 812 837 6 2013 1 1 554 558 -4 740 728 7 2013 1 1 555 600 -5 913 854 8 2013 1 1 557 600 -3 709 723 9 2013 1 1 557 600 -3 838 84610 2013 1 1 558 600 -2 753 745# ℹ 51,945 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows当你运行 filter() 时,dplyr 会执行过滤操作,生成一个新的数据框,然后把它打印出来。它不会修改现有的航班数据集,因为 dplyr 的函数从不修改它们的输入。要保存结果,你需要使用赋值运算符 <-:
jan1 <- flights |> filter(month == 1 & day == 1)常见错误
当你刚开始学习 R 时,最容易犯的错误是:在测试是否相等时用 =,而不是 ==。一旦发生这种情况,filter() 会提示你:
> flights |> + filter(month = 1)Error in `filter()`:! We detected a named input.ℹ This usually means that you've used `=` instead of `==`.ℹ Did you mean `month == 1`?Run `rlang::last_trace()` to see where the error occurred.另一个错误是你会像在英语里那样去写“or”(或)语句:
flights |> filter(month == 1 | 2)这“能用”,就指它不会报错,但它并没有做到你想要的事情:| 会先检查条件 month == 1,然后再检查条件 2,而 2 并不是一个合理的条件来检查。
arrange()
arrange() 会根据列的取值来改变行的顺序。它接受一个数据框,以及一组列名(或更复杂的表达式)用于排序。若你提供不止一个列名,那么后续每增加一个列,都将用于在前一个列的取值相同的情况下打破“并列”。例如,下面的代码会按出发时间进行排序,而这个出发时间由四个列共同表示。我们会先得到最早的年份;然后在同一年内部,再得到最早的月份;依此类推。
> flights |> + arrange(year, month, day, dep_time)# A tibble: 336,776 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum origin dest air_time distance hour minute <int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr> <int> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> 1 2013 1 1 517 515 2 830 819 11 UA 1545 N14228 EWR IAH 227 1400 5 15 2 2013 1 1 533 529 4 850 830 20 UA 1714 N24211 LGA IAH 227 1416 5 29 3 2013 1 1 542 540 2 923 850 33 AA 1141 N619AA JFK MIA 160 1089 5 40 4 2013 1 1 544 545 -1 1004 1022 -18 B6 725 N804JB JFK BQN 183 1576 5 45 5 2013 1 1 554 600 -6 812 837 -25 DL 461 N668DN LGA ATL 116 762 6 0 6 2013 1 1 554 558 -4 740 728 12 UA 1696 N39463 EWR ORD 150 719 5 58 7 2013 1 1 555 600 -5 913 854 19 B6 507 N516JB EWR FLL 158 1065 6 0 8 2013 1 1 557 600 -3 709 723 -14 EV 5708 N829AS LGA IAD 53 229 6 0 9 2013 1 1 557 600 -3 838 846 -8 B6 79 N593JB JFK MCO 140 944 6 010 2013 1 1 558 600 -2 753 745 8 AA 301 N3ALAA LGA ORD 138 733 6 0# ℹ 336,766 more rows# ℹ 1 more variable: time_hour <dttm># ℹ Use `print(n = ...)` to see more rows你可以在 arrange() 里对某个列使用 desc(),根据该列的取值将数据框重新排序为降序(从大到小)。例如,下面这段代码会把航班按延误程度从最多到最少进行排序:
> flights |> + arrange(desc(dep_delay))# A tibble: 336,776 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum origin dest air_time distance hour minute <int> <int> <int> <int> <int> <dbl> <int> <int> <dbl> <chr> <int> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> 1 2013 1 9 641 900 1301 1242 1530 1272 HA 51 N384HA JFK HNL 640 4983 9 0 2 2013 6 15 1432 1935 1137 1607 2120 1127 MQ 3535 N504MQ JFK CMH 74 483 19 35 3 2013 1 10 1121 1635 1126 1239 1810 1109 MQ 3695 N517MQ EWR ORD 111 719 16 35 4 2013 9 20 1139 1845 1014 1457 2210 1007 AA 177 N338AA JFK SFO 354 2586 18 45 5 2013 7 22 845 1600 1005 1044 1815 989 MQ 3075 N665MQ JFK CVG 96 589 16 0 6 2013 4 10 1100 1900 960 1342 2211 931 DL 2391 N959DL JFK TPA 139 1005 19 0 7 2013 3 17 2321 810 911 135 1020 915 DL 2119 N927DA LGA MSP 167 1020 8 10 8 2013 6 27 959 1900 899 1236 2226 850 DL 2007 N3762Y JFK PDX 313 2454 19 0 9 2013 7 22 2257 759 898 121 1026 895 DL 2047 N6716C LGA ATL 109 762 7 5910 2013 12 5 756 1700 896 1058 2020 878 AA 172 N5DMAA EWR MIA 149 1085 17 0# ℹ 336,766 more rows# ℹ 1 more variable: time_hour <dttm># ℹ Use `print(n = ...)` to see more rows请注意,行数并没有变化——我们只是对数据进行重新排序,并没有进行筛选。
distinct()
distinct() 用于查找数据集中所有不重复的行,因此从技术上讲,它主要是基于“行”来操作。大多数时候,你会希望得到某些变量的不同组合,因此你也可以(可选地)提供列名:
> # 移除重复行> flights |> + distinct()# A tibble: 336,776 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 517 515 2 830 819 2 2013 1 1 533 529 4 850 830 3 2013 1 1 542 540 2 923 850 4 2013 1 1 544 545 -1 1004 1022 5 2013 1 1 554 600 -6 812 837 6 2013 1 1 554 558 -4 740 728 7 2013 1 1 555 600 -5 913 854 8 2013 1 1 557 600 -3 709 723 9 2013 1 1 557 600 -3 838 84610 2013 1 1 558 600 -2 753 745# ℹ 336,766 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows> # 移除重复行,保留 origin 和 dest 列> flights |> + distinct(origin, dest)# A tibble: 224 × 2 origin dest <chr> <chr> 1 EWR IAH 2 LGA IAH 3 JFK MIA 4 JFK BQN 5 LGA ATL 6 EWR ORD 7 EWR FLL 8 LGA IAD 9 JFK MCO 10 LGA ORD # ℹ 214 more rows# ℹ Use `print(n = ...)` to see more rows如果你在筛选不重复的行时还想保留其他列,也可以使用 .keep_all = TRUE 选项。
> # 移除重复行,保留 origin 和 dest 列,并保留其他列 > flights |> + distinct(origin, dest, .keep_all = TRUE)# A tibble: 224 × 19 year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 517 515 2 830 819 2 2013 1 1 533 529 4 850 830 3 2013 1 1 542 540 2 923 850 4 2013 1 1 544 545 -1 1004 1022 5 2013 1 1 554 600 -6 812 837 6 2013 1 1 554 558 -4 740 728 7 2013 1 1 555 600 -5 913 854 8 2013 1 1 557 600 -3 709 723 9 2013 1 1 557 600 -3 838 84610 2013 1 1 558 600 -2 753 745# ℹ 214 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows这些航班之所以都出现在 1 月 1 日并非巧合:distinct() 会在数据集中找到某个“唯一行”的第一次出现,并丢弃其余重复项。
如果你想找的是出现次数,那么最好把 distinct() 换成 count()。通过设置 sort = TRUE,你还可以按“出现次数”从高到低对结果进行排序。
> # 计算每个 origin 和 dest 组合的航班数量,并按数量降序排序> flights |>+ count(origin, dest, sort = TRUE)# A tibble: 224 × 3 origin dest n <chr> <chr> <int> 1 JFK LAX 11262 2 LGA ATL 10263 3 LGA ORD 8857 4 JFK SFO 8204 5 LGA CLT 6168 6 EWR ORD 6100 7 JFK BOS 5898 8 LGA MIA 5781 9 JFK MCO 546410 EWR BOS 5327# ℹ 214 more rows# ℹ Use `print(n = ...)` to see more rows练习
- 在每个条件的单个流水线(pipeline)中,找到所有满足该条件的航班:
- 到达延误达到两小时或以上
- 飞往休斯敦(IAH 或 HOU)
- 由联合航空(United)、美国航空(American)或达美航空(Delta)运营
- 在夏季出发(7 月、8 月和 9 月)
- 到达晚于两小时以上,但出发并没有晚点
- 至少延误一小时,但在飞行过程中弥补了超过 30 分钟
library(nycflights13)library(dplyr)# 1) 到达延误达到两小时或以上flights |> filter(arr_delay >= 120)# 2) 飞往休斯敦(IAH 或 HOU)flights |> filter(dest %in% c("IAH", "HOU"))# 3) 由联合航空(United)、美国航空(American)或达美航空(Delta)运营flights |> filter(carrier %in% c("UA", "AA", "DL"))# 4) 在夏季出发(7 月、8 月和 9 月)flights |> filter(month %in% c(7, 8, 9))# 5) 到达晚于两小时以上,但出发并没有晚点flights |> filter(arr_delay > 120, dep_delay <= 0)# 6) 至少延误一小时,但在飞行过程中弥补了超过 30 分钟# (即:dep_delay >= 60 且 arrived_delay 比 dep_delay 少超过 30)flights |> filter( dep_delay >= 60, dep_delay - arr_delay > 30 )- 对航班进行排序,以找出出发延误最长的航班。再找出在早晨最早出发的航班。
flights |> arrange(desc(dep_delay), dep_time) |> slice(1) # 选择第一行,即 dep_delay 最大的航班- 对航班进行排序以找出最快的航班。(提示:试着在函数里加入一个数学计算。)
flights |> mutate(speed = distance / (air_time / 60)) |> # 计算速度,单位为英里/小时 arrange(desc(speed)) |> # 按照速度降序排序 slice(1) # 选择第一行,即速度最快的航班- 哪些航班飞行距离最远?哪些航班飞行距离最短?
# 飞行距离最远的一班(含并列)flights |> filter(distance == max(distance, na.rm = TRUE))# 飞行距离最短的一班(含并列)flights |> filter(distance == min(distance, na.rm = TRUE))- 如果你同时使用
filter()和arrange(),它们使用的先后顺序会有影响吗?为什么?想一想结果会怎样变化,以及这两个函数需要做多少工作。
一般建议把 filter() 尽量放前面(越早筛掉越好),让 arrange()/group_by() 等“昂贵”的操作尽可能在更少的行上进行。
列
有四个重要的动词会影响列而不改变行:mutate() 创建从现有列派生出来的新列,select() 改变哪些列会出现,rename() 改变列名,relocate() 改变列的顺序位置。
mutate()
mutate() 的作用是添加新的列,这些列是根据现有的列计算得来的。你将学习一大组函数,用来处理不同类型的变量并对它们进行操作。就目前而言,我们先专注于基础代数:它让我们可以计算收益,也就是延误航班在空中补回了多少时间,以及速度(英里每小时):
flights |> mutate( gain = dep_delay - arr_delay, speed = distance / air_time * 60 )默认情况下,mutate() 会把新列添加到数据集的右侧,这使得你很难看清这里发生了什么。我们可以使用 .before 参数,而是把这些变量添加到左侧:
> flights |> + mutate(+ gain = dep_delay - arr_delay,+ speed = distance / air_time * 60,+ .before = 1+ )# A tibble: 336,776 × 21 gain speed year month day dep_time sched_dep_time dep_delay arr_time <dbl> <dbl> <int> <int> <int> <int> <int> <dbl> <int> 1 -9 370. 2013 1 1 517 515 2 830 2 -16 374. 2013 1 1 533 529 4 850 3 -31 408. 2013 1 1 542 540 2 923 4 17 517. 2013 1 1 544 545 -1 1004 5 19 394. 2013 1 1 554 600 -6 812 6 -16 288. 2013 1 1 554 558 -4 740 7 -24 404. 2013 1 1 555 600 -5 913 8 11 259. 2013 1 1 557 600 -3 709 9 5 405. 2013 1 1 557 600 -3 83810 -10 319. 2013 1 1 558 600 -2 753# ℹ 336,766 more rows# ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, carrier <chr>,# flight <int>, tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,# distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows. 表示 .before 是传给该函数的一个参数,而不是我们正在创建的第三个新变量的名字。你也可以使用 .after 来把内容加在某个变量之后;在 .before 和 .after 中,你都可以用变量名来代替位置参数。比如,我们可以把新变量加在 day 之后:
flights |> mutate( gain = dep_delay - arr_delay, speed = distance / air_time * 60, .after = day )另外,你也可以使用 .keep 参数来控制保留哪些变量。尤其有用的参数是 "used":它指定我们只保留在 mutate() 步骤中所涉及或创建的列。例如,下面的输出将只包含变量 dep_delay、arr_delay、air_time、gain、hours 和 gain_per_hour。
flights |> mutate( gain = dep_delay - arr_delay, hours = air_time / 60, gain_per_hour = gain / hours, .keep = "used" )请注意,由于我们并没有把上面这次计算的结果重新赋值回 flights,因此新的变量 gain、hours 和 gain_per_hour 只会被打印出来,但不会被存储到数据框中。并且,如果我们希望让它们能够在未来使用时继续出现在数据框里,我们就需要认真考虑:是否要把结果赋值回 flights,用更多变量覆盖原始数据框;还是将结果保存到一个新的对象中。很多时候,正确做法是创建一个新的对象,并用具有信息量的名称来表明其内容,例如 delay_gain。当然,你也可能有充分理由选择覆盖掉 flights。
select()
拿到包含数百甚至上千个变量的数据集并不罕见。在这种情况下,最先面临的挑战往往只是:关注你真正感兴趣的变量。select() 允许你基于变量名所进行的操作,快速缩小到一个有用的子集:
按名称选择列:
flights |> select(year, month, day)选择从 year 到 day(包含 day)之间的所有列:
flights |> select(year:day)选择所有列,但不包括从 year 到 day(含)之间的列:
flights |> select(!year:day)从历史上看,这个操作使用的是 - 而不是 !,所以你很可能在实际中会看到这种写法。两个运算符的作用相同,但在行为上有一些微妙差异。我们建议使用 !,因为它读作 “not”,并且与 & 和 | 组合得很自然。
选择所有字符列:
flights |> select(where(is.character))在 select() 内部,你可以使用一些辅助函数:
starts_with("abc"):匹配以 “abc” 开头的名称。ends_with("xyz"):匹配以 “xyz” 结尾的名称。contains("ijk"):匹配包含 “ijk” 的名称。num_range("x", 1:3):匹配 x1、x2 和 x3。
更多细节请查看 ?select。一旦你了解正则表达式,你也就能使用 matches() 来选择与某种模式相匹配的变量。
你可以在选择 select() 变量时使用 = 来重命名:新的名称出现在 = 的左侧,旧的变量出现在右侧:
flights |> select(tail_num = tailnum)rename()
如果你想保留所有现有变量,只是想重命名其中一些,那么就可以使用 rename() 而不是 select():
flights |> rename(tail_num = tailnum)如果你有一堆命名不一致的列,而且手动逐一修正会很痛苦,可以看看 janitor::clean_names(),它提供了一些有用的自动清理功能。
relocate()
使用 relocate() 来移动变量。你可能想把相关的变量收集到一起,或者把重要的变量移到前面。默认情况下,relocate() 会把变量移动到最前面:
flights |> relocate(time_hour, air_time)You can also specify where to put them using the .before and .after arguments, just like in mutate():
flights |> relocate(year:dep_time, .after = time_hour)flights |> relocate(starts_with("arr"), .before = dep_time)练习
- 从 flights 中选择 dep_time、dep_delay、arr_time 和 arr_delay。
flights |> select(dep_time, dep_delay, arr_time, arr_delay)- 如果在一次
select()调用中多次指定同一个变量名,会发生什么?
在 R 的 dplyr::select() 里同一个变量名在同一次 select() 调用中出现多次,通常会导致以下效果之一(取决于版本与具体写法):
最常见情况:只保留一次
select() 会对结果进行去重(不会让同一列在输出里重复出现多份)。
也可能:报错或忽略重复
如果你的写法属于某些“选择语法”(比如和负选择、重命名、范围选择混在一起),有时会触发提示/报错,或以“后面覆盖/忽略”的方式处理。
- 运行下面这段代码的结果是否让你感到意外?默认情况下,
select的这些辅助函数是如何处理大写和小写的?你又该如何改变这种默认行为?
flights |> select(contains("TIME"))在 dplyr 的 select() 辅助函数(如 contains())里,默认是不区分大小写(也就是 TIME 会匹配到 time)。
- 将
air_time重命名为air_time_min以表明其计量单位,并把它移动到数据框的开头。
flights |> rename(air_time_min = air_time) |> # 将 air_time 列重命名为 air_time_min relocate(air_time_min, .before = 1)- 为什么下面这段代码不起作用?这个报错信息是什么意思?
> flights |> + select(tailnum) |> + arrange(arr_delay)Error in `arrange()`:ℹ In argument: `..1 = arr_delay`.Caused by error:! object 'arr_delay' not foundRun `rlang::last_trace()` to see where the error occurred.flights |> arrange(arr_delay) |> # 先排序再选择 select(tailnum) # 选择后只有一列管道
我们已经向你展示了上面管道的一些简单示例,但它真正的强大之处在于当你开始把多个动词组合在一起时就体现出来了。比如,假设你想找出飞往休斯顿 IAH 机场的最快航班:你需要把 filter()、mutate()、select() 和 arrange() 组合在一起:
flights |> filter(dest == "IAH") |> mutate(speed = distance / air_time * 60) |> select(year:day, dep_time, carrier, flight, speed) |> arrange(desc(speed))尽管这个管道有四个步骤,但因为每行开头都是动词,所以很容易快速浏览:先从 flights 数据开始,然后 filter,接着 mutate,然后 select,最后 arrange。
如果我们没有管道(pipe)会怎样呢?我们就需要把每次函数调用嵌套到前一次调用之中:
arrange( select( mutate( filter( flights, dest == "IAH" ), speed = distance / air_time * 60 ), year:day, dep_time, carrier, flight, speed ), desc(speed))或者,我们也可以使用一大堆中间对象:
flights1 <- filter(flights, dest == "IAH")flights2 <- mutate(flights1, speed = distance / air_time * 60)flights3 <- select(flights2, year:day, dep_time, carrier, flight, speed)arrange(flights3, desc(speed))尽管这两种写法各有其适用场景,但管道(pipe)通常会产出更容易编写、也更容易阅读的数据分析代码。
要在你的代码中加入管道,我们建议使用内置的键盘快捷键 Ctrl/Cmd + Shift + M。你需要在 RStudio 的选项里做一个小调整,以便在代码中使用 |> 代替 %>%,如图 3.1 所示;关于 %>%,我们稍后再讲。

组
到目前为止,你已经学习了那些在“行”和“列”上工作的函数。当你加入对“分组(groups)”的处理能力时,dplyr 会变得更加强大。在这一节中,我们将重点介绍最重要的函数:group_by()、summarize() 以及 slice 系列的函数。
group_by()
使用 group_by() 将你的数据集划分为对分析而言有意义的组:
> flights |> + group_by(month)# A tibble: 336,776 × 19# Groups: month [12] year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time <int> <int> <int> <int> <int> <dbl> <int> <int> 1 2013 1 1 517 515 2 830 819 2 2013 1 1 533 529 4 850 830 3 2013 1 1 542 540 2 923 850 4 2013 1 1 544 545 -1 1004 1022 5 2013 1 1 554 600 -6 812 837 6 2013 1 1 554 558 -4 740 728 7 2013 1 1 555 600 -5 913 854 8 2013 1 1 557 600 -3 709 723 9 2013 1 1 557 600 -3 838 84610 2013 1 1 558 600 -2 753 745# ℹ 336,766 more rows# ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,# tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rowsgroup_by() 不会改变数据,但如果你仔细看输出,会发现输出表明它是“按月分组”的(Groups: month [12])。这意味着后续的操作将改为“按月”进行。group_by() 会为数据框添加这种分组特性(称为 class),从而改变对数据应用的后续动词的行为。
summarize()
最重要的分组操作是汇总。如果只用来计算一个汇总统计量,它会把数据框减少为:每个组只有一行。在 dplyr 中,这个操作由 summarize()执行,正如下面的示例所示,它按月计算平均离港延误时间:
> flights |> + group_by(month) |> + summarize(+ avg_delay = mean(dep_delay)+ )# A tibble: 12 × 2 month avg_delay <int> <dbl> 1 1 NA 2 2 NA 3 3 NA 4 4 NA 5 5 NA 6 6 NA 7 7 NA 8 8 NA 9 9 NA10 10 NA11 11 NA12 12 NA哎呀!出错了,我们所有的结果都是 NA(读作 “N-A”),这是 R 中表示缺失值的符号。这是因为有些观测到的航班在延误(delay)列中缺少数据,所以当我们在计算均值时把这些值也包含进去,就得到了 NA 的结果。我们会通过将参数 na.rm 设置为 TRUE,来告诉 mean() 函数忽略所有缺失值:
> flights |> + group_by(month) |> + summarize(+ avg_delay = mean(dep_delay, na.rm = TRUE)+ )# A tibble: 12 × 2 month avg_delay <int> <dbl> 1 1 10.0 2 2 10.8 3 3 13.2 4 4 13.9 5 5 13.0 6 6 20.8 7 7 21.7 8 8 12.6 9 9 6.7210 10 6.2411 11 5.4412 12 16.6 你可以在一次对 summarize() 的调用中创建任意数量的汇总结果。接下来的章节会学到各种有用的汇总方式,但其中一个非常有用的汇总是 n():它会返回每个组中的行数:
> flights |> + group_by(month) |> + summarize(+ avg_delay = mean(dep_delay, na.rm = TRUE), + n = n()+ )# A tibble: 12 × 3 month avg_delay n <int> <dbl> <int> 1 1 10.0 27004 2 2 10.8 24951 3 3 13.2 28834 4 4 13.9 28330 5 5 13.0 28796 6 6 20.8 28243 7 7 21.7 29425 8 8 12.6 29327 9 9 6.72 2757410 10 6.24 2888911 11 5.44 2726812 12 16.6 28135在数据科学中,均值和计数往往能让你走得出奇地远!
切片(slice)函数.
有五个方便的函数,允许你在每个组内提取特定的行:
df |> slice_head(n = 1)从每个组中取出第一行。df |> slice_tail(n = 1)从每个组中取出最后一行。df |> slice_min(x, n = 1)取出在列x上数值最小的那一行。df |> slice_max(x, n = 1)取出在列x上数值最大的那一行。df |> slice_sample(n = 1)随机取出一行。
你可以改变 n 来选择多于一行;或者不使用 n = ...,而是用 prop = 0.1 来选择(例如)每个组中 10% 的行。比如,下面这段代码会找出:到达每个目的地时延误最严重的航班:
flights |> group_by(dest) |> # 按目的地 dest 对数据分组 slice_max(arr_delay, n = 1) |> # 在每个目的地组内,取 arr_delay 最大的那一行(延误时间最长) relocate(dest) # 将列 dest 移动到数据框的前面(放置位置更靠前,方便查看) # A tibble: 108 × 19# Groups: dest [105] dest year month day dep_time sched_dep_time dep_delay arr_time <chr> <int> <int> <int> <int> <int> <dbl> <int> 1 ABQ 2013 7 22 2145 2007 98 132 2 ACK 2013 7 23 1139 800 219 1250 3 ALB 2013 1 25 123 2000 323 229 4 ANC 2013 8 17 1740 1625 75 2042 5 ATL 2013 7 22 2257 759 898 121 6 AUS 2013 7 10 2056 1505 351 2347 7 AVL 2013 8 13 1156 832 204 1417 8 BDL 2013 2 21 1728 1316 252 1839 9 BGR 2013 12 1 1504 1056 248 162810 BHM 2013 4 10 25 1900 325 136# ℹ 98 more rows# ℹ 11 more variables: sched_arr_time <int>, arr_delay <dbl>, carrier <chr>,# flight <int>, tailnum <chr>, origin <chr>, air_time <dbl>, distance <dbl>,# hour <dbl>, minute <dbl>, time_hour <dttm># ℹ Use `print(n = ...)` to see more rows注意,这里有 105 个目的地,但我们得到的是 108 行。怎么回事?slice_min() 和 slice_max() 会保留并列的值,因此当 n = 1 时,它会把所有具有最高值的行都返回。如果你想做到每个组恰好只有一行,可以把 with_ties = FALSE 设置上。
这和用 summarize() 计算最大延误是类似的,但你得到的是完整的对应行(如果有并列,则是对应的多行),而不是单个汇总统计量。
按多个变量进行分组
你可以使用多个变量来创建分组。例如,我们可以为每个日期创建一个组。
daily <- flights |> group_by(year, month, day)daily#> # A tibble: 336,776 × 19#> # Groups: year, month, day [365]#> year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time#> <int> <int> <int> <int> <int> <dbl> <int> <int>#> 1 2013 1 1 517 515 2 830 819#> 2 2013 1 1 533 529 4 850 830#> 3 2013 1 1 542 540 2 923 850#> 4 2013 1 1 544 545 -1 1004 1022#> 5 2013 1 1 554 600 -6 812 837#> 6 2013 1 1 554 558 -4 740 728#> # ℹ 336,770 more rows#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …当你对一个按多个变量分组的 tibble 进行汇总时,每次汇总都会“剥离”掉最后一层分组。回过头看,这并不是让这个函数工作的理想方式,但在不破坏现有代码的前提下很难进行更改。为了让你清楚地知道发生了什么,dplyr 会显示一条消息,告诉你可以如何修改这种行为:
daily_flights <- daily |> summarize(n = n())#> `summarise()` has regrouped the output.#> ℹ Summaries were computed grouped by year, month, and day.#> ℹ Output is grouped by year and month.#> ℹ Use `summarise(.groups = "drop_last")` to silence this message.#> ℹ Use `summarise(.by = c(year, month, day))` for per-operation grouping#> (`?dplyr::dplyr_by`) instead.如果你对这种行为感到满意,你也可以明确地要求它,从而抑制该消息:
daily_flights <- daily |> summarize( n = n(), .groups = "drop_last" )或者,你可以通过设置不同的值来改变默认行为,例如使用“drop”来删除所有分组,或使用“keep”来保留相同的分组。
发表评论