ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

R语言vegan包vegdist函数详解:群落数据分析中的距离计算与选择

R语言vegan包vegdist函数详解:群落数据分析中的距离计算与选择

1. 项目概述:从群落数据到距离矩阵

如果你刚开始接触生态学、微生物组学或者任何涉及群落物种组成分析的研究,那么“距离”这个概念很快就会成为你绕不开的核心。我们手里通常有一张表格,行是样本(比如不同土壤、不同水体、不同病人的肠道),列是物种(比如细菌OTU、植物种类、基因家族),表格里填的是每个物种在每个样本中的丰度。直接比较两个样本,看它们像不像,总不能一个个数物种吧?这时候,我们就需要一个量化的“尺子”来测量样本间的差异,这把尺子就是距离相异性

vegdist()函数,来自R语言中生态学数据分析的“瑞士军刀”包——vegan,就是专门用来制造这把尺子的工具。它不生产数据,它只是距离的计算工。简单来说,你给它一个物种丰度矩阵,它就能给你算出一个样本两两之间的距离矩阵。这个距离矩阵,是后续进行排序分析(如NMDS、PCoA)、聚类分析、差异检验(如PERMANOVA)的绝对基石。

我刚开始用的时候,以为这不过就是个算距离的函数,随便选个方法就行。结果在分析微生物组数据时,用错了距离算法,导致后续的PERMANOVA结果完全解释不通,白白浪费了两周时间排查数据问题。所以,今天我就结合这些年踩过的坑和积累的经验,把vegdist()里里外外讲透,让你不仅能“会用”,更能“懂用”,知道在什么场景下该选哪把“尺子”。

2. 核心概念与函数参数全解

在深入代码之前,我们必须统一思想:什么是“距离”?在群落生态学中,我们通常计算的是相异性,值越大表示两个样本越不相似。有些指数(如Bray-Curtis)取值范围是0到1,有些(如欧氏距离)则没有上限。vegdist()函数的核心任务,就是根据你指定的算法,将样本对的物种组成向量转化为一个代表差异的数值。

2.1 函数基本语法与参数

vegdist()函数的基本调用格式如下:

vegdist(x, method = "bray", binary = FALSE, diag = FALSE, upper = FALSE, na.rm = FALSE, ...)

别看参数不多,每一个都至关重要,选错了直接影响结果。

  • x: 这是输入数据,通常是一个数值矩阵或数据框。行是样本列是物种/变量。这是最容易出错的地方,务必确保你的数据矩阵是这个方向。
  • method: 这是核心中的核心,指定计算距离的方法。vegan包内置了丰富的选项,也是我们重点讲解的对象。默认是"bray"(Bray-Curtis相异性)。
  • binary: 逻辑值(TRUE/FALSE)。如果设为TRUE,会在计算距离前,将丰度数据转换为“有(1)/无(0)”的二元数据。这相当于先做了一次“存在与否”的转换,再计算距离。对于某些关注物种有无而非丰度的研究(如分布地理学)很有用。
  • diag: 逻辑值。是否在输出的距离矩阵中打印对角线上的值(样本到自身的距离,通常为0)。
  • upper: 逻辑值。是否以上三角矩阵的形式打印输出(只显示矩阵右上角部分)。
  • na.rm: 逻辑值。是否在计算时移除缺失值(NA)。对于群落数据,NA可能意味着未检测或真实缺失,需要根据研究背景谨慎处理。

注意diagupper参数只影响距离矩阵的显示方式,不影响其作为dist对象的内在结构和后续分析。在R中,dist对象是一种高效存储对称距离矩阵下三角部分的数据类型。

2.2 关键方法(method)深度解析

选择哪种距离方法,取决于你的数据特性和科学问题。下面我把最常用的几种方法掰开揉碎了讲。

2.2.1 Bray-Curtis 相异性 (method = "bray")

这是生态学中最经典、最常用的距离之一,尤其适用于群落丰度数据。

  • 公式(思想):计算两个样本共有物种的丰度绝对值差之和,然后除以两个样本的总丰度之和。公式为:BC = sum|A_i - B_i| / sum(A_i + B_i)。其中A_i和B_i是物种i在两个样本中的丰度。
  • 特点与适用场景
    1. 关注群落组成:它对物种组成的变化敏感,既考虑物种有无,也考虑丰度差异。
    2. 不受零值过度影响:大量两个样本都没有的物种(双零值)不会增加它们的相似性(这是与某些相关系数距离的关键区别)。这在生态学上是合理的,两个沙漠都没有某树种,并不能说明它们相似。
    3. 对丰度敏感:一个物种在样本A中是100,在样本B中是10,与在A中是10,在B中是1,所产生的差异贡献是不同的。
    4. 范围在[0,1]:0表示两个样本完全相同,1表示完全不同(没有共有物种)。
  • 实操心得:对于绝大多数微生物16S rRNA基因测序得到的OTU/ASV丰度表、宏基因组物种组成表,Bray-Curtis通常是首选的基准距离。它非常稳健,我建议在初次分析时都从它开始。

2.2.2 Jaccard 相异性 (method = "jaccard")

这是一个基于物种有无(二元数据)的相异性指数。

  • 公式(思想)J = (b + c) / (a + b + c)。其中a是两个样本共有的物种数,b是样本1有而样本2无的物种数,c是样本2有而样本1无的物种数。
  • 特点与适用场景
    1. 忽略丰度,只关心有无:它把所有的丰度信息都丢掉了,只关注物种是否存在。这对于某些类型的DNA指纹数据(如DGGE、T-RFLP)或关注物种分布格局的研究特别有用。
    2. 双零值无影响:和Bray-Curtis一样,双零值不贡献相似性。
    3. 范围在[0,1]
  • 与Bray-Curtis的关系:当你设置binary = TRUE并使用method = "bray"时,你计算的就是Jaccard距离。因为Bray-Curtis公式在二元数据下会简化为Jaccard。这是一个需要记住的等价关系。
  • 实操心得:当你怀疑样本间的差异主要源于物种的“出现-消失”(比如强环境过滤导致某些物种完全不存在),而不是丰度的增减时,可以用Jaccard距离来验证。与Bray-Curtis的结果对比,如果两者格局差异很大,可能说明你数据中稀有物种(低丰度但广泛存在)的影响很显著。

2.2.3 欧氏距离 (method = "euclidean")

这是最广为人知的几何距离,但在群落数据分析中需要格外小心。

  • 公式:就是多维空间中点与点的直线距离:sqrt(sum((A_i - B_i)^2))
  • 特点与适用场景
    1. 对丰度绝对值敏感:一个在所有样本中丰度都很高的物种,其微小的相对变化就能对欧氏距离产生巨大贡献。这可能导致结果被少数高丰度物种主导。
    2. 受双零值影响:两个样本都没有的物种,差值为0,不增加距离。这看起来合理,但在高维稀疏的群落数据中,这会导致拥有大量共有“零值”的样本被拉近,可能产生误导。
    3. 没有上限
  • 在群落数据中的问题:原始丰度数据直接计算欧氏距离通常不是好主意。它没有标准化过程,样本总测序深度(文库大小)的差异会严重影响距离。样本A总读段100万,样本B总读段10万,即使组成比例相似,欧氏距离也会很大。
  • 如何正确使用:欧氏距离通常用于经过转化的数据。例如,对丰度数据进行Hellinger转化decostand(x, "hellinger"))后再计算欧氏距离,是一个非常有效且数学性质良好的方法,常用于基于冗余分析(RDA)的模型。所以,不要轻易对原始OTU表用method = "euclidean"

2.2.4 UniFrac 距离

这是一个基于系统发育信息的距离,特别适用于微生物组数据。它分为未加权UniFrac(只考虑分支有无)和加权UniFrac(同时考虑分支长度和物种丰度)。vegdist()函数本身不直接计算UniFrac距离,但vegan包通过distance()函数或专门的phyloseq包可以方便地调用。由于其重要性,这里简要提及其思想:

  • 核心思想:比较两个样本的微生物群落时,不仅看物种是否相同,还看它们系统发育上的远近。丢失一个独有物种和丢失一个在多个样本中常见的物种的近亲,意义是不同的。
  • 适用场景:当你拥有物种的系统发育树(如16S数据通过QIIME2、mothur等流程生成)时,强烈建议使用UniFrac距离。它能揭示基于进化关系的群落差异。

为了更直观地对比,我将常用方法总结如下表:

方法 (method)核心关注点对双零值的处理对丰度的敏感性典型适用场景注意事项
bray(Bray-Curtis)物种组成与丰度忽略(不增加相似性)敏感绝大多数群落丰度数据(微生物组、动植物群落)默认选择,稳健通用
jaccard物种有无(存在/缺失)忽略不敏感物种分布研究、二元化数据相当于binary=TRUE时的bray
euclidean(欧氏距离)多维空间几何距离视为相同(差为0)极度敏感(对绝对值)不推荐直接用于原始群落数据需先进行数据标准化/转化(如Hellinger)
manhattan(曼哈顿距离)丰度绝对差异之和视为相同(差为0)敏感(对绝对值)可作为某些分析的替代,但不如Bray-Curtis常用受总丰度影响大
kulczynski对稀有物种更宽容的Bray-Curtis变体忽略敏感群落中存在大量低丰度物种时比Bray-Curtis更稳定
unifrac(需通过其他函数)系统发育分支的共享情况由系统发育树定义加权版本敏感拥有系统发育树的微生物组数据能揭示进化尺度的差异

3. 完整实操流程:从数据到距离矩阵

理论说再多,不如亲手跑一遍。我们假设你手头有一个名为otu_table.csv的OTU丰度表,现在我们来完成从数据导入、检查、预处理到计算距离的全过程。

3.1 数据准备与检查

首先,加载必要的包并读入数据。

# 安装并加载vegan包 # install.packages("vegan") # 如果未安装,需先运行此命令 library(vegan) # 读入数据。假设你的数据是CSV格式,第一列是OTU ID,第一行是样本名 # 注意:read.csv默认会把行名放在第一列,我们需要正确处理 otu_raw <- read.csv("otu_table.csv", row.names = 1, check.names = FALSE) # 查看数据前6行和前6列,了解数据结构 head(otu_raw[, 1:6]) dim(otu_raw) # 查看数据维度:行数(物种数) x 列数(样本数)

关键检查点1:数据方向dim()输出的结果,通常应该是[物种数目, 样本数目]vegdist()要求样本在行,物种在列。如果你的数据是转置的(样本在列),需要先转置:otu_raw <- t(otu_raw)

关键检查点2:数据格式。用str(otu_raw)class(otu_raw)查看,确保它是一个data.framematrix,且内部的数值都是numeric(整数或小数)。如果有非数值列(如分类信息),需要先拆分出去。

关键检查点3:缺失值与零。用sum(is.na(otu_raw))检查是否有NA。群落数据中,NA可能代表未检测,通常需要根据情况处理(如视为0或移除)。用sum(otu_raw == 0) / length(otu_raw)可以计算数据的稀疏度(零的比例),微生物组数据通常非常稀疏(>70%的零)。

3.2 数据预处理(标准化)

对于群落数据,直接计算距离前,往往需要进行标准化,以消除样本间总测序深度(文库大小)不同带来的影响。最常用的方法是总和标准化(Total Sum Scaling),即将每个样本的计数转换为相对丰度(百分比)。

# 方法1:使用vegan包的decostand函数进行总和标准化 otu_relab <- decostand(otu_raw, method = "total") # 检查:每个样本的总和现在应该是1(或100,如果乘以100的话) colSums(otu_relab)[1:5] # 方法2:手动计算(原理相同) # otu_relab <- apply(otu_raw, 2, function(x) x / sum(x)) # 注意:apply的第二个参数,2表示按列(样本)计算。如果你的数据样本在行,则应为1。

重要提示vegdist()函数在计算某些距离(如bray,jaccard)时,内部会进行与算法相关的标准化处理。例如,Bray-Curtis计算时,每个样本的丰度在公式分母中已经被总和考虑了。因此,对于Bray-Curtis,你既可以使用原始计数,也可以使用相对丰度,两者计算出的距离矩阵在数值上可能不同,但样本间的相对距离关系(排序)通常高度一致。我个人的习惯是:对于Bray-Curtis,我倾向于使用原始计数,让函数内部处理;而对于计划使用欧氏距离的数据,则必须先进行标准化或Hellinger转化。

3.3 计算距离矩阵并解读

现在,我们来计算最常用的Bray-Curtis距离矩阵。

# 使用相对丰度数据计算Bray-Curtis距离 dist_bray <- vegdist(t(otu_relab), method = "bray") # 注意!如果otu_relab是物种为行,样本为列,需要转置(t)它,因为vegdist要求样本在行。 # 如果上一步你的数据已经是样本在行,则不需要t()。 # 查看距离对象 dist_bray # 输出是一个‘dist’对象,只显示了下三角部分,节省空间。 # 你可以看到距离的大致范围,确认是否在[0,1]之间。 # 查看距离矩阵的维度(样本数) attr(dist_bray, "Size") # 或者用:nrow(as.matrix(dist_bray)) # 将dist对象转换为矩阵,以便查看具体值 dist_matrix <- as.matrix(dist_bray) # 查看前4个样本之间的距离 dist_matrix[1:4, 1:4]

这个dist_bray对象,就是后续所有分析的起点。你可以把它输入到metaMDS()函数做NMDS排序,输入到hclust()函数做层次聚类,或者输入到adonis2()函数做PERMANOVA分析。

3.4 不同距离方法的对比计算

为了理解不同方法带来的差异,我们可以同时计算几种距离并简单比较。

# 计算几种常用距离 dist_jaccard <- vegdist(t(otu_relab), method = "jaccard") # 注意:这里用的还是丰度数据,但jaccard方法会将其视为二元数据?不! # 等一下,这里有坑!对于丰度数据直接使用method=“jaccard”,vegan实际上使用的是基于丰度的Jaccard变体(如“jaccard”对应的是“binomial”)。如果想用经典的二元Jaccard,应该: # 正确计算经典二元Jaccard距离(先转换为有无) otu_pa <- decostand(otu_raw, method = "pa") # “pa”即 presence-absence (0/1) dist_jaccard_binary <- vegdist(t(otu_pa), method = "jaccard") # 或者用 vegdist(t(otu_raw), method="jaccard", binary=TRUE) # 计算欧氏距离(在相对丰度数据上) dist_euclidean <- vegdist(t(otu_relab), method = "euclidean") # 计算曼哈顿距离 dist_manhattan <- vegdist(t(otu_relab), method = "manhattan") # 简单比较:查看第一个样本与其他样本在不同距离下的值 sample1_distances <- data.frame( Sample = rownames(dist_matrix)[2:attr(dist_bray, "Size")], Bray_Curtis = dist_matrix[2:attr(dist_bray, "Size"), 1], Jaccard_Binary = as.matrix(dist_jaccard_binary)[2:attr(dist_bray, "Size"), 1], Euclidean = as.matrix(dist_euclidean)[2:attr(dist_bray, "Size"), 1] ) head(sample1_distances)

通过这个对比,你可以直观感受不同度量标准下,样本间“差异”的绝对大小和排序是否一致。通常,Bray-Curtis和二元Jaccard的结果可能差异较大,这反映了丰度信息的重要性。

4. 高级应用与结果可视化

得到距离矩阵不是终点,而是起点。这里介绍两个最直接的应用:可视化与统计检验。

4.1 距离矩阵的可视化:热图与聚类树

热图是展示距离矩阵最直观的方式。

# 加载绘图需要的包 library(pheatmap) # 或者用ggplot2扩展包,但pheatmap最简单 # 使用pheatmap绘制距离矩阵热图 pheatmap(as.matrix(dist_bray), cluster_rows = TRUE, # 对行(样本)聚类 cluster_cols = TRUE, # 对列(样本)聚类,通常与行一致 clustering_distance_rows = dist_bray, # 聚类使用的距离 clustering_distance_cols = dist_bray, clustering_method = "average", # 聚类方法,可选"ward.D", "complete", "average"等 main = "Bray-Curtis Dissimilarity Heatmap", color = colorRampPalette(c("navy", "white", "firebrick3"))(100) # 自定义颜色梯度 )

从热图中,你可以快速看出哪些样本彼此更相似(颜色偏蓝/浅),哪些差异更大(颜色偏红)。聚类树状图展示了样本的层次分组关系。

4.2 基于距离的统计检验:PERMANOVA入门

如果你想检验不同分组(如处理组 vs 对照组)的群落结构是否有显著差异,PERMANOVA(通过vegan包的adonis2函数实现)是最常用的方法。它的原理是基于距离矩阵进行方差分析。

# 假设你有一个分组信息的数据框metadata,其中有一列“Group”表示样本的分组 # metadata的行名需要与距离矩阵的样本名一致 # 确保样本顺序一致 rownames(metadata) <- metadata$SampleID # 假设SampleID是样本名列 common_samples <- intersect(rownames(metadata), rownames(dist_matrix)) metadata <- metadata[common_samples, ] dist_matrix_sub <- as.matrix(dist_bray)[common_samples, common_samples] # 运行PERMANOVA # 注意:adonis2要求输入数据是dist对象,而不是矩阵 permanova_result <- adonis2(dist_bray ~ Group, data = metadata, permutations = 999) # 公式 dist ~ Group 表示检验Group分组对距离的解释程度 # permutations = 999 表示使用999次置换检验来计算p值 # 查看结果 print(permanova_result)

结果中,你会关注R2值(类似于回归中的R-squared,表示分组变量能解释的距离方差的比例)和Pr(>F)值(p值)。一个显著的p值(如<0.05)表明不同分组间的群落结构存在统计学差异。

重要警告:PERMANOVA的一个关键前提是组内离散度同质性(类似于方差齐性)。如果不同分组的样本在其多维空间内的分散程度(离散度)差异很大,PERMANOVA的结果可能不可靠,容易产生假阳性。在报告PERMANOVA结果前,务必用betadisper()函数检验离散度同质性。

# 检验离散度同质性 dispersion <- betadisper(dist_bray, group = metadata$Group) anova(dispersion) # 查看离散度差异的ANOVA检验结果 permutest(dispersion, permutations = 999) # 置换检验更稳健 plot(dispersion) # 可视化各组离散度(主坐标分析)

如果离散度检验结果显著(p<0.05),说明组间离散度不同,此时PERMANOVA的结果需要谨慎解释,或者考虑使用对离散度差异不敏感的其他方法。

5. 常见陷阱、问题排查与经验总结

即使理解了原理,实操中依然会遇到各种问题。下面是我总结的几个高频“坑点”。

5.1 错误:Error in rowSums(x, na.rm = TRUE) : 'x'必需是数值

  • 问题描述:运行vegdist()时出现此错误。
  • 原因排查
    1. 数据非数值:最常见。你的数据框中可能混入了字符型列(如分类学信息“k__Bacteria;p__Firmicutes”)。用str(your_data)检查每一列的数据类型。
    2. 数据方向错误vegdist()对每行求和,如果某一行全是非数值(比如物种名),就会报错。
  • 解决方案
    # 确保只将数值部分(通常是OTU丰度表)传递给vegdist # 假设你的数据框`df`前7列是分类信息,从第8列开始是样本丰度 otu_numeric <- df[, 8:ncol(df)] # 或者,如果分类信息是行名,直接使用整个数据框(但需确保全是数字) # 转换所有列为数值型(如果确信可以) otu_numeric <- apply(otu_numeric, 2, as.numeric) dist <- vegdist(otu_numeric, method="bray")

5.2 错误:距离值异常(全为0、NaN或非常大)

  • 全为0或NaN
    • 可能原因1:数据所有值都相同或几乎全为0。检查数据:summary(as.vector(as.matrix(your_data)))
    • 可能原因2:使用了binary=TRUE但数据已经是0/1,且样本间物种组成完全相同(概率极低)。
    • 可能原因3:数据中有大量NA,且na.rm=FALSE(默认)。计算时遇到NA会导致结果为NA。使用sum(is.na(your_data))检查,并用na.rm=TRUE或事先处理NA(如用0填充,但需有生物学依据)。
  • 距离值非常大(如欧氏距离)
    • 根本原因:未进行标准化,样本总丰度差异巨大。务必先进行标准化(如decostand(x, "total"))或使用对总丰度不敏感的距离(如Bray-Curtis)。

5.3 问题:应该选择哪种距离方法?

这是最常被问到的问题。我的决策流程通常是:

  1. 默认起点:对于绝大多数群落丰度数据,Bray-Curtis(method="bray") 是第一选择。它平衡了稳健性和解释性。
  2. 关注物种有无:如果你的科学问题更关注物种的分布、存在与否(例如,研究物种的地理分布界限),或者你的数据本质就是二元化的(如PCR产物电泳的有无),使用二元Jaccard(binary=TRUE或对0/1数据用method="jaccard")。
  3. 拥有系统发育树:对于微生物组数据,如果拥有可靠的系统发育树,一定要使用UniFrac距离(通过phyloseq::distance()GUniFrac包)。它能提供纯组成分析无法揭示的进化维度信息。
  4. 用于线性模型:如果你计划使用基于欧氏距离的线性模型方法,如RDA,那么对数据做Hellinger转化后计算欧氏距离是一个经典且数学性质良好的选择 (dist(decostand(x, "hellinger")),注意这里用stats::dist,因为vegdist的欧氏距离与dist相同)。
  5. 敏感性分析:在关键研究中,可以尝试2-3种不同的距离算法(如Bray-Curtis, Jaccard, UniFrac),看看主要结论(如分组是否分离)是否一致。如果结论一致,则结果非常稳健。如果不一致,则需要深入思考哪种距离更贴合你的生物学问题。

5.4 性能与大数据处理

当你的样本量很大(如>1000)时,计算距离矩阵和后续的置换检验(如PERMANOVA)会非常耗时。

  • 计算加速vegdist()函数本身是用C代码编写的,效率已经很高。对于超大规模数据,可以考虑:
    • 使用parallel包进行并行计算(如果算法支持)。
    • 使用专门为大数据设计的包,如bigstatsrBiocParallel,但可能需要自定义距离函数。
    • 在云计算平台(如RStudio Server on AWS)上使用更高配置的实例。
  • 降低维度:在计算距离前,可以考虑先过滤掉极低丰度或低出现率的物种(如在所有样本中总丰度<10或出现样本数<5%的OTU),这能显著减少数据维度且对整体群落模式影响通常很小。
    # 示例:过滤掉总丰度小于20的OTU otu_filtered <- otu_raw[rowSums(otu_raw) >= 20, ]

5.5 我的个人经验与最终建议

  • 标准化是习惯,不是教条:我养成的习惯是,对于任何新的群落数据集,在计算距离前,都会先用decostand(x, "total")看一眼相对丰度。这能帮我理解数据的规模。即使用Bray-Curtis,我也会对比一下使用原始计数和相对丰度结果的距离矩阵相关性(mantel检验),确保结论不受影响。
  • 距离矩阵的对象类型:记住vegdist()返回的是一个dist对象,不是矩阵。很多函数(如hclust(),adonis2())直接接受dist对象。当你需要提取特定样本对的距离,或进行矩阵运算时,再用as.matrix()转换。
  • 保存中间结果:距离矩阵的计算,特别是对于大型数据或像UniFrac这样的复杂距离,可能很耗时。计算完成后,用saveRDS(dist_bray, file = "my_dist_matrix.rds")将其保存到本地。下次分析时用readRDS()加载,可以节省大量时间。
  • 理解“零”的含义:在群落数据中,零可能代表“真不存在”、“存在但未检测到”或“测序深度不足”。对待零值的方式(如在Bray-Curtis中忽略双零)是生态学指数设计的智慧,也是与普通统计距离的根本区别。始终带着“生态学意义”去选择和理解距离。

最后,没有“唯一正确”的距离。vegdist()提供了一系列工具,最好的选择源于你对数据的理解、科学问题的界定以及方法本身的前提假设。从Bray-Curtis开始,结合具体问题尝试其他方法,并学会用mantel()函数比较不同距离矩阵的相关性,你会逐渐培养出对群落距离的直觉,让这把“尺子”真正为你所用,量出数据背后真实的生物学故事。

返回列表