3 数据处理
3.1 引言
可视化是数据分析时的重要手段,但前提是数据格式严格符合要求。因此针对格式不当的数据需要进行一些处理。
本章主要介绍使用dplyr包对数据进行处理,dplyr同样归属于tidyverse。另外为了举例,还需加载含有纽约航班信息的包:
> library(nycflights13)
> library(tidyverse)nycflights13包中包含 2013 年从纽约市出发的所有 336,776 个航班,记录在nycflights13::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
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …flights表格是一个 “Tibble”,这是一种特殊的数据框。Tibble 和普通数据框之间最重要的区别是其显示方式,tibble专为大型数据集而设计,因此它们仅显示前几行,并且仅显示适配屏幕大小的列。如果使用 RStudio操作则更方便,会打开一个可交互、可滚动、可筛选的视图。
注意到表头下方用尖括号标注了数据类型,大致有:
- int 表示整数型变量。
- dbl表示双精度浮点数型变量,或称实数。
- chr 表示字符向量,或称字符串。
- lgl表示逻辑型变量,是一个仅包括 TRUE 和 FALSE 的向量。
- fctr 表示因子,R用其来表示具有固定数目的值的分类变量。
- date 表示日期型变量。
- dttm 表示日期时间(日期+时间)型变量。
本章将学习5个dplyr核心函数,用于数据处理,大致为:
- 按值筛选(
filter())。 - 对行进行重新排序(
arrange()) - 按名称选取变量(
select()) - 使用现有变量的函数创建新变量(
mutate()) - 将多个值总结为一个摘要统计量(
summarize())
上述5个函数的工作方式大致相同,有以下共通点:
- 第一个参数是数据框。
- 后续参数使用不带引号的变量名称,描述针对数据框进行的操作。
- 输出结果是一个新数据框。
下面对其一一阐述。
3.2 行
操作数据行的最主要函数为:
filter():用于筛选数据。改变的是行的种类,但不改变顺序;arrange():用于排序。改变的是行的顺序,但不改变行的内容。
这两个函数仅作用于行,不会修改列。
此外,还有 distinct() 函数,用于查找具有唯一值的行。与 arrange() 和 filter() 不同,distinct() 在筛选行的同时也可以选择性地修改列。
3.2.1 filter()
filter() 函数用于根据列中的值保留数据框中的某些行。第一个参数是数据框,后续的参数是判断各行是否保留的条件。例如,以下代码能找出所有起飞延误超过 120 分钟的航班:
flights |>
filter(dep_delay > 120)除了 >(大于)之外,还可以使用以下比较运算符:
>=(大于等于)<(小于)<=(小于等于)==(等于)!=(不等于)
还可以使用 & 或 , 表示“与”(同时满足多个条件),使用 | 表示“或”(满足任一条件)。例如要筛选出所有在1月1日起飞的航班:
flights |>
filter(month == 1 & day == 1)结合 | 与 == 时,有一个简洁的写法:%in%,用于匹配某个变量是否属于一组值之一。比如筛选1月或2月的航班:
flights |>
filter(month %in% c(1, 2))在运行 filter() 时,dplyr 会返回一个新的数据框,而不会修改原始的 flights 数据集。要保存筛选结果,使用赋值操作符 <-:
jan1 <- flights |>
filter(month == 1 & day == 1)初学者常犯以下两点错误
- 用
=判断相等,而非==。此时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`?- 像口语一样写“或”条件:
flights |>
filter(month == 1 | 2)正确写法是 month == 1 | month == 2。
3.2.2 arrange()
arrange() 根据某些列的值对行进行排序。它接收数据框及一组列名或表达式。如果提供多个列名,则后面的列用于在前面的列值相同时进一步排序。
例如,下面的代码按年、月、日和起飞时间排序,得到的是最早起飞的航班排在前面:
flights |>
arrange(year, month, day, dep_time)若希望按某列的降序排列,可以用 desc():
# 按照起飞延误时间从大到小排序
flights |>
arrange(desc(dep_delay))3.2.3 distinct()
distinct() 查找数据框中所有唯一(去重)行。在实际使用中,更常用于获取某些列组合的唯一值,且会保留每组中第一次出现的那一行。
# 删除重复行
flights |>
distinct()
# 获取所有起点和终点的组合
flights |>
distinct(origin, dest)如需保留其他列信息,可添加 .keep_all = TRUE参数。
若希望获取各组合的出现次数,使用 count() 更为合适,并可通过 sort = TRUE 参数按频数降序排列:
flights |>
count(origin, dest, sort = TRUE)3.3 列操作
在数据处理过程中,有四个 dplyr 中的重要函数可用于操作列而不改变行的结构:
mutate():基于现有列创建新列。select():筛选保留指定列。rename():重命名列。relocate():重新排列列的位置。
3.3.1 mutate()
mutate() 用于在数据框中添加新列,这些新列的值是通过现有列计算得出的。例如:
flights |>
mutate(
gain = dep_delay - arr_delay,
speed = distance / air_time * 60
)此代码添加了两个新列 gain(“起飞延误”减去“到达延误”)和 speed(飞行速度,单位:mph)。默认情况下,新列会添加在数据框的最右侧。为了便于观察,可以使用 .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
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, …此外,.after 可用于将新列插入某一指定列之后。.keep 参数可控制哪些列被保留。例如仅保留参与 mutate() 计算的列:
flights |>
mutate(
gain = dep_delay - arr_delay,
hours = air_time / 60,
gain_per_hour = gain / hours,
.keep = "used"
)注意:若未将结果赋值回对象(如
flights或新对象),新生成的变量仅在当前操作中可见,不会永久保存。
3.3.2 select()
在处理包含大量变量的数据集时,select() 可用于快速提取需要研究的列。常见用法包括:
# 指定列名
select(year, month, day)
# 选择连续区间
select(year:day)
# 排除某一列区间
select(!year:day)
# 选择字符型列
select(where(is.character))还可使用辅助函数进行模式匹配:
starts_with("abc"):匹配以 abc 开头的列名。ends_with("xyz"):匹配以 xyz 结尾的列名。contains("ijk"):包含 ijk 的列名。num_range("x", 1:3):匹配 x1, x2, x3。
此外,也可在 select() 中重命名列,但是只保留被选择的列,未被选中的列会被移除。
flights |>
select(tail_num = tailnum)3.3.3 rename()
若只想重命名部分列而保留所有现有列,可使用 rename():
flights |>
rename(tail_num = tailnum)相比 select(),rename() 不会改变列的数量,仅修改名称。
若存在大量命名不规范的列名,可考虑使用 janitor::clean_names() 进行批量清洗。
3.3.3.1 relocate()
relocate() 用于调整列的顺序,可以将某些更关键的列移动到前面:
flights |>
relocate(time_hour, air_time)
#> # A tibble: 336,776 × 19
#> time_hour air_time year month day dep_time sched_dep_time
#> <dttm> <dbl> <int> <int> <int> <int> <int>
#> 1 2013-01-01 05:00:00 227 2013 1 1 517 515
#> 2 2013-01-01 05:00:00 227 2013 1 1 533 529
#> 3 2013-01-01 05:00:00 160 2013 1 1 542 540
#> 4 2013-01-01 05:00:00 183 2013 1 1 544 545
#> 5 2013-01-01 06:00:00 116 2013 1 1 554 600
#> 6 2013-01-01 05:00:00 150 2013 1 1 554 558
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: dep_delay <dbl>, arr_time <int>, ….before 或 .after 精确定位:
flights |>
relocate(year:dep_time, .after = time_hour)
flights |>
relocate(starts_with("arr"), .before = dep_time)
3.4 管道符
管道符提升代码的可读性、简洁性和逻辑性,避免嵌套调用。
3.4.1 Base R 管道操作符 |>
自 R 4.1.0 起,R 语言原生支持管道符 |>。其核心原理是“把前一步的结果作为后一个函数的第一个参数”。
举例,找出飞往 IAH 的航班中速度最快的几架飞机:
flights |>
filter(dest == "IAH") |>
mutate(speed = distance / air_time * 60) |>
select(year:day, dep_time, carrier, flight, speed) |>
arrange(desc(speed))等价于嵌套写法:
arrange(
select(
mutate(
filter(flights, dest == "IAH"),
speed = distance / air_time * 60
),
year:day, dep_time, carrier, flight, speed
),
desc(speed)
)3.4.2 |> 与 %>% 的区别
|> 是 base R 提供的原生操作符,不依赖任何包。
%>% 来源于 magrittr 包(tidyverse 的一部分),功能更强。比如可使用.占位符传递非首参数。
不过,如果只在 dplyr 和 ggplot2 语境下处理数据,|> 通常已足够。
3.5 分组操作与汇总
管道操作仅简化流程,但对某些任务,如“对每个月统计平均延误时间”,则需要借助分组与汇总函数。
3.5.1 group_by()
示例:按月份分组
flights |>
group_by(month)此时返回的 tibble 看似不变,但其实多了一个“分组结构”属性,后续函数如 summarize() 将以此分组为单位运算。
3.5.2 summarize()
用于计算每组的统计量,如平均数、个数、最大值等。
flights |>
group_by(month) |>
summarize(
avg_delay = mean(dep_delay, na.rm = TRUE),
flight_count = n()
)
na.rm = TRUE用于忽略缺失值;n()返回当前分组的行数(即航班数);- 默认情况下
summarize()会“剥离”最后一个分组变量。
3.5.3 多重分组与 .groups 参数
可同时按多个变量分组:
flights |>
group_by(year, month, day) |>
summarize(avg_delay = mean(dep_delay, na.rm = TRUE))可通过 .groups 参数明确控制输出是否保留某层分组:
summarize(..., .groups = "drop_last") # 保留上层分组
summarize(..., .groups = "drop") # 全部取消分组
summarize(..., .groups = "keep") # 保留所有分组3.5.4 ungroup()移除分组结构
若后续不再需分组操作,需要使用 ungroup() 函数进行声明,避免出现意外。
daily_summary |>
ungroup() |>
summarize(total_flights = sum(n))3.5.5 slice_*() 系列函数:获取组内特定行
slice_*() 系列函数常用于提取组内最值、样本等,结果保留原始列结构。
slice_head(n = 1):每组取最前一行slice_tail(n = 1):每组取最后一行slice_max(order_by, n = 1):每组取最大值slice_min(order_by, n = 1):每组取最小值slice_sample(n = 1):每组随机取一行
例如,找出每个目的地到达延误最长的航班:
flights |>
group_by(dest) |>
slice_max(arr_delay, n = 1) |>
relocate(dest)默认行为中,若多个航班并列最大延误,则全部保留。若需限制为仅一行,可加 with_ties = FALSE。
3.5.6 .by 参数
dplyr 1.1.0 引入 .by 参数,提供了一种更简洁、局部化的分组操作语法。与传统 group_by() 不同,.by 仅在当前动词范围内生效,不影响后续操作的分组状态,适合一次性分组计算。
基本用法如下例所示:
flights |>
summarize(
delay = mean(dep_delay, na.rm = TRUE),
n = n(),
.by = month
)可支持多变量分组:
flights |>
summarize(
delay = mean(dep_delay, na.rm = TRUE),
n = n(),
.by = c(origin, dest)
).by参数特性总结如下:
- 作用范围限于当前动词,运算结束即“自动取消分组”;
- 可用于
summarize()、mutate()、filter()等所有动词; - 避免了
.groups警告信息,简化结果处理流程; - 写法更贴近函数式风格,便于封装与组合。
以下是两种写法的对比:
- 传统写法:
flights |>
group_by(month) |>
summarize(delay = mean(dep_delay, na.rm = TRUE)) |>
ungroup().by简化:flights |> summarize(delay = mean(dep_delay, na.rm = TRUE), .by = month)