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)

初学者常犯以下两点错误

  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`?
  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 的一部分),功能更强。比如可使用.占位符传递非首参数。

不过,如果只在 dplyrggplot2 语境下处理数据,|> 通常已足够。

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)