第四章 dplyr数据操作:核心动词与管道思维

学R做丰度数据,说到底就是和数据框打交道。dplyr这套包,我用了快十年,它把最常见的操作凝练成几个动词——select、filter、mutate、arrange、summarise。配合管道符%>%,代码读起来像在念句子。

很多人刚上手会问:「为什么要学dplyr?基础R的[]不是也能做吗?」嗯,区别就像手写信件和发邮件——功能一样,但效率和可读性差太远了。我在处理16S扩增子数据时,一个管道链能代替十几行嵌套的[],改起来也快。

4.1 管道符 %>%:让代码从左读到右

管道符%>%是dplyr的灵魂。它的作用很简单:把左边的结果,传给右边函数的第一个参数。

打个比方:你先给数据框,再告诉它你要做什么。而不是把数据框包在函数里面。

你看这两种写法:

# 不写管道,嵌套着读
arrange(filter(select(otu_table, Sample1, Sample2, Taxonomy), Phylum == "Firmicutes"), desc(Sample1))

# 写管道,从左往右读
otu_table %>%
  select(Sample1, Sample2, Taxonomy) %>%
  filter(Phylum == "Firmicutes") %>%
  arrange(desc(Sample1))

第二种写法,我闭着眼睛都能改。每个步骤之间加一行注释也很方便。

实操小贴士:RStudio里快捷键 Ctrl+Shift+M 直接打出 %>%,省事。

4.2 select():选列就像点菜

select用来挑出你想要的列(变量)。在丰度表里,物种列、样本列、分组列,想留谁留谁。

# 选单个列
otu_data %>% select(Sample_ID)

# 选多个列
otu_data %>% select(Sample_ID, Phylum, Abundance)

# 按位置选
otu_data %>% select(1:3)

# 排除列
otu_data %>% select(-Taxonomy)

我还常用starts_with()、ends_with()、contains()这些辅助函数。比如样本列名都叫Sample_001、Sample_002…,一句select(starts_with("Sample_"))全挑出来。

我有一次手滑,把分组信息列给排除掉了,结果下游分析全乱套。嗯,建议每次做select后先用names()检查一下。

4.3 filter():按条件筛选行

filter用来挑行(观测),比如只保留某个门、某个丰度阈值以上的物种。

# 单条件
otu_data %>% filter(Phylum == "Firmicutes")

# 多条件:且关系
otu_data %>% filter(Phylum == "Firmicutes", Abundance > 0.01)

# 或关系
otu_data %>% filter(Phylum == "Firmicutes" | Phylum == "Bacteroidetes")

# 筛选数值范围
otu_data %>% filter(Abundance >= 0.001, Abundance <= 0.1)

注意:filter里的条件用==,不是=。曾经有学员写了filter(Phylum = "Firmicutes"),结果R返回错误还一脸懵。这种小坑,踩过一次就记住了。

避坑指南:字符型因子要小心前后空格。比如"Firmicutes"和"Firmicutes "可不是一回事。我通常会在导入数据时用stringsAsFactors = FALSE,或者用str_trim()清扫一遍。

4.4 mutate():新增或修改列

mutate用于创建新列,或者对已有列做变换。在丰度数据处理中,经常需要计算相对丰度、取对数、合并分类层级。

# 新建一列:相对丰度(百分比)
otu_data %>%
  mutate(RelAbund = Abundance / sum(Abundance) * 100)

# 多列同时处理
otu_data %>%
  mutate(
    log_Abund = log10(Abundance + 1),
    Abund_round = round(Abundance, 4)
  )

# 修改已有列
otu_data %>%
  mutate(Phylum = ifelse(Abundance < 0.001, "Low_abund", Phylum))

我在处理多批次合并的丰度表时,经常用mutate来统一分类名。比如把"unclassified"和"Unassigned"都改成"Others",一个mutate搞定。

4.5 arrange():排序

arrange按指定列排序,默认升序。想降序就用desc()。

# 按丰度升序
otu_data %>% arrange(Abundance)

# 按丰度降序
otu_data %>% arrange(desc(Abundance))

# 多级排序
otu_data %>% arrange(Phylum, desc(Abundance))

说实话,arrange在分析环节用得不多,一般是输出表格时为了让结果更直观才排个序。不过我常在检查数据时用arrange(desc(Abundance))快速看丰度最高的物种是啥。

4.6 summarise() + group_by():汇总统计

summarise用来聚合计算,但单独用意义不大——你得先分组。group_by告诉dplyr按哪个变量分组,然后summarise针对每个组做统计。

# 按门水平分组,求每个门的平均丰度
otu_data %>%
  group_by(Phylum) %>%
  summarise(
    mean_Abund = mean(Abundance),
    sd_Abund = sd(Abundance),
    n = n()
  )

注意:summarise会丢掉其他未分组的列。如果想保留原有数据,可以用mutate()配合group_by(),但我个人更习惯分开操作,逻辑清晰。

还有,summarise里面常用的函数包括n()(计数)、mean()、sd()、sum()、min()、max()。曾有个项目需要对每个样本的物种数做统计,一句group_by(Sample_ID) %>% summarise(richness = n())就出来了。

综合示例:一个典型的dplyr管道

下面这个例子,是我处理扩增子OTU表时的真实流程。原始表包含样本列、分类信息和丰度值,我要计算每个门在全部样本中的总相对丰度,并排序。

otupipeline %>%
  # 只保留丰度大于0的行(去除零值)
  filter(Abundance > 0) %>%
  # 只取需要的列
  select(Phylum, Abundance) %>%
  # 按门分组
  group_by(Phylum) %>%
  # 汇总每个门的总丰度
  summarise(total_abund = sum(Abundance)) %>%
  # 计算相对丰度
  mutate(rel_abund = total_abund / sum(total_abund) * 100) %>%
  # 降序排列
  arrange(desc(rel_abund))

# 结果是一个两列的tibble:门名和相对丰度

你看,整个管道链6步,从原始数据到最终结果,一步不少。

核心逻辑:dplyr的五个动词 + 管道符 = 可读性极高的数据操作语言。select选列,filter选行,mutate造新列,arrange排序,summarise分组汇总。配合group_by,几乎覆盖了丰度数据清洗和转换的所有场景。

dplyr操作流程示意图

下面的SVG图概括了dplyr五个动词在管道中的典型位置与流向:

dplyr 数据操作管道流程 原始数据框 select() filter() mutate() group_by() + summarise() (分组汇总) arrange() 结果tibble 说明 箭头表示%>% 从左向右传递数据 步骤可任意增删

图中每个函数都是可选的,你可以根据分析需求自由组合。记住,管道里每个环节只做一件事,代码就干净了。