4. dplyr 数据清洗:filter、select、mutate、group_by、summarise

数据清洗这步,我做了这么多年项目,每次都要花掉 60% 以上的时间。说白了,数据不洗没法用。R 语言里 dplyr 是清洗的利器,今天我们一口气把五个核心函数讲透。

先看整体逻辑: 原始数据 → 过滤行(filter) → 选列(select) → 创建新列(mutate) → 分组(group_by) → 汇总(summarise)。不绕弯子,直接上图。

dplyr 数据清洗五步流程 原始数据 filter( ) 行过滤 select( ) 列选择 mutate( ) 新建列 group_by( ) summarise( ) 分组+汇总 清洗结果 用 %>% 管道把五个函数串起来,一步到位

4.1 filter – 按条件筛选行

filter() 好比筛子,只保留满足条件的行。我个人习惯把逻辑条件写在逗号后面,多个条件是「与」关系。

library(dplyr)

# 筛选 drug_approved == "是" 且 target_count > 5 的药物
drug_clean <- drug_raw %>%
  filter(drug_approved == "是", target_count > 5)
我用过的一个坑: 曾经直接写 filter(drug_approved == "是" | target_count > 5) 忘了加括号,结果数据量少了一半。建议不确定优先级时就加括号,filter((drug_approved == "是") | (target_count > 5))。

另外,filter() 里也能用 %in%、between()、is.na() 这些辅助函数,很顺手。

4.2 select – 挑选需要的列

数据量大了以后,列太多看着头晕。用 select() 只留关键字段。

# 只保留药物名、批准状态、靶点数量
drug_selected <- drug_raw %>%
  select(drug_name, drug_approved, target_count)

# 也可以用 - 去掉不要的列
drug_selected <- drug_raw %>%
  select(-c(drug_id, create_date, update_date))

select() 还支持很多帮手函数,比如 starts_with()、ends_with()、contains()。我在药物知识图谱项目里,经常用 select(drug_name, starts_with("target_")) 把靶点信息一网打尽。

4.3 mutate – 生成新列

清洗中经常要计算新的指标,比如药物靶点密度,或者把分类变量转成数值。这时候 mutate() 就上场了。

# 计算靶点密度: 靶点数量 / 分子量
drug_mutated <- drug_raw %>%
  mutate(target_density = target_count / molecular_weight)
注意: mutate() 里可以引用当前数据框的其他列,但不要在同一行 mutate 里交叉引用后续生成的新列。需要链式操作就用两次 mutate。

我经常搭配 if_else() 做条件赋值,比如给靶点数量打标签:

drug_mutated <- drug_raw %>%
  mutate(target_level = if_else(target_count > 10, "high", "low"))

4.4 group_by – 分组

group_by() 不改变数据本身,它只是给数据框打上分组标签,后续的操作(如 summarise()、mutate())就会按组执行。

# 按药物批准状态分组
drug_grouped <- drug_raw %>%
  group_by(drug_approved)

分组后可以用 ungroup() 解除分组。我记得刚用 dplyr 时总忘记取消分组,导致后续计算一直按之前的分组跑,结果全是错的。所以养成好习惯:分组汇总完立刻取消分组。

4.5 summarise – 汇总统计

summarise() 和 group_by() 是绝配。它们一起能快速产出按组的统计量,比如均值、中位数、计数等。

# 按批准状态分组,计算靶点数量的均值、标准差、个数
drug_summary <- drug_raw %>%
  group_by(drug_approved) %>%
  summarise(
    mean_target = mean(target_count, na.rm = TRUE),
    sd_target   = sd(target_count, na.rm = TRUE),
    n           = n()
  ) %>%
  ungroup()

这里 n() 是 dplyr 自带函数,返回每组行数。如果数据里有缺失值,记得加 na.rm = TRUE,不然结果全是 NA。我碰过一回,查了半天才发现是缺失值没去掉。

4.6 全部串起来:一个真实案例

假设我们有一个药物‑靶点关系数据框 drug_target,包含字段:drug_name、target_gene、affinity(亲和力)、approved。现在要找出已批准药物中亲和力大于 7 的靶点,按药物分组统计靶点数量,最后按数量降序排列。

result <- drug_target %>%
  filter(approved == "是", affinity > 7) %>%
  select(drug_name, target_gene, affinity) %>%
  mutate(affinity_class = if_else(affinity >= 9, "strong", "moderate")) %>%
  group_by(drug_name) %>%
  summarise(
    strong_targets = sum(affinity_class == "strong"),
    moderate_targets = sum(affinity_class == "moderate"),
    total_targets = n()
  ) %>%
  ungroup() %>%
  arrange(desc(total_targets))

你看,用 %>% 管道符串起来,逻辑清楚,每一行只做一个动作。我写的所有清洗脚本都保持这种风格,维护起来特别轻松。

常用函数速查表

函数作用典型用法
filter()行过滤filter(approved == "是", age > 30)
select()列选择select(drug, target, affinity)
mutate()新建列mutate(score = affinity * weight)
group_by()分组group_by(drug_class)
summarise()汇总summarise(mean = mean(score))

我的经验: 虽然单个函数很简单,但组合起来威力巨大。我过去做过的药物特征分析,90% 的数据清洗都用这五个函数搞定。多用几次,肌肉记忆就出来了。