跳到正文

dplyr 数据处理入门:filter、select、mutate、arrange 实战

base R 里筛一行数据要写 mtcars[mtcars$cyl == 4 & mtcars$hp > 100, c("mpg", "hp")]:方括号里塞两个条件、一个列名向量,少写一个逗号就报错,列多了还得数位置。dplyr 换了个思路,把「筛行」「选列」「造新列」拆成独立动词(verb),一个函数只做一件事,函数名就是它在做的事,再用管道串成一条从上往下读的流程。

动词 作用 类比 SQL
filter() 按条件保留行 WHERE
select() 挑选、排除、重命名列 SELECT
mutate() 新增或改写列 SELECT ... AS
arrange() 按列排序 ORDER BY
summarise() 把多行压成一行汇总值 聚合函数

前四个是这一篇的主角,summarise() 要配合分组才有威力,留在下一篇讲。下面所有例子的数据都来自 mtcars——R 内置的 32 辆车的油耗与性能数据,不需要读任何外部文件。

library(tidyverse)
# mtcars 的行名是车型,转成一列,后面按车型筛选方便
cars <- as_tibble(mtcars, rownames = "model")
cars

as_tibble() 把 data.frame 换成 tibble(一种打印更克制的现代数据框):会显示每一列的类型,列放不下就截断并提示还有几列没打印,行多了只显示前 10 行。相比之下 data.frame 会把整张表糊满屏幕。

filter() 的第一个参数是数据,后面跟任意多个逻辑条件,条件之间是「且」的关系:

cars |>
filter(cyl == 4, hp > 100) |>
select(model, mpg, hp, wt) |>
arrange(desc(hp))
# A tibble: 2 × 4
model mpg hp wt
<chr> <dbl> <dbl> <dbl>
1 Lotus Europa 30.4 113 1.51
2 Volvo 142E 21.4 109 2.78

四缸车有 11 辆,其中马力超过 100 的只剩两辆。hp > 100 里的 > 是逻辑运算,返回 TRUE/FALSE 向量,filter() 只保留 TRUE 对应的行。

几个容易踩的点:

  • 多条件用逗号隔开,等价于用 & 连接。想表达「或」必须显式写 |,别指望逗号。
  • === 不是一回事filter(cars, cyl = 4) 会报错并提示 We detected a named input——它以为你要给参数命名。判断相等永远是 ==
  • NA 会被静默丢掉filter() 只保留条件为 TRUE 的行,条件算出 NA 的行(比如某列本身缺失)既不进也不出。用 airquality 试一下就明白:airquality |> filter(Ozone > 100) 返回的行数远少于 airquality |> filter(is.na(Ozone) | Ozone > 100),因为 Ozone 有 37 个缺失值。
  • 浮点数别用 ==filter(cars, wt == 2.62) 看着没问题,但 wt 是二进制浮点数,等于判断随时可能落空。判断「足够接近」用 near(wt, 2.62),或者直接写区间 wt > 2.61, wt < 2.63
  • 成员判断用 %in%filter(cars, cyl %in% c(4, 6)) 保留四缸和六缸,比写两遍 | 清楚。
cars |>
select(model, starts_with("d")) |>
head(3)
# A tibble: 3 × 3
model disp drat
<chr> <dbl> <dbl>
1 Mazda RX4 160 3.9
2 Mazda RX4 Wag 160 3.9
3 Datsun 710 108 3.85

starts_with() 属于「选择辅助函数」(selection helper),同类还有 ends_with()contains()num_range()everything(),以及最灵活的 where()——它接受一个函数,对每一列判断是否保留:

iris |>
select(where(is.numeric)) |>
head(2)
Sepal.Length Sepal.Width Petal.Length Petal.Width
1 5.1 3.5 1.4 0.2
2 4.9 3.0 1.4 0.2

iris 有一个 Species 因子列,where(is.numeric) 一句话就把它排除掉,列名换了也不用改代码。排除列用负号:select(cars, -c(mpg, cyl)) 表示「除了这两列都要」。改名直接在 select() 里写 新名 = 旧名,例如 select(cars, 车型 = model)

注意 select() 里写的列名是不带引号的。写成 select(cars, "model") 在新版 dplyr 里也能跑,但混着写容易出错,建议统一不加引号。列名存在字符串变量里时,用 all_of(cols) 明确告诉 dplyr「这些是外部变量」。

mutate() 在数据框末尾追加新列,表达式里可以直接引用同一批新造的列:

cars |>
mutate(hp_per_wt = hp / wt) |>
select(model, hp, wt, hp_per_wt) |>
arrange(desc(hp_per_wt)) |>
head(3)
# A tibble: 3 × 4
model hp wt hp_per_wt
<chr> <dbl> <dbl> <dbl>
1 Maserati Bora 335 3.57 93.8
2 Ford Pantera L 264 3.17 83.3
3 Lotus Europa 113 1.51 74.7

hp / wt 是推重比,用来衡量「每吨车重摊到多少马力」,比单看马力更能说明加速能力。mutate() 常用参数:

  • .keep = "used" 只保留用到的列和新建的列,中间调试时很省事;
  • .before / .after 控制新列插在哪儿,比如 .after = model
  • 想「只要新列」,用 transmute()transmute(cars, model, kpl = mpg * 0.4251) 只返回车型和换算成公里/升的油耗。

一个反直觉的地方:mutate() 不改原对象,只返回新数据框。mutate(cars, x = 1) 跑完之后 cars 一个字节都没变,必须写 cars <- cars |> mutate(...) 才生效。这是有意的设计——分析脚本里哪个变量被改过,读代码时一眼能看出来。

arrange() 默认升序,desc() 包住列名就是降序。多列排序按书写顺序依次生效,前一个值相同的行再看后一个:

cars |>
arrange(cyl, desc(mpg)) |>
select(model, cyl, mpg) |>
head(4)
# A tibble: 4 × 3
model cyl mpg
<chr> <dbl> <dbl>
1 Toyota Corolla 4 33.9
2 Fiat 128 4 32.4
3 Honda Civic 4 30.4
4 Lotus Europa 4 30.4

Honda Civic 和 Lotus Europa 的油耗都是 30.4,谁在前取决于它们在数据里原本的先后。arrange() 不保证并列行的顺序,如果下游结果依赖这个顺序(比如取前 N 行做样本),要么多写一个排序列把顺序定死,要么在并列时改用 slice_sample() 随机抽取。想按行号取「最省油的前 5 辆」这种需求,slice_max(mpg, n = 5) 比排序加 head() 更贴切,它还会保留并列值。

排序还有一条实用规则:缺失值永远排在最后,升序降序都一样。这对含缺失的问卷数据很友好,airquality |> arrange(desc(Ozone)) |> head(3) 拿到的是臭氧浓度最高的三天,不会先冒出一堆 NA

同样的操作不用管道要写成嵌套函数,阅读顺序是从最里面往外翻:

head(arrange(select(filter(cars, cyl == 4), model, mpg, hp), desc(hp)), 3)
cars |>
filter(cyl == 4) |>
select(model, mpg, hp) |>
arrange(desc(hp)) |>
head(3)
# A tibble: 3 × 3
model mpg hp
<chr> <dbl> <dbl>
1 Lotus Europa 30.4 113
2 Volvo 142E 21.4 109
3 Toyota Corona 21.5 97

|> 是 R 4.1 起自带的原生管道(native pipe),x |> f(y) 等价于 f(x, y),即把左边的结果塞进右边函数的第一个参数。老代码里常见的 %>% 来自 magrittr 包,功能基本重叠,但有两个差别值得记:%>% 支持用 . 占位,把左侧结果放到非第一个参数的位置,|> 不支持;R 4.2 起 |> 可以用 _ 占位,但只能作为具名参数的值出现一次,比如 cars |> lm(mpg ~ wt, data = _)。本系列统一用 |>

数据量在几十万行以内,dplyr 的写法收益最大:意图清楚、调试方便。真正上亿行、内存吃紧时,data.tablearrow 的语法会更划算,dplyr 的整洁语法会带来额外的开销。另外,dplyr 的动词都是「不修改原对象」的纯函数,如果你需要原地修改避免复制,那套语义得去 data.table 里找。

下一步是 summarise()group_by() 的组合,见 /r/tidyverse/dplyr-grouping。如果你也用 Python,pandas 里对应的写法是 df.loc[]df.query(),可以对照 /python/pandas/filtering-selection 看同一件事的两种表达。