微生物组季节性分析¶
一句话概述¶
对纵向微生物组数据进行时间序列周期性分析,利用Lomb-Scargle周期图、谐波回归等方法检测菌群组成的季节性波动模式,揭示环境因素驱动的微生物组时间动态。
核心知识点总览¶
| 知识点 | 关键内容 | 重要程度 |
|---|---|---|
| 纵向采样设计 | 时间分辨率/采样频率/持续时长 | ⭐⭐⭐⭐⭐ |
| Lomb-Scargle周期图 | 不等间隔采样的频率分析 | ⭐⭐⭐⭐⭐ |
| 谐波回归 | 正弦/余弦拟合年度周期 | ⭐⭐⭐⭐ |
| 季节性多样性 | Alpha/Beta多样性的季节模式 | ⭐⭐⭐⭐ |
| 环境驱动因素 | 温度/饮食/日照与菌群关联 | ⭐⭐⭐ |
| 去趋势处理 | 分离长期趋势与周期性 | ⭐⭐⭐ |
| 功能季节性 | 代谢通路的季节波动 | ⭐⭐⭐ |
| 人群vs个体 | 群体模式与个体变异 | ⭐⭐⭐ |
各步骤详解¶
第一步:纵向微生物组数据特点与研究设计¶
白话解释: 微生物组不是静止的——它随时间波动。一些波动是随机噪声,一些有规律性(如季节循环)。要检测季节性模式,需要覆盖至少1-2个完整年度周期的纵向采样数据,采样频率足够密(如每月至少1-2次)。
技术细节: 季节性分析的采样设计要求: - 持续时长:至少12-24个月覆盖完整年度周期 - 采样频率:每2-4周采样一次(满足Nyquist采样定理) - 样本量:多个个体的重复测量增加统计效力 - 元数据:采样日期、温度、饮食记录等环境变量
不等间隔采样的挑战:实际纵向研究中,采样时间点往往不均匀(如节假日缺失、受试者退出)。传统FFT要求等间隔数据,而Lomb-Scargle方法专门处理不等间隔时间序列。
# === 数据准备 ===
library(phyloseq)
library(dplyr)
# 读取纵向微生物组数据
ps <- readRDS("longitudinal_phyloseq.rds")
# 提取元数据
meta <- sample_data(ps) %>% as.data.frame()
meta$date <- as.Date(meta$collection_date)
meta$day_of_year <- as.numeric(format(meta$date, "%j")) # 1-365
meta$month <- as.numeric(format(meta$date, "%m"))
meta$season <- ifelse(meta$month %in% c(12, 1, 2), "Winter",
ifelse(meta$month %in% c(3, 4, 5), "Spring",
ifelse(meta$month %in% c(6, 7, 8), "Summer", "Autumn")))
# 检查采样分布
table(meta$month) # 各月样本数
range(meta$date) # 时间跨度
cat(sprintf("Duration: %d days\n", as.numeric(diff(range(meta$date)))))
第二步:Lomb-Scargle周期图分析¶
白话解释: Lomb-Scargle方法是天文学家发明的工具,专门用于分析不等间隔采样的时间序列中的周期性。它计算不同频率下数据的"功率"——如果某个频率处的功率很高,说明数据中存在该频率对应的周期性波动。对微生物数据,我们主要关注年周期(频率=1/365天)和半年周期。
技术细节:
# === Lomb-Scargle 周期图 ===
library(lomb)
# 对单个菌属的丰度做Lomb-Scargle分析
genus_ra <- transform_sample_counts(ps, function(x) x/sum(x))
genus_table <- as.data.frame(otu_table(genus_ra))
# 选择一个菌属
target_genus <- "Prevotella"
abundance <- as.numeric(genus_table[target_genus, ])
time_points <- as.numeric(meta$date - min(meta$date)) # 天数
# Lomb-Scargle周期图
ls_result <- lsp(abundance, times = time_points,
from = 7, to = 400, # 搜索7-400天的周期
type = "period",
ofac = 10, # 过采样因子
plot = TRUE)
# 查看最显著周期
cat(sprintf("Peak period: %.1f days\n", ls_result$peak.at[1]))
cat(sprintf("P-value: %.4f\n", ls_result$p.value))
# 如果peak在365天附近且p<0.05,说明有年周期性
# 批量分析所有菌属
all_genera <- rownames(genus_table)
ls_results <- data.frame()
for (genus in all_genera) {
abd <- as.numeric(genus_table[genus, ])
if (sum(abd > 0) < 10) next # 跳过低流行度菌
tryCatch({
ls <- lsp(abd, times = time_points, from = 7, to = 400,
type = "period", ofac = 10, plot = FALSE)
ls_results <- rbind(ls_results, data.frame(
genus = genus,
peak_period = ls$peak.at[1],
power = ls$peak,
p_value = ls$p.value
))
}, error = function(e) NULL)
}
# 筛选有年周期性的菌属(周期在300-400天且显著)
annual_periodic <- ls_results %>%
filter(peak_period > 300 & peak_period < 400 & p_value < 0.05) %>%
arrange(p_value)
cat(sprintf("Genera with annual periodicity: %d\n", nrow(annual_periodic)))
第三步:谐波回归模型¶
白话解释: 谐波回归用正弦和余弦函数来拟合周期性数据。如果微生物丰度确实有年周期性,那么它应该能被 a×sin(2π×t/365) + b×cos(2π×t/365) 这样的函数很好地拟合。谐波回归的优势是可以直接估计振幅(波动大小)和相位(峰值在一年中的什么时候)。
技术细节:
# === 谐波回归 ===
# 年周期的谐波回归
fit_harmonic <- function(abundance, day_of_year, subject_id = NULL) {
# 定义正弦余弦项
sin_term <- sin(2 * pi * day_of_year / 365.25)
cos_term <- cos(2 * pi * day_of_year / 365.25)
# 如果有重复测量,加入个体随机效应
if (!is.null(subject_id)) {
library(lme4)
df <- data.frame(y = abundance, sin_t = sin_term, cos_t = cos_term,
subject = subject_id)
model <- lmer(y ~ sin_t + cos_t + (1|subject), data = df)
} else {
df <- data.frame(y = abundance, sin_t = sin_term, cos_t = cos_term)
model <- lm(y ~ sin_t + cos_t, data = df)
}
# 提取振幅和相位
# 注意:R没有Python式的三元表达式,必须用if/else语句
if (!is.null(subject_id)) {
coefs <- fixef(model)
} else {
coefs <- coef(model)
}
A <- coefs["sin_t"]
B <- coefs["cos_t"]
amplitude <- sqrt(A^2 + B^2)
phase <- atan2(B, A) * 365.25 / (2 * pi) # 转换为天数
if (phase < 0) phase <- phase + 365.25
# 模型显著性与R-squared
if (!is.null(subject_id)) {
null_model <- lmer(y ~ (1|subject), data = df)
p_val <- anova(null_model, model)$`Pr(>Chisq)`[2]
# lmer模型没有r.squared,使用MuMIn::r.squaredGLMM或手动计算
r_sq <- NA # 混合模型的R^2需要额外计算
} else {
p_val <- summary(model)$fstatistic
p_val <- pf(p_val[1], p_val[2], p_val[3], lower.tail = FALSE)
r_sq <- summary(model)$r.squared
}
return(list(amplitude = amplitude, phase = phase, p_value = p_val,
model = model, r_squared = r_sq))
}
# 对所有菌属运行谐波回归
harmonic_results <- data.frame()
for (genus in rownames(genus_table)) {
abd <- as.numeric(genus_table[genus, ])
if (mean(abd > 0) < 0.1) next
result <- fit_harmonic(abd, meta$day_of_year, meta$subject_id)
harmonic_results <- rbind(harmonic_results, data.frame(
genus = genus,
amplitude = result$amplitude,
peak_day = result$phase,
p_value = result$p_value,
r_squared = result$r_squared
))
}
harmonic_results$padj <- p.adjust(harmonic_results$p_value, method = "BH")
seasonal_genera <- harmonic_results %>% filter(padj < 0.05) %>% arrange(padj)
第四步:多样性的季节性模式¶
白话解释: 除了单个菌的丰度变化,菌群整体的多样性(丰富度、均匀度)也可能有季节模式。比如在采集狩猎人群中,雨季和旱季的食物来源不同,导致肠道菌群多样性呈现明显的干湿季交替。
技术细节:
# === Alpha多样性季节性 ===
library(vegan)
# 计算alpha多样性
alpha_div <- data.frame(
Shannon = diversity(t(otu_table(ps)), index = "shannon"),
Richness = specnumber(t(otu_table(ps))),
Evenness = diversity(t(otu_table(ps))) / log(specnumber(t(otu_table(ps))))
)
alpha_div$month <- meta$month
alpha_div$day_of_year <- meta$day_of_year
alpha_div$subject <- meta$subject_id
# 谐波回归
diversity_seasonal <- fit_harmonic(alpha_div$Shannon, alpha_div$day_of_year,
alpha_div$subject)
cat(sprintf("Shannon diversity seasonality p = %.4f\n", diversity_seasonal$p_value))
cat(sprintf("Amplitude = %.3f, Peak day = %.0f\n",
diversity_seasonal$amplitude, diversity_seasonal$phase))
# 可视化月度变化(箱线图)
ggplot(alpha_div, aes(x = factor(month), y = Shannon)) +
geom_boxplot(fill = "lightblue") +
labs(x = "Month", y = "Shannon Diversity",
title = "Monthly Shannon Diversity") +
theme_minimal()
# === Beta多样性季节性 ===
# 使用PERMANOVA检验季节是否解释群落组成变异
bray_dist <- phyloseq::distance(ps, method = "bray")
adonis_season <- adonis2(bray_dist ~ season + subject_id,
data = meta, permutations = 999)
print(adonis_season)
# PCoA按季节着色
ordinate_result <- ordinate(ps, method = "PCoA", distance = "bray")
plot_ordination(ps, ordinate_result, color = "season") +
stat_ellipse() +
theme_minimal()
第五步:环境因素与季节性关联¶
白话解释: 微生物的季节性变化不是凭空产生的,而是由环境因素驱动——温度、日照、饮食变化(如冬季高脂饮食 vs 夏季高纤维蔬果)、户外活动等。将环境变量与菌群变化做相关分析,可以揭示季节性波动的驱动力。
技术细节:
# === 环境驱动因素分析 ===
library(Maaslin2)
# 准备环境变量
env_data <- data.frame(
sample_id = meta$sample_id,
temperature = meta$avg_temperature, # 月均温
daylight_hours = meta$daylight, # 日照时长
fiber_intake = meta$dietary_fiber, # 纤维摄入
day_of_year = meta$day_of_year,
subject_id = meta$subject_id
)
# MaAsLin2 纵向模型
fit_maaslin <- Maaslin2(
input_data = as.data.frame(t(genus_table)),
input_metadata = env_data,
output = "maaslin2_seasonal",
fixed_effects = c("temperature", "daylight_hours", "fiber_intake"),
random_effects = c("subject_id"),
normalization = "CLR",
transform = "NONE",
analysis_method = "LM"
)
# 分析哪些菌与温度显著关联
temp_assoc <- fit_maaslin$results %>%
filter(metadata == "temperature" & qval < 0.1) %>%
arrange(qval)
print(temp_assoc)
# 可视化温度-菌群关系
ggplot(data.frame(temp = env_data$temperature,
prevotella = as.numeric(genus_table["Prevotella", ])),
aes(x = temp, y = prevotella)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "loess") +
labs(x = "Temperature (°C)", y = "Prevotella relative abundance") +
theme_minimal()
第六步:去趋势与分解¶
白话解释: 时间序列可能同时包含:长期趋势(如随年龄缓慢变化)、季节性周期、和随机波动。需要将这些成分分解开来,才能准确识别真正的季节性模式。STL(Seasonal and Trend decomposition using Loess)是经典的时间序列分解方法。
技术细节:
# === 时间序列分解 ===
# 对于等间隔数据,使用STL分解
# 首先需要创建ts对象(假设月度汇总数据)
monthly_mean <- meta %>%
mutate(year_month = format(date, "%Y-%m")) %>%
group_by(year_month) %>%
summarise(
Shannon = mean(Shannon),
Prevotella = mean(genus_table["Prevotella", cur_group_rows()])
) %>%
arrange(year_month)
# 创建时间序列对象
ts_shannon <- ts(monthly_mean$Shannon, frequency = 12, start = c(2020, 1))
# STL分解
stl_result <- stl(ts_shannon, s.window = "periodic")
plot(stl_result, main = "STL Decomposition of Shannon Diversity")
# 提取各成分
seasonal_component <- stl_result$time.series[, "seasonal"]
trend_component <- stl_result$time.series[, "trend"]
remainder <- stl_result$time.series[, "remainder"]
# 季节性强度指标(Wang, Smith & Hyndman, 2006)
# 正确公式: F_S = max(0, 1 - Var(R) / Var(S + R))
# 分母是S+R的方差,不是Var(S)+Var(R)
var_sr <- var(seasonal_component + remainder)
var_remainder <- var(remainder)
seasonality_strength <- max(0, 1 - var_remainder / var_sr)
cat(sprintf("Seasonality strength: %.3f\n", seasonality_strength))
# 接近1表示季节性主导, 接近0表示无季节性, > 0.7认为有较强季节性
第七步:个体间季节性异质性¶
白话解释: 群体水平的季节性模式背后,不同个体的表现可能非常不同——有人的菌群波动剧烈,有人则稳定如常。分析个体间的季节性异质性,以及什么因素决定了个体的"季节性敏感度",是深入理解的关键。
技术细节:
# === 个体水平季节性分析 ===
# 对每个个体分别做谐波回归
subjects <- unique(meta$subject_id)
individual_seasonality <- data.frame()
for (subj in subjects) {
subj_idx <- meta$subject_id == subj
subj_data <- genus_table[, subj_idx]
subj_days <- meta$day_of_year[subj_idx]
if (sum(subj_idx) < 8) next # 至少8个时间点
# 对Shannon多样性
shannon_vals <- diversity(t(subj_data), index = "shannon")
result <- tryCatch(
fit_harmonic(shannon_vals, subj_days, subject_id = NULL),
error = function(e) NULL
)
if (!is.null(result)) {
individual_seasonality <- rbind(individual_seasonality, data.frame(
subject = subj,
amplitude = result$amplitude,
peak_day = result$phase,
p_value = result$p_value
))
}
}
# 个体季节性振幅分布
hist(individual_seasonality$amplitude, breaks = 20,
main = "Individual Seasonality Amplitude",
xlab = "Amplitude of annual cycle")
# 哪些因素与个体季节性敏感度相关?
# 合并个体元数据(如饮食多样性、BMI、运动量)
merged <- merge(individual_seasonality, subject_metadata, by = "subject")
cor.test(merged$amplitude, merged$dietary_diversity)
cor.test(merged$amplitude, merged$outdoor_hours)
实战命令速查¶
# 快速季节性分析
library(lomb)
# Lomb-Scargle
lsp(abundance, times=time_days, from=7, to=400, type="period")
# 谐波回归
lm(abundance ~ sin(2*pi*day/365) + cos(2*pi*day/365))
# PERMANOVA
adonis2(dist ~ season + subject, data=meta)
面试常问点¶
Q1: 为什么用Lomb-Scargle而不是傅里叶变换(FFT)?¶
A: FFT要求等间隔采样(exactly periodic sampling),而真实纵向研究的采样时间点几乎不可能完全均匀。Lomb-Scargle专门设计用于不等间隔时间序列,通过拟合正弦函数评估每个频率的功率,不需要插值或重采样,且提供统计显著性评估。
Q2: 检测年周期性需要多长的数据?¶
A: 理论上至少需要覆盖1个完整周期(12个月),但可靠检测通常需要2-3个完整周期(24-36个月)。只有1个周期时无法区分真实周期性和单次变化。采样频率也重要:月周期需要至少每2周采样,年周期需要至少每月采样(Nyquist定理)。
Q3: 谐波回归中的振幅和相位如何解释?¶
A: 振幅代表季节性波动的"大小"——振幅越大,季节间差异越明显。相位代表"峰值时间"——相位=180天表示峰值在7月(北半球夏季)。两者结合可以完整描述季节模式:如"Prevotella在夏季(第200天)达到峰值,振幅为0.05(相对丰度变化5%)"。
Q4: 如何区分真实季节性和混杂因素导致的伪周期性?¶
A: (1) 控制已知季节性混杂因素(如饮食、药物使用的季节模式)加入模型;(2) 检查不同年份的pattern是否一致(真实季节性应可重复);(3) 考虑技术批次——如果所有样本按季节批次处理,批次效应可能伪装成季节性;(4) 在多个独立队列中验证。
Q5: Hadza人群的菌群季节性研究发现了什么?¶
A: Smits et al. (Science, 2017) 发现坦桑尼亚Hadza狩猎采集人群的肠道菌群有显著季节循环:旱季(更多狩猎/肉食)时菌群多样性更高,Bacteroidetes(尤其Prevotellaceae)丰度高;雨季(更多浆果/蜂蜜等采集食物)时Bacteroidetes(主要是Prevotellaceae)显著下降约70%,但在下一个旱季又重新出现。Firmicutes则相对稳定。一些在工业化人群中"丢失"的菌在Hadza中呈现旱季出现、雨季消失的周期性波动,暗示工业化导致的菌群多样性丧失可能与失去季节性饮食变化有关。
易错点¶
1. 采样时间跨度不足就声称有/无季节性¶
仅6个月的数据无法可靠检测年周期性(混淆trend和cycle)。至少覆盖12个月,推荐24+个月。
2. 忽略个体内重复测量的统计处理¶
纵向数据中同一个体的多次测量不独立。必须使用混合效应模型或GEE处理repeated measures,否则p值被严重低估。
3. 多重检验问题¶
同时检验数百个菌属的季节性,不做FDR校正会产生大量假阳性。
4. 将南北半球数据混合分析¶
南北半球季节相反。如果队列跨越赤道,直接按月份分析会掩盖真实季节性。应按纬度分组或使用连续的环境变量(温度)代替离散的季节标签。
5. 组成性数据的伪季节性¶
相对丰度数据中,一个菌的真实增加会导致其他菌的相对丰度"下降"——这种下降是数学假象而非真实变化。使用CLR变换或绝对定量(spike-in/qPCR)可缓解。
补充知识¶
代表性研究¶
- Smits et al., Science, 2017 (Hadza季节性)
- Davenport et al., Cell Host & Microbe, 2014 (Hutterite人群)
- Flores et al., Genome Biology, 2014 (每日纵向采样)
相关R包¶
- lomb:Lomb-Scargle周期图
- season:季节性分析工具集
- forecast:时间序列分析与预测
- astsa:时间序列分析(含谱分析)
引用推荐¶
- Lomb-Scargle: Lomb, Astrophysics and Space Science, 1976; Scargle, The Astrophysical Journal, 1982
- 微生物组季节性综述: Reitmeier et al., Cell Host & Microbe, 2021