☰
数据挖掘实战总结:CRISP-DM流程、模型评估与GEO差异分析
2026/9/30 1:21:41 网站建设 项目流程

数据挖掘这门课,我开学时以为是教算法的,上了三周才发现它真正教的是"怎么把一堆又脏又乱的数据,变成能站得住脚的结论"。考完期末最后一门,我把整学期的笔记、实验代码和期末大作业从头翻了一遍,才意识到这门课的知识点其实分成两条线:一条是卷面上反复考的理论骨架,另一条是真正动手时才会暴露出来的工程细节。前一条线决定你能不能及格,后一条线决定你做完项目之后敢不敢说"这个结果我信"。这篇总结不打算按教材目录复述,而是按我自己的理解顺序,把数据挖掘的流程、算法决策逻辑、模型评估的坑,以及期末大作业里那套从 GSE 数据下载、预处理质控到差异分析的完整流程,全部摊开讲一遍。如果你正在准备期末、或者要交一份数据挖掘方向的大作业,希望这些内容能让你少走几步弯路。

1. 把一学期的数据挖掘课浓缩成一张能力地图

1.1 CRISP-DM 那六步,为什么考试和项目都绕不开

教材上一定会给出 KDD 流程和 CRISP-DM 两个版本,很多人背完就忘,其实这两个东西的性质完全不同。KDD 是 Fayyad 在 1996 年提出的学术框架,讲的是"从数据库中发现知识"这个动作本身,包括选择、预处理、变换、挖掘、解释评估五个阶段,重点是"知识"这两个字。而 CRISP-DM 是工业界总结出来的项目管理流程,六个阶段是商业理解、数据理解、数据准备、建模、评估、部署,它的核心是"循环"——建模之后发现数据不够,要退回数据准备;评估之后发现商业目标没对齐,要退回商业理解。

期末卷子特别喜欢考两个点:一是"哪个阶段最耗时",标准答案是数据准备,通常占到整个项目 60% 到 80% 的工作量;二是"部署阶段到底做什么",很多人以为部署就是发个报告,实际上它包括把模型封装成接口、写监控脚本、定义模型衰减后的重训触发条件。我当时答这道题只写了"上线运行",被扣了一半分,后来才明白部署的本质是"让模型在没人盯着的情况下也能持续产出可用的结果"。

把这两个框架放在一起看,你会发现它们其实是一回事:CRISP-DM 把 KDD 的"预处理"拆成了数据理解和数据准备,把"解释评估"拆成了评估和部署。考试答题时,如果题目只问流程,写 CRISP-DM 更稳;如果问的是知识发现的本质,写 KDD 更贴题。

1.2 预处理吃掉一半时间,这不是效率低

刚做实验的时候我特别不理解,为什么老师给的实验数据总是缺几个值、多几个异常点,是不是故意为难人。后来做了大作业才知道,真实数据就是这样:传感器断线、问卷漏填、系统导出的时间戳格式不统一、同一张表里"北京"和"北京市"并存。

预处理这件事可以拆成四类动作,每一类都有它的判断依据:

  • 缺失值:先看缺失比例和缺失机制。缺失率低于 5% 且完全随机缺失,直接删行或用中位数填补都行;缺失率超过 30% 的字段,要看它是不是本身就有信息量,比如"是否有房贷"这个字段空缺,很可能代表"没有",那就不能随便补。KNN 填补和基于模型的填补适合缺失有规律的情况,但计算成本高,而且会引入人为的相关性。
  • 异常值:IQR 方法(上下四分位加减 1.5 倍四分位距)适合偏态分布,Z-score 的 3σ 规则适合近似正态分布。关键是要先判断异常值是错误还是真实信号,信用卡欺诈检测里的"异常值"恰恰是目标,删掉就完了。
  • 规范化:Min-Max 把数据压到 [0,1],对异常值极其敏感;Z-score 标准化后均值为 0、方差为 1,是最常用的选择。只要模型涉及距离计算或者梯度下降,比如 KNN、K-Means、SVM、神经网络,就必须标准化;决策树和随机森林基于分裂点,对量纲不敏感,可以不做。
  • 离散化与编码:等宽、等频、基于熵的分箱各有适用场景,类别变量用 One-Hot 编码时要注意维度爆炸,高基数类别(比如用户 ID)更适合目标编码或者嵌入。

这几类动作看起来零碎,但卷子上的简答题基本都是从"为什么要做"这个角度问的。我后来总结出一句话:预处理不是把数据洗干净,而是把数据整理成模型能理解的形状。

1.3 算法只是工具箱,选型依据才是得分点

学期中段我一度陷入"背算法"的误区,把 SVM 的对偶形式、EM 算法的推导全部抄了一遍,结果做项目的时候面对一份电商用户行为数据,完全不知道该从哪个算法下手。

后来我强迫自己建立一个选型清单,按顺序问四个问题:

判断维度追问内容典型结论
有没有标签目标变量是否已知有标签走分类/回归,无标签走聚类/关联
数据规模与维度样本量、特征数、稀疏程度小样本高维优先 SVM、正则化线性模型;大样本优先树模型、神经网络
可解释性要求是否需要向业务方解释每个决策金融风控、医疗诊断偏好决策树、逻辑回归;推荐排序可以用 GBDT、深度模型
训练与推理成本是否要在线实时预测实时场景慎用 KNN(需要保存全部训练样本)、深度模型

这四个问题回答完,候选算法通常就剩两三个了。剩下的用交叉验证跑一遍,看指标和稳定性,基本就能定下来。考试的时候,如果题目给了一个具体场景让你选算法,按这个顺序写一遍,比直接写"我用随机森林因为效果好"要拿分得多。

2. 期末卷子上反复出现的算法,我按决策逻辑重新串了一遍

2.1 决策树:从"第一刀切在哪"到剪枝止损

决策树的本质是一个递归的二分过程,每一步都在问:用哪个特征、取哪个阈值,能让切分后的子集"纯度"提升最多。这里的纯度就是信息论的主场。ID3 用信息增益,公式是父节点熵减去子节点熵的加权和,熵的定义是负的各类别概率乘以其对数之和,以 2 为底。信息增益有个明显缺陷:取值多的特征天然占便宜,比如"身份证号"这种每个样本都不同的特征,增益接近最大值,但完全没有泛化能力。C4.5 用信息增益率修正,分母是该特征自身的分裂信息,相当于给"碎"的特征加了个惩罚项。CART 干脆换成基尼指数,计算只涉及平方,不用算对数,工程上更快,而且天然支持回归任务。

剪枝是决策树从"能背下训练集"变成"能泛化"的关键。预剪枝在分裂前判断,比如限制最大深度、要求叶子节点最少样本数、要求分裂带来的增益超过阈值,优点是省计算,缺点是容易欠拟合,因为有些分裂当下看起来没用,两步之后才有价值。后剪枝是先把树长满,再自底向上地把子树替换成叶节点,用验证集判断替换后是否变好,代价是计算量大,但效果通常更好。CART 用的代价复杂度剪枝就是后剪枝,通过一个惩罚系数控制树的规模,考试里经常让你写出这个损失函数的形式。

还有两个细节容易被忽略:连续值特征的处理,是先把取值排序,然后在相邻取值的中点作为候选切分点,逐个计算增益,选最优的那个;缺失值处理上,C4.5 会让样本以不同权重同时进入多个分支,而不是简单丢掉。这两点在大题里出现过,属于"不会写就丢分"的类型。

2.2 K-Means 的三个致命参数:K、初始点、停止条件

K-Means 的算法本身只有三行:随机选初始中心、把每个点分配给最近的中心、用簇内均值更新中心,重复到收敛。但它有三个参数会直接决定结果好坏。

K 值的选择靠肘部法和轮廓系数。肘部法是画出不同 K 对应的簇内平方和(SSE),找曲线拐点;轮廓系数是衡量每个点与自己簇的紧密度和与最近邻簇的分离度,取值范围从 -1 到 1,越大越好。实际项目里我用得更多的是业务约束加轮廓系数交叉验证,比如用户分群业务方要求分成 3 到 5 类,那就在这个范围里选轮廓系数最高的。

初始点的问题在于 K-Means 只能保证收敛到局部最优,不同的初始化跑出来的结果可能差别很大。K-Means++ 的做法是让第一个中心随机选,后续每个中心以与已有中心距离平方成正比的概率被选中,这样初始中心天然分散。工程上更省事的办法是设置随机种子跑二十次,取 SSE 最小的那一次。我在实验里就吃过亏,没设种子,同一份数据两次运行分出来的簇完全不一样,报告里的结论前后矛盾,只能重跑。

停止条件一般用中心点位移小于阈值或者达到最大迭代次数,但要注意数据必须标准化,否则量纲大的特征会主导距离计算,聚类结果基本等于按那一个维度排序。另外 K-Means 假设簇是凸的、大小相近的球形,遇到环形分布、密度差异大的数据,就应该换 DBSCAN 或者谱聚类。DBSCAN 有两个参数 eps 和 MinPts,能识别噪声点,也能发现任意形状的簇,代价是对参数敏感,密度不均匀的数据很难调。

2.3 Apriori 的剪枝 vs FP-Growth 的不生成候选

关联规则挖掘这块,考试基本围绕支持度、置信度、提升度三个指标转。支持度是项集出现的频次占比,置信度是条件概率,提升度是置信度除以后件的支持度,用来判断两个项是不是真的相关——提升度等于 1 说明独立,大于 1 才是正相关。

Apriori 的核心是先验性质:一个项集如果是频繁的,它的所有子集也必然频繁;反过来,如果一个项集的某个子集不频繁,那这个项集一定不频繁。整个算法就是先扫一遍数据库得到频繁 1 项集,然后由 k 项集连接生成 k+1 项集的候选,再用数据库扫描剪掉不满足最小支持度的候选,循环到没有候选为止。它的痛点是每次都要重新扫描数据库,候选集数量在密集数据上会爆炸。

FP-Growth 换了个思路,先把数据库压成一棵 FP 树,每个节点记录该项在路径上的计数,然后在树上递归地构造条件模式基,直接长出频繁项集,全程不生成候选集,只需要扫两遍数据库。两者的对比是高频考点:

对比项AprioriFP-Growth
数据库扫描次数每个长度层扫一次两次
是否生成候选集生成,可能爆炸不生成
内存占用较低需要驻留 FP 树
适用数据稀疏、项集较短密集、长事务
实现难度简单较复杂

实际用的时候,如果只是想做"啤酒加尿布"这类简单组合推荐,Apriori 设一个合适的最小支持度就够了;如果是购物篮很长、组合特别多的情况,FP-Growth 明显更快。还有个容易丢分的点:强关联规则不一定有商业价值,必须结合提升度看,同时要警惕"辛普森悖论",分层之后结论可能反转。

2.4 集成学习:Bagging 降方差,Boosting 降偏差

集成学习这块我用一句话记住:Bagging 是并行的,主要降方差;Boosting 是串行的,主要降偏差。随机森林就是 Bagging 的加强版,除了对样本做有放回抽样,还在每个节点分裂时随机抽取一部分特征作为候选,这样进一步降低了树之间的相关性。因为每棵树都在不同的数据子集和特征子集上训练,平均之后方差显著下降,而且不容易过拟合,深度可以放得比较开,甚至不剪枝。

Boosting 的思路完全不同,它是让后面的模型专门去修正前面的错误。AdaBoost 通过提高被分错样本的权重来聚焦难样本;GBDT 则是在函数空间里做梯度下降,每一轮拟合的是上一轮预测的负梯度,也就是残差方向。XGBoost 和 LightGBM 是在 GBDT 基础上的工程优化,前者加了二阶导数和正则项,后者用了直方图分箱和叶子生长策略,训练速度提升明显。

Stacking 是另一种玩法,用多个基模型的第一层输出作为第二层模型的输入,相当于让一个元学习器去学习"什么时候该信哪个基模型"。它的效果通常不错,但代价是训练成本成倍增加,而且必须严格用交叉验证生成第一层的输出,否则会严重泄漏标签。我在大作业里试过一次 Stacking,因为图省事直接用全量数据训练基模型再预测,验证集上的分数高得离谱,换成五折交叉验证生成预测之后再评估,分数立刻掉回正常水平,这也是后面会专门讲的坑。

3. 模型评估这块最容易丢分:几个我自己踩过的判断误区

3.1 准确率高不等于模型好:不平衡数据下的假象

假设一个二分类数据集,正样本占 1%,负样本占 99%。一个把所有样本都预测成负类的"傻瓜模型",准确率是 99%,但它一个正样本都找不出来。这就是准确率悖论。数据挖掘的期末考几乎每年都会拿这个例子出题。

正确的做法是看混淆矩阵的四个格子:真正例、假正例、真负例、假负例。围绕这四个数衍生出一整套指标:精确率是预测为正的样本里真正为正的比例,召回率是真实正样本里被找出来的比例,F1 是两者的调和平均,用来在两者之间找平衡。什么时候更看重精确率?比如垃圾邮件拦截,误拦一封正常邮件代价很高。什么时候更看重召回率?比如疾病初筛,宁可误报也不能漏诊。

多分类场景下还要区分宏平均和微平均。宏平均是先把每个类别的指标算出来再算术平均,每个类别权重相同,适合关心小类别表现的情况;微平均是把所有类别的 TP、FP、FN 各自加总再算,实际上受样本量大的类别主导。加权平均介于两者之间,按类别样本量加权。这三个概念很容易混,我在模拟卷上就写反过一次,后来记的方法是:宏平均是"民主投票",微平均是"按人头统计"。

3.2 ROC-AUC 高但在业务上没法用的情况

ROC 曲线的横轴是假正例率,纵轴是真正例率,AUC 是曲线下的面积,取值 0.5 到 1。它有个很好的性质:对类别分布不敏感,所以经常被当作不平衡数据下的首选指标。但"不敏感"恰恰是它的问题。

在极端不平衡的场景下,假正例率的分母是负样本总数,这个数非常大,所以即使模型把大量负样本误判为正样本,FPR 的变化也不明显,ROC 曲线看起来依然漂亮。但对业务来说,这些误判是实打实的成本。比如一个广告点击率模型,负样本以亿计,模型多误判十万个负样本,在 ROC 上几乎看不出来,但在实际投放里就是十万次无效曝光。这时候应该看 PR 曲线(精确率-召回率曲线),它的基准线是正样本占比,对不平衡更加敏感。

还有一个更隐蔽的问题:AUC 是一个全局排序指标,它衡量的是随机取一个正样本和一个负样本,模型给正样本打分更高的概率。但业务往往只关心头部,比如只推前 1000 个用户。这时候 AUC 高不代表前 1000 个里正样本多,应该直接看 Top-K 的命中率,或者用 NDCG 这类排序指标。我当时在做课程项目时,两个模型 AUC 分别是 0.82 和 0.80,但 Top 100 的转化率差了将近一倍,最后选了 AUC 低的那个。这件事让我明白,指标永远要跟着业务目标走。

3.3 交叉验证里的数据泄漏,防不胜防

数据泄漏指的是训练阶段用到了预测阶段拿不到的信息,导致验证分数虚高,上线之后表现断崖式下跌。这件事听起来很初级,但实际操作中极其容易踩。

第一种是预处理顺序错误。很多人习惯拿到数据先把缺失值填了、把特征标准化了,然后再切分训练集和测试集。问题是填补用的均值、标准化用的均值和方差,都是从全量数据算出来的,测试集的信息已经透到训练过程里了。正确顺序是先切分,然后在训练集上 fit 转换参数,再用同样的参数去 transform 测试集,这也是 sklearn 里 Pipeline 存在的意义。

第二种是特征选择泄漏。用全量数据做卡方检验或者递归特征消除,选出和标签最相关的特征,再拿去做交叉验证,分数会明显偏高。正确的做法是把特征选择放进交叉验证的每一折里,用这一折的训练数据选特征。

第三种是时间序列的特殊性。时间序列不能随机切分,因为未来数据会泄漏到过去。正确做法是按时间顺序切分,比如用前 70% 训练、后 30% 测试,或者用滚动窗口的方式做时序交叉验证。我在做一份销量预测的实验时,随机五折交叉验证的 R² 有 0.9,改成时序切分之后掉到 0.6,那 0.3 的差距全是泄漏贡献的。

第四种是分组泄漏。如果数据里有同一个用户的多条记录,随机切分会让同一个用户同时出现在训练集和测试集里,模型只要记住这个用户的特征就能"预测"对,但换个新用户就废了。这时候要用 GroupKFold 按用户分组切分。

3.4 过拟合与欠拟合的定位流程

判断过拟合最直接的方法是看训练集和验证集的指标差距。训练集接近完美、验证集明显差,就是过拟合;两个都差,就是欠拟合。但光看差距还不够,还要看学习曲线:随着训练样本增加,如果训练分数一直很高而验证分数一直上不去,说明模型容量太大,需要加正则或者减复杂度;如果两条曲线收敛到一起但分数都很低,说明模型太简单,需要加特征或者换更强的模型。

解决过拟合的手段按成本从低到高排:增加数据量、加 L1/L2 正则、减少特征数量或做降维、降低模型复杂度、加早停、加 Dropout(神经网络)。L1 正则会让部分权重归零,天然带特征选择效果;L2 正则让权重整体变小,更平滑。这两者的区别也是考点,我的记法是 L1 产生稀疏解,像用刀切掉不重要的特征,L2 是均匀缩小,像把整体音量调低。

欠拟合相对好处理,但也不能盲目加复杂度。有时候欠拟合是因为特征工程没做好,比如把本该是连续值的年龄分了箱,信息量损失严重;有时候是因为数据本身噪声太大,标签质量差,这时候再复杂的模型也学不到东西。我在课程实验里遇到过一份标注数据,同样的问题有 30% 的样本标注不一致,再怎么调参,验证集准确率都卡在 75% 上不去,最后发现问题出在标注环节,而不是模型。

4. 大作业实战:GEO 数据挖掘从下载到差异分析的全流程复盘

4.1 为什么把课程大作业押在 GEO 上

课程大作业要求找一个真实数据集,走一遍完整的数据挖掘流程。大部分同学选了公开的房价数据、电商订单数据或者泰坦尼克号,这些数据洗干净了直接用,难度主要在建模。我想做点不一样的,就把方向定在了基因表达数据上,选的是 GEO 数据库。

GEO 全称 Gene Expression Omnibus,是一个公共的基因表达数据仓库,里面每个数据集以 GSE 编号标识,比如 GSE 后面跟一串数字。它的好处是数据真实、维度高(通常是两万多个基因作为特征,几十到几百个样本)、而且自带明显的分组信息,非常适合拿来做分类、聚类和差异分析的练习。它的坏处也很明显:格式不统一、注释缺失、样本量小、批次效应严重,几乎把数据挖掘里所有的坑都集中到一份数据里了。

课程要求里其实没限定物种和疾病,我选了一个人类疾病对比正常对照的数据集,每组样本数在 5 到 10 之间。选数据集的时候我给自己定了三条硬标准:一是分组必须明确写清哪些样本是疾病组、哪些是对照组;二是必须基于同一个平台,因为不同平台的探针无法直接比较;三是每组样本数不少于 3,否则统计检验的稳定性没法保证。这三条看起来简单,但真到筛选的时候筛掉了大半候选数据集,很多 GSE 只有病例没有对照,或者样本信息藏在补充文件里根本没有整理好。

4.2 数据下载与分组表整理:GSE、GPL、GSM 三者的关系

GEO 的层级结构是初学最容易糊涂的地方,搞清楚之后后面就顺了。GSE 是一个完整的实验系列,相当于一个项目;GSM 是单个样本,一个 GSE 下面挂几十个 GSM;GPL 是检测平台,决定了用的是什么芯片、包含哪些探针。同一个 GSE 理论上可以跨多个 GPL,但为了后续分析方便,最好选只有一个 GPL 的数据集。

下载方式有两种。一种是用 R 里的 GEOquery 包,一行代码就能把表达矩阵和样本信息全部拉下来:

library(GEOquery) gset <- getGEO("GSE12345", destdir = ".", getGPL = TRUE, AnnotGPL = TRUE) expr_matrix <- exprs(gset[[1]]) # 表达矩阵,行是探针,列是样本 pheno <- pData(gset[[1]]) # 样本信息表

另一种是手动下载。在 GEO 页面的 "Series Matrix File" 处可以下到GSE12345_series_matrix.txt.gz,里面是表达矩阵加样本注释;"SOFT formatted family file" 下到的是GSE12345_family.soft.gz,信息更全,包括平台注释。手动下载的好处是你可以先打开文件看看表头,确认数据长什么样,不容易出意外。

拿到数据后第一件事是检查维度、取值范围和样本名:

dim(expr_matrix) range(expr_matrix, na.rm = TRUE) colnames(expr_matrix)

如果最大值只有十几,说明数据已经做过 log 转换;如果最大值上千甚至上万,大概率是原始荧光强度值,需要自己转 log2。这个判断如果搞错,后面所有分析都是错的。

分组表的整理是最考验耐心的一步。样本的分组信息通常散落在 pheno 表的title、source_name_ch1、characteristics_ch1这几列里,格式五花八门,有的写 "disease" / "normal",有的写 "tumor tissue" / "adjacent normal tissue",有的干脆用编号。我的做法是先人工看一遍这些列的取值分布,用table()统计,然后手动构造一个和样本顺序严格对应的分组向量:

group_list <- c(rep("Control", 8), rep("Case", 8)) # 关键:分组顺序必须和 colnames(expr_matrix) 完全一致 identical(names(group_list), colnames(expr_matrix))

最后那句检查我强烈建议每个人都加上。我在第一次做的时候,分组写反了顺序(先写了 Case 后写 Control),结果差异分析出来的上下调基因全反了,看图的时候觉得不对劲,回头查才发现是这里错了,白折腾了一晚上。

4.3 预处理与质控:log2 转换、标准化、箱线图与 PCA

预处理的核心目标是让不同样本之间的表达值可比。芯片数据常见的问题是样本间的中位数水平不一致,如果不处理,差异分析会把技术差异当成生物学差异。

第一步是 log2 转换。如果原始数据是未取对数的强度值,用log2(x + 1)转换,加 1 是为了避免 0 值取对数变成负无穷。判断标准前面说过,看最大值和分布形态。这里要注意,有些数据集在提交时已经做过 log 转换了,你再转一次就变成了"对数的对数",数据的可比性会被彻底破坏,画出来的箱线图看着还行,但差异分析的结果会非常离谱。

第二步是标准化。芯片数据常用分位数标准化,让所有样本的表达值分布对齐:

library(limma) expr_norm <- normalizeBetweenArrays(expr_matrix_log, method = "quantile")

第三步是质控,这是最容易被跳过、但最能救命的一步。我一般看三个东西:

  • 箱线图:每个样本一条箱子,看中位数是否齐平、分布范围是否接近。如果某个样本的箱子明显偏离,要么是标准化没做好,要么是这个样本质量有问题。
  • 密度图:看各样本的表达值分布曲线是否重叠良好,理想情况是几乎所有曲线挤成一束。
  • PCA 散点图:把样本投影到前两个主成分上,看分组是否自然分开,有没有样本跑到对面阵营里。跑到对面的那个样本,很可能是分组标错了,或者是批次效应的受害者。

我在质控阶段就抓出过一个样本,PCA 上它离本组其他样本特别远,回去查原始信息发现它的实验日期比其他样本晚了一年,属于典型的批次效应。处理方式有两种:一是直接剔除,代价是损失一个样本;二是用 ComBat 这类方法做批次校正,但如果批次和分组完全混杂(比如病例组全在批次一、对照组全在批次二),校正也救不回来,因为生物学差异和技术差异已经分不开了。

还有一个容易被忽略的点:探针过滤。有些探针在所有样本里表达值都很低,接近背景噪声,这些探针留着只会增加多重检验的负担。常见做法是要求探针在至少一半样本中表达值大于某个阈值,或者直接过滤掉表达值方差接近 0 的探针。

4.4 探针注释与差异分析:limma 的完整调用链

表达矩阵的行是探针编号,人是看不懂的,必须注释成基因名。注释来源是平台文件,也就是 GPL。如果用的是 GEOquery 的AnnotGPL = TRUE,fData 里会直接带基因符号;如果没有,就需要用对应的注释包,比如人类常见芯片有对应的数据库包,通过探针 ID 查询基因符号。

注释过程有两个坑。第一是多对一:一个基因可能对应多个探针,而一个探针也可能对应多个基因。处理方式通常是取表达值最大的那个探针代表该基因,或者对同一基因的多个探针取平均。两种方法都有争议,但在课程项目里选一种并说明理由就够了。第二是丢失:注释之后往往有 20% 到 40% 的探针找不到对应的基因符号,这部分直接去掉,不用心疼,因为它们本来也解释不了。

差异分析我用的是 limma,它在样本量小的情况下表现稳定,是芯片数据的事实标准。完整调用链是这样的:

library(limma) # 1. 构建设计矩阵 group_list <- factor(group_list, levels = c("Control", "Case")) design <- model.matrix(~ 0 + group_list) colnames(design) <- levels(group_list) # 2. 构建对比矩阵 contrast_matrix <- makeContrasts(Case - Control, levels = design) # 3. 线性拟合与经验贝叶斯 fit <- lmFit(expr_gene, design) fit2 <- contrasts.fit(fit, contrast_matrix) fit2 <- eBayes(fit2) # 4. 提取结果 all_diff <- topTable(fit2, coef = 1, n = Inf, adjust.method = "BH")

这里最关键的是设计矩阵和对比矩阵的方向。Case - Control表示 Case 组相对于 Control 组的变化,logFC 为正表示在病例组中上调。如果方向写反,所有结论都会反过来。经验贝叶斯这一步是 limma 的精髓,它在样本量小的时候通过借用所有基因的方差信息来稳定每个基因的方差估计,相当于做了个"方差收缩",比直接用 t 检验稳健得多。

阈值选择上,最常见的是|logFC| > 1 且 adj.P.Val < 0.05。这里必须用调整后的 P 值,因为一次做了两万多次检验,不做多重检验校正的话,按 0.05 的显著性水平,光靠随机就能得到一千多个"显著"基因。limma 默认用 BH 方法控制错误发现率,这是目前最主流的做法。logFC 的阈值不要盲目写 1,如果数据整体变化幅度小,1 可能把所有基因都筛掉了,可以先看 logFC 的分布再定,或者用 0.585(对应 1.5 倍变化)这种更宽松的值做敏感性分析。

4.5 结果可视化与富集分析:火山图、热图、GO/KEGG 的坑

拿到差异结果之后,可视化是让结论站住脚的关键。火山图横轴是 logFC,纵轴是负对数 P 值,右上角和左上角的点就是显著上调下调的基因。画图的时候要注意纵轴要用-log10(adj.P.Val)而不是原始 P 值,颜色分类要手动指定阈值,并且把阈值线画出来,让人一眼能看出筛选标准。

热图用的是差异基因的表达值矩阵,关键参数是scale = "row",也就是按基因做标准化。这一点特别重要,因为不同基因的绝对表达值差异极大,不标准化的话,热图呈现的颜色差异主要来自基因本身的表达量高低,而不是样本之间的差异。聚类方面,行聚类看基因模块,列聚类看样本分组,如果列聚类的结果和实际分组高度吻合,说明差异基因确实有区分能力,这是可以在报告里写的证据。

富集分析是把基因列表翻译成生物学意义的过程,常用的工具是 clusterProfiler,做 GO 和 KEGG 两条线:

library(clusterProfiler) library(org.Hs.eg.db) gene_ids <- bitr(gene_symbols, fromType = "SYMBOL", toType = "ENTREZID", OrgDb = org.Hs.eg.db) ego <- enrichGO(gene = gene_ids$ENTREZID, OrgDb = org.Hs.eg.db, ont = "BP", pAdjustMethod = "BH", pvalueCutoff = 0.05, qvalueCutoff = 0.05, readable = TRUE) ekk <- enrichKEGG(gene = gene_ids$ENTREZID, organism = "hsa", pvalueCutoff = 0.05)

这里的坑有三处:一是物种对应的数据库不能错,人类是 org.Hs.eg.db,小鼠是 org.Mm.eg.db,搞错会直接报错或者返回空结果;二是基因 ID 类型必须匹配,KEGG 要 Entrez ID,直接扔基因符号进去会全军覆没;三是富集分析的输入最好用显著差异基因,但也不能太少,少于 50 个基因做出来基本没有统计功效,这时候可以考虑放宽阈值,或者改用 GSEA——GSEA 不需要设阈值,它用的是全部基因的排序信息,在小样本场景下往往比超几何检验更靠谱。

我在这一步踩过的坑是把上调基因和下调基因混在一起做了富集,结果什么通路都不显著。分开做之后,上调基因里的炎症相关通路一下就跑出来了,和我预期的疾病机制完全对得上。所以富集分析一定要按方向分别做,不要图省事合并处理。

5. 复习节奏、考场技巧和如果重来一次我会改的地方

5.1 三周复习时间怎么切

我的复习时间大概是三周,分配方式是:第一周把课件和教材过一遍,做一份自己的知识点索引;第二周集中做历年真题和课后习题,重点攻手算题;第三周回到大作业和代码,把手写推导和实际实现对齐。

第一周的目标不是记住,而是建立索引。我的做法是每看完一章,就用一张 A4 纸画一张图,中间写算法名字,四周写输入、输出、关键公式、优缺点、适用场景。画图的过程比抄笔记慢,但一周下来,六个主要算法(决策树、KNN、朴素贝叶斯、SVM、K-Means、Apriori)的轮廓就非常清楚了。这个方法的额外好处是,考试遇到"比较两个算法"的题,你脑子里的那张图直接就能转成答案。

第二周重点是手算。这门课的手算题主要有四类:算信息增益选分裂特征、算支持度和置信度、手写 K-Means 的一次迭代、算混淆矩阵的各项指标。这几类的共同点是步骤固定、容易算错,唯一的办法就是重复。我当时把课后题里的同类题集中做了三遍,第三遍开始基本不会错了。特别提醒一句,算信息增益的时候对数底数要统一,题目里如果没写,一般默认以 2 为底,单位是比特;熵的加权是按子集样本数占总数的比例加权,不是简单平均,这个错我犯过好几次。

第三周我主要用来看代码。数据挖掘的代码题通常不难,但细节多,比如 sklearn 里fit和transform的调用顺序、train_test_split的stratify参数、交叉验证的cross_val_score怎么和 Pipeline 配合。我习惯把这些常用片段整理成一个速查文件,考试前扫一遍,心里就有底了。

5.2 手算题与代码题的准备差异

这两类题的准备方式完全不同,混在一起复习效率很低。手算题靠的是熟练度和细心,需要反复练到形成肌肉记忆;代码题靠的是对 API 的理解和对数据流的把握,需要动手跑通而不是背代码。

手算题的训练重点是"慢就是快"。我一开始为了赶进度,算 K-Means 迭代的时候跳过中间步骤,结果总是出现某个点归属错误,回头查半天才发现是距离开方算错了。后来我强迫自己每一步都写出来:先算每个点到各中心的欧氏距离,再比较大小确定归属,然后按簇求均值更新中心,最后判断中心是否变化。写全之后正确率上来了,速度反而更快。

代码题的重点是理解每个函数在做什么。我见过同学能默写出cross_val_score(model, X, y, cv=5)这一行,但被问到"为什么用 5 折而不是 10 折"就答不上来。这类问题的答案通常是:折数越多,每折的训练数据越多,评估越稳定,但计算成本线性增长;同时折数不能超过每个类别的最小样本数,否则分层抽样会报错。数据量小的时候用留一法(LOOCV)或者 10 折,数据量大、训练成本高的时候用 5 折甚至 3 折,这是工程上的权衡。

5.3 我在大作业里踩过的四个具体坑

回顾整个大作业,我有四个坑印象最深,每个都花了至少半天才爬出来。

第一个是分组顺序错位,前面提过。教训是:任何涉及两组数据对齐的地方,写完立刻用identical()或者all.equal()验证一遍,不要相信自己的记忆。

第二个是数据已经 log 过又转了一次。当时我只看最大值判断,那个数据集最大值是 16,我以为没转过,又做了一次 log2,最大值变成 4,整体分布被压缩,差异分析出来的 logFC 全都很小,几乎没有显著基因。后来去 GEO 页面的原始说明里看到了 "values are log2 transformed",才知道自己判断错了。教训是:不要只看数值范围猜,去数据集页面的说明文档里找明确描述,实在找不到,就看数据的整体分布是不是接近正态、最小值是不是接近 0。

第三个是探针注释丢了一半基因。我第一次注释之后,两万多个探针只剩六千多个基因,感觉不对,检查发现注释包用错了版本。人类芯片平台有很多版本,探针设计不同,注释包必须和 GPL 编号严格对应。教训是:拿不准的时候,直接去 GEO 页面下载 SOFT 文件,里面自带平台注释表,用它做映射最稳妥。

第四个是富集分析结果为空。查了半天,原因是传给 enrichKEGG 的是基因符号而不是 Entrez ID,而且我没有检查 bitr 的转换率。函数没报错,只是返回了一个空结果,这种"静默失败"最坑。教训是:任何转换步骤之后都要检查丢了多少钱,bitr会给出转换失败的比例,这个数字如果超过 30%,说明 ID 类型或者物种选错了。

5.4 考场上的答题顺序经验

最后说点考场上的事。数据挖掘这门课的卷子通常分三块:概念简答、计算分析、综合应用。我的答题顺序是先把综合应用题扫一遍,在草稿纸上写几个关键词,然后从概念题开始做。

这么做的原因有两个。一是综合题往往需要调用后面的知识点,提前扫一遍能给大脑一个"预热"的机会,等做到它的时候思路已经清晰了。二是概念题分值稳定、时间可控,先把它们做完,心里有底,不会因为后面大题卡住而慌乱。

计算题一定要留足时间并且写出中间步骤。评分标准通常是按步骤给分,即使最后答案错了,中间过程对也能拿大部分分。我有个习惯是把关键公式和每一步的结果都标上序号,检查的时候一眼就能看出哪一步可能出问题。

如果遇到完全没头绪的题,不要空着。数据挖掘的题目大多有套路,实在不会算,就把相关的定义、公式和思路写上去,比如问某个算法的优缺点,至少能写出它的核心机制和适用场景。我考场上就遇到过一道集成学习的题,具体数值算不出来,但把 Bagging 和 Boosting 的差异、偏差方差的角度写清楚之后,也拿到了不错的分数。

回头再看这半年,我觉得数据挖掘最有价值的不是记住了多少算法,而是养成了一个习惯:拿到任何一份数据,先问它干不干净、够不够代表真实情况;做出任何结论,先问自己的评估方式有没有漏洞。这个习惯在大作业的 GEO 分析里救了我好几次,在以后任何涉及数据的场景里,大概也一样管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询