模拟数据集生成:用 NumPy 搭建多组学数据
做 MOFA 模型调优之前,我最头疼的一件事就是找不到合适的数据集。公开数据集要么样本量太大,要么预处理不干净,跑一次实验光清理数据就得搭进去半天。所以我自己动手,用 NumPy 生成一套模拟多组学数据。听起来像造假?其实不是,模拟数据能帮你精准控制信号强度、噪声水平、组学间的共享结构,非常适合验证算法效果。
转录组:模拟基因表达矩阵
转录组数据通常是连续值,比如 RNA-seq 的 TPM 或 FPKM。我一般用负二项分布来模拟,因为真实表达数据通常是过离散的。不过为了简单,这里先用正态分布替代,加一点随机噪声就行。
import numpy as np
np.random.seed(42)
n_samples = 100
n_genes = 500
# 假设有 3 个潜在因子(比如处理条件)
latent = np.random.randn(n_samples, 3)
# 基因载荷矩阵
loadings = np.random.randn(3, n_genes) * 0.5
# 生成表达矩阵
expr = latent @ loadings + np.random.randn(n_samples, n_genes) * 0.1
# 转成正数(模拟表达值)
expr = np.abs(expr)
print(f"转录组数据形状: {expr.shape}")
我习惯把噪声标准差设成 0.1,这样信号强度大概占方差的三分之二,不至于一眼就看穿模式。你在实际调参时可以试试调到 0.3 或 0.5,观察 MOFA 对噪声的鲁棒性。
甲基化:用 Beta 分布模拟 β 值
DNA 甲基化数据通常是 0 到 1 之间的 β 值。Beta 分布天然适合这种区间,而且它的形状参数能控制尖峰和拖尾。我早年做表观遗传项目时,发现直接硬截断正态分布会引入假阳性,后来就一直用 Beta 分布来模拟甲基化矩阵。
n_cpgs = 300
# 用潜在因子生成甲基化倾向
methyl_prop = np.random.beta(2, 2, (n_samples, n_cpgs))
# 加入因子结构
shared_factor = latent[:, :2] @ np.random.randn(2, n_cpgs) * 0.3
methyl_beta = np.clip(methyl_prop + shared_factor, 0.01, 0.99)
print(f"甲基化β值范围: {methyl_beta.min():.2f} ~ {methyl_beta.max():.2f}")
注意 Beta 分布的参数 a=2, b=2 时比较均匀,如果改成 a=1, b=1 就变成均匀分布,更适合极端情况测试。
蛋白质组:模拟对数强度值
蛋白质组学数据通常来自质谱检测,表达值一般是对数转换后的强度,近似正态分布。我直接复用转录组的结构,但把噪声加大一点,模拟技术差异。
n_proteins = 200
protein = latent[:, :2] @ np.random.randn(2, n_proteins) * 0.4
protein += np.random.randn(n_samples, n_proteins) * 0.3
protein = np.abs(protein) + 1 # 避免零值
print(f"蛋白质组数据形状: {protein.shape}")
这里特意只用了前两个潜在因子,和转录组的三个因子不完全一样。为什么要这样?因为真实的多组学数据中,不同组学受共享因子影响的程度不同,留一点私有因子才能更真实地模拟 MOFA 需要分解的结构。
合并成多组学列表
MOFA 要求输入是一个列表,每个元素是样本 × 特征的矩阵。我把三个组学压成一个 list,再分别标准化。
data_list = [expr, methyl_beta, protein]
# 标准化(按组学独立做,不能跨组学混在一起)
data_scaled = []
for mat in data_list:
mean = mat.mean(axis=0)
std = mat.std(axis=0)
std[std == 0] = 1 # 防止除零
data_scaled.append((mat - mean) / std)
# 保存为 NumPy 文件备用
np.savez('multi_omics_sim.npz', *data_scaled)
标准化这一步很多人会忽略,但 MOFA 假设每个组学的特征服从平均 0 方差 1 的分布。如果不标准化,数值大的组学(比如蛋白质组的强度)会主导因子分解,结果就可能偏差。
参数对比一览
| 组学类型 | 分布假设 | 样本数 | 特征数 | 共享因子数 | 噪声水平 |
|---|---|---|---|---|---|
| 转录组 | 正态(绝对值) | 100 | 500 | 3 | 0.1 |
| 甲基化 | Beta (2,2)+因子偏移 | 100 | 300 | 2 | 0.3 |
| 蛋白质组 | 对数正态(简化) | 100 | 200 | 2 | 0.3 |
上面这组参数只是起点,我个人建议你至少跑三次不同噪声级别的实验,看看 MOFA 的因子一致性是否稳定。我曾经因为噪声设太低,导致模型完美拟合垃圾信号,浪费了两周时间。嗯,避坑指南:模拟数据一定要包含噪声和缺失因子结构,别做得太完美。
流程可视化
我画了一张示意图,帮你快速理解整个模拟流水线。
关键要点:三个组学用了不同分布和不同共享因子数目,目的就是让模拟数据既存在共享结构,又保留各自的私有噪声。这样 MOFA 才能展现出它分解“共享因子+私有因子”的核心能力。
常见踩坑与我的解决经验
- 因子数目不匹配:我在生成甲基化数据时只用了两个共享因子,转录组用了三个。如果后面做 MOFA 训练时统一设成 K=3,甲基化那一层的第三个因子就会退化或变噪声。我建议你分别设不同因子数,看看 MOFA 是否能自动识别哪些因子共享度高。
- 缺失值处理:真实多组学里总有缺失值,但模拟数据里我没加。你可以主动删除 5% 的甲基化数据,观察 MOFA 的 imputation 效果。我之前做过一次 30% 缺失率的测试,结果因子结构和完整数据几乎一致,说明 MOFA 对缺失挺稳健。
- 样本对齐:三个组学的样本 ID 必须一一对应。我遇到过有人故意打乱甲基化行顺序,直接用 .values 拼接导致对齐错误。切记保存时用同样的索引顺序。
小技巧:保存模拟数据时,可以顺便把真实的因子矩阵(latent)和载荷(loadings)一起存下来。后面做因子解读时,直接跟真实因子对比,一眼就能看出模型学得准不准。
慎重:不要用过于简单的噪声模型。比如只用加法高斯噪声,MOFA 很容易就完美恢复因子结构。现实中数据噪声是异质的(不同组学、不同样本差异大),建议在蛋白质组和甲基化里加入 10% 的极端值(outlier)模拟技术假象。
好了,现在你手里已经有了一个结构清晰、噪声可控的多组学模拟数据集。后面几章我们会拿它来调 MOFA 的参数、评估因子分解效果。这段代码在我自己的实验里跑了不下五十遍,每次改一点参数就能看到模型行为的变化,值不值得你花十分钟亲手敲一遍?