第四章:探索性数据分析——基础统计、箱线图、直方图、密度图

拿到数据第一件事,别急着调参数。

我做数据分析这几年,最深刻的体会就是:先看数据长什么样。你连数据的脾气都没摸清楚,上来就咔咔跑模型,多半要翻车。

这一章,咱们就聊聊怎么用R语言给丰度数据做“体检”。说白了就是四板斧:基础统计量、箱线图、直方图、密度图。别看都是老掉牙的东西,想用透真不容易。

核心要诀:EDA不是为了好看,是为了发现异常、判断分布、为后续参数调优打底。你花在这个阶段的时间,绝对能在后面几十倍地赚回来。

4.1 用基础统计量摸清数据底细

我第一次接触宏基因组丰度数据时,上来就画热图。结果图是挺漂亮,可有一半的数值都是零,根本没法看。

从那以后,我养成了一个习惯:先跑一轮summary()。

# 假设你的丰度数据长这样
abundance_data <- read.csv("otu_table.csv", row.names = 1)

# 先看整体
summary(abundance_data[, 1:5])  # 挑前几个样本看看

# 常用的统计量
library(psych)
describe(abundance_data)

# 重点关注几个指标
# 1. 零值占比
zero_ratio <- apply(abundance_data, 1, function(x) sum(x == 0) / length(x))
# 2. 极值检查
max_vals <- apply(abundance_data, 1, max, na.rm = TRUE)

到这里,你就能回答几个关键问题了:

  • 数据是稀疏还是稠密?(零值比例高不高)
  • 有没有离谱的异常值?(最大值比第三四分位数大几个数量级)
  • 数据是不是偏态分布?(均值和中位数差多少)

我的小技巧:个人习惯先看quantile()的0%、25%、50%、75%、100%五段。如果100%那行比75%大了几十倍,基本就是个极端异常值。要不要删?别急,等画完图再说。

4.2 箱线图:一眼看穿离群样本

箱线图这玩意儿,说白了就是把summary()的结果画出来。但视觉冲击力完全不一样。

我记得有一次分析肠道菌群数据,summary一切正常,结果画了箱线图才发现:有个样本的中位丰度是其他样本的3倍。后来查记录,是标记错了。

你想想看,要是没这一步,后面所有结论都白搭。

# 基础箱线图
boxplot(abundance_data, 
        main = "样本丰度分布概览",
        xlab = "样本编号", 
        ylab = "OTU丰度",
        col = "#3498db",
        outline = TRUE)  # 保留异常值

# 如果样本太多,转置一下看OTU分布
boxplot(t(abundance_data[, 1:20]), 
        main = "前20个OTU的丰度分布",
        las = 2)  # 标签旋转

# 配合ggplot2更灵活
library(ggplot2)
library(reshape2)

abundance_melt <- melt(abundance_data)
ggplot(abundance_melt, aes(x = variable, y = value)) +
  geom_boxplot(fill = "#2ecc71", outlier.color = "#e74c3c") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
  labs(x = "样本", y = "丰度")

看箱线图重点关注什么?

  • 箱子高度——样本间的变异程度
  • 须的长度——极端值多不多
  • 中点位置——中位数在箱子里的位置,决定了偏态方向
  • 异常点——那些超出1.5倍四分位距的点,逐个检查

避坑指南:我曾经把异常值直接全删了,结果发现有一类样本的异常值是生物学信号。后来学乖了:画图时先用outline = FALSE看一下整体分布趋势,再单独把异常点拎出来看。

4.3 直方图:判断分布形态

直方图能告诉你数据的“形状”。是正态、是偏态、还是双峰?这个信息直接决定你后续用什么统计方法。

比如,你要做差异分析。数据是正态分布,可以用t-test;不是的话,得用Wilcoxon。选错了,结果不可靠。

# 单个样本的直方图
hist(abundance_data[, 1], 
     breaks = 30,  # 调整组数
     main = "样本1的丰度分布",
     xlab = "丰度",
     col = "#9b59b6",
     border = "white")

# 多图对比,用par布局
par(mfrow = c(2, 3))
for(i in 1:6) {
  hist(abundance_data[, i], 
       breaks = 20,
       main = paste("样本", i),
       xlab = "丰度",
       col = "#1abc9c")
}

# 或者用ggplot2分面
ggplot(abundance_melt, aes(x = value)) +
  geom_histogram(bins = 30, fill = "#f39c12", color = "white") +
  facet_wrap(~ variable, scales = "free") +
  labs(x = "丰度", y = "频数")

看直方图时,我会问自己三个问题:

  1. 数据是不是严重右偏?(大多数丰度数据都是,因为很多OTU丰度很低)
  2. 有没有明显的双峰?(可能暗示不同的菌群类型)
  3. 零值是不是单独成了一个峰?(需要决定要不要做零值处理)

参数选择:breaks这个参数挺关键。组数太少,细节全丢了;组数太多,毛刺太多看不清趋势。我的习惯是先试breaks = "FD"(Freedman-Diaconis法则),不满意再手动调。

4.4 密度图:平滑版的直方图

密度图可以理解为直方图的平滑版本。它用核密度估计(KDE)画出一条光滑曲线,能更清晰地展示数据的分布趋势。

# 基础密度图
plot(density(abundance_data[, 1]), 
     main = "样本1的丰度密度曲线",
     xlab = "丰度",
     col = "#e74c3c",
     lwd = 2)

# 多条曲线叠加
plot(density(abundance_data[, 1]), 
     col = "#3498db", 
     lwd = 2,
     main = "多个样本的密度对比",
     xlim = c(0, 100))
lines(density(abundance_data[, 2]), col = "#e74c3c", lwd = 2)
lines(density(abundance_data[, 3]), col = "#2ecc71", lwd = 2)
legend("topright", 
       legend = paste("样本", 1:3),
       col = c("#3498db", "#e74c3c", "#2ecc71"),
       lwd = 2)

# 对数转换后的密度图
library(vegan)
# 假设做了对数转换
log_data <- decostand(abundance_data, method = "log")
plot(density(log_data[, 1]), 
     main = "log转换后的丰度分布",
     col = "#9b59b6",
     lwd = 2)

密度图最实用的地方,是你能直观对比不同组样本的分布。比如:

  • 处理组和对照组的分布有没有明显错位?
  • 对数转换后,数据是不是更接近正态?
  • 不同样本的曲线形状是否一致?(不一致可能意味着批次效应)

一个实用经验:做密度图时,调一下bw(带宽)参数看看。我觉得默认的"SJ"方法效果不错,但有时候用"bcv"能抓到更多细节。两种都试试,选一个最合理的。

4.5 图形结合:完整的数据体检流程

到这里,基础统计、箱线图、直方图、密度图,四样工具我们都过了一遍。但实际的EDA不能孤立地用。

我的标准流程是这样的:先跑summary看数字,发现异常后用箱线图定位,判断分布用直方图,细节呈现用密度图。四者配合起来,就能在几分钟内掌握数据的全貌。

EDA数据体检四步走 第一步:基础统计 summary() / describe() 第二步:箱线图 boxplot / geom_boxplot 第三步:直方图 hist / geom_histogram 第四步:密度图 density / geom_density 检查要点 • 零值比例是否过高 • 是否存在极端异常值 • 分布是否严重偏态 • 样本间变异是否一致 常见应对 • 零值太多→丰度过滤 • 偏态严重→对数转换 • 异常值→核实或缩尾 • 批次效应→校正处理 汇总:数据预处理决策依据 四步校验通过后,再进入参数调优阶段

4.6 实战:从原始数据到分析决策

拿一个真实的土壤微生物数据来说。

原始数据有5000个OTU、30个样本。走完上述四步,我发现了几个问题:

  1. summary结果显示,有40%的OTU在所有样本中都是零——这部分得过滤掉。
  2. 箱线图显示,样本S03的箱子远高于其他样本——查发现是DNA提取量不同,需作标准化。
  3. 直方图显示,原始丰度是典型的幂律分布——后续差异分析必须用非参数方法。
  4. 密度图显示,对数转换后,数据接近正态——可以放心用线性模型。
# 完整的EDA流程(组合拳)
eda_report <- function(data) {
  cat("===== 1. 基础统计 =====\n")
  print(apply(data, 2, summary))
  
  cat("\n===== 2. 零值比例 =====\n")
  zero_pct <- apply(data, 2, function(x) sum(x == 0) / length(x) * 100)
  print(sort(zero_pct, decreasing = TRUE)[1:10])
  
  cat("\n===== 3. 箱线图 =====\n")
  boxplot(data, outline = FALSE, col = "#3498db")
  
  cat("\n===== 4. 直方图(汇总) =====\n")
  hist(rowMeans(data), breaks = 30, col = "#2ecc71")
  
  cat("\n===== 5. 密度图(对数尺度) =====\n")
  log_data <- log1p(data)
  plot(density(rowMeans(log_data)), col = "#e74c3c", lwd = 2)
}

# 跑一遍
eda_report(abundance_data)

这个函数我每次拿到新数据都会先跑一次。里面的参数都是调过的,箱子颜色、组数、尺度,都是多年经验积累出来的。

最后啰嗦一句:EDA做到位,后面调参就是水到渠成的事。反过来,数据没看明白就调参,纯粹是浪费计算资源。我见过太多人在交叉验证上花几个月,结果问题出在数据质量上。先体检,再治病——这个顺序千万别搞反。