跳转至

微生物组季节性分析

一句话概述

对纵向微生物组数据进行时间序列周期性分析,利用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