dplyr 数据处理入门:filter、select、mutate、arrange 实战
base R 里筛一行数据要写 mtcars[mtcars$cyl == 4 & mtcars$hp > 100, c("mpg", "hp")]:方括号里塞两个条件、一个列名向量,少写一个逗号就报错,列多了还得数位置。dplyr 换了个思路,把「筛行」「选列」「造新列」拆成独立动词(verb),一个函数只做一件事,函数名就是它在做的事,再用管道串成一条从上往下读的流程。
五个动词和一条管道
Section titled “五个动词和一条管道”| 动词 | 作用 | 类比 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")carsas_tibble() 把 data.frame 换成 tibble(一种打印更克制的现代数据框):会显示每一列的类型,列放不下就截断并提示还有几列没打印,行多了只显示前 10 行。相比之下 data.frame 会把整张表糊满屏幕。
filter:按行筛选
Section titled “filter:按行筛选”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.512 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))保留四缸和六缸,比写两遍|清楚。
select:按列挑选
Section titled “select:按列挑选”cars |> select(model, starts_with("d")) |> head(3)# A tibble: 3 × 3 model disp drat <chr> <dbl> <dbl>1 Mazda RX4 160 3.92 Mazda RX4 Wag 160 3.93 Datsun 710 108 3.85starts_with() 属于「选择辅助函数」(selection helper),同类还有 ends_with()、contains()、num_range()、everything(),以及最灵活的 where()——它接受一个函数,对每一列判断是否保留:
iris |> select(where(is.numeric)) |> head(2) Sepal.Length Sepal.Width Petal.Length Petal.Width1 5.1 3.5 1.4 0.22 4.9 3.0 1.4 0.2iris 有一个 Species 因子列,where(is.numeric) 一句话就把它排除掉,列名换了也不用改代码。排除列用负号:select(cars, -c(mpg, cyl)) 表示「除了这两列都要」。改名直接在 select() 里写 新名 = 旧名,例如 select(cars, 车型 = model)。
注意 select() 里写的列名是不带引号的。写成 select(cars, "model") 在新版 dplyr 里也能跑,但混着写容易出错,建议统一不加引号。列名存在字符串变量里时,用 all_of(cols) 明确告诉 dplyr「这些是外部变量」。
mutate:新增和改写列
Section titled “mutate:新增和改写列”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.82 Ford Pantera L 264 3.17 83.33 Lotus Europa 113 1.51 74.7hp / 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:排序
Section titled “arrange:排序”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.92 Fiat 128 4 32.43 Honda Civic 4 30.44 Lotus Europa 4 30.4Honda Civic 和 Lotus Europa 的油耗都是 30.4,谁在前取决于它们在数据里原本的先后。arrange() 不保证并列行的顺序,如果下游结果依赖这个顺序(比如取前 N 行做样本),要么多写一个排序列把顺序定死,要么在并列时改用 slice_sample() 随机抽取。想按行号取「最省油的前 5 辆」这种需求,slice_max(mpg, n = 5) 比排序加 head() 更贴切,它还会保留并列值。
排序还有一条实用规则:缺失值永远排在最后,升序降序都一样。这对含缺失的问卷数据很友好,airquality |> arrange(desc(Ozone)) |> head(3) 拿到的是臭氧浓度最高的三天,不会先冒出一堆 NA。
管道:把嵌套改成流水线
Section titled “管道:把嵌套改成流水线”同样的操作不用管道要写成嵌套函数,阅读顺序是从最里面往外翻:
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 1132 Volvo 142E 21.4 1093 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
Section titled “什么时候别用 dplyr”数据量在几十万行以内,dplyr 的写法收益最大:意图清楚、调试方便。真正上亿行、内存吃紧时,data.table 或 arrow 的语法会更划算,dplyr 的整洁语法会带来额外的开销。另外,dplyr 的动词都是「不修改原对象」的纯函数,如果你需要原地修改避免复制,那套语义得去 data.table 里找。
下一步是 summarise() 与 group_by() 的组合,见 /r/tidyverse/dplyr-grouping。如果你也用 Python,pandas 里对应的写法是 df.loc[] 与 df.query(),可以对照 /python/pandas/filtering-selection 看同一件事的两种表达。