第四章 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))
第二种写法,我闭着眼睛都能改。每个步骤之间加一行注释也很方便。
%>%,省事。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操作流程示意图
下面的SVG图概括了dplyr五个动词在管道中的典型位置与流向:
图中每个函数都是可选的,你可以根据分析需求自由组合。记住,管道里每个环节只做一件事,代码就干净了。