每次跑完影像分割和特征提取手里握着一大张特征表——上千个数值型变量、患者数量却只有几十到一两百——这时候最头疼的不是特征少而是特征太多。太多特征意味着模型几乎可以在训练集上“背”出所有答案换一批数据立刻露馅。这就是影像组学里最典型的困境特征维度远大于样本量。这篇内容想解决的就是把“从N个特征到十几个真正可靠特征”的整条筛选链路理清楚并给出一套可以直接照着跑的 Python 代码。从数据清洗、稳定性评估、相关性去冗余、单变量检验到 LASSO、随机森林重要性排序再到防止筛选过程本身导致过拟合的嵌套交叉验证。适合刚接触影像组学、想把流程跑通的研究生和临床医生也适合有图像分割经验但不太清楚特征筛选细节的开发者。1. 筛选框架怎么定先把“为什么分层筛选”想清楚1.1 特征越多模型越容易“作弊”先建立一个非常直观的认知特征数量远超样本量时任何一个分类器都可以找到一组“看起来有效”的规则去拟合训练集。举个数字上的例子60 个样本、800 个特征如果没有做任何特征筛选线性模型的自由度都远高于样本量这时候训练集上的 AUC 可能冲到 0.95 以上但这不是模型学会了规律而是模型记住了每个样本的编号。业内通常把这类问题叫作“维数灾难”而维数灾难的直接后果就是验证集上的性能断崖式下跌。影像组学特征还有一个特殊之处很多特征之间高度相关。灰度共生矩阵里的一阶统计量、灰度游程矩阵里的游程长度不均匀性本质上都在描述“图像纹理的某种粗糙度”去掉其中一个信息量损失很少。如果不管这层冗余机器选择的特征集会随样本扰动剧烈变化今天选出来的是 A 和 B明天加三个样本可能变成 C 和 D这样即使 AUC 稳定特征本身也不可信。1.2 分步筛选先保证质量再谈预测力我以前也走过弯路一开始图省事把所有特征直接塞进 LassoCV让它自动挑。结果发现第一Lasso 对高度相关的特征组选择很不稳定哪一组进来纯看噪声方向第二特征里如果有极端离群值Lasso 的系数路径会被严重干扰第三单靠 Lasso 选出的特征缺少“稳定性保证”换一次交叉验证折次选中的特征就变了。后来我把流程拆成了四步每一步解决一类问题筛选层次目标常用方法数据清洗去掉缺失过多、方差为 0、极端离群的特征缺失比例、方差阈值、Z-score 截断可靠性筛选留下重复测量中稳定的特征ICC 组内相关系数冗余性筛选去掉两两高度相关的特征Spearman 相关矩阵、平均相关排序预测力筛选选出真正有区分能力的特征组合Mann-Whitney U、LASSO、随机森林重要性这个顺序是有讲究的。如果不先做数据清洗就把特征丢进相关矩阵一个离群值就能让若干特征的相关系数全部失真如果不去冗余就做 LASSO选出来的特征名单稳定性会很差。等于说前面每一步都是在为后面一步“打扫战场”。2. 第一关数据清洗、标准化以及新手最容易忽略的泄漏2.1 读入特征表并处理缺失和零方差特征表的常见格式是一行一个样本、一列一个特征。我一般会在读入之后先做三件事查看缺失比例、删除缺失超过 20% 的列、删除所有样本取值完全相同的列。影像组学特征里方差为 0 的情况不算罕见特别是某些对图像灰度分布极其敏感的三阶矩特征在归一化预处理不好的数据里可能一整批都是 0。import pandas as pd import numpy as np df pd.read_csv(radiomics_features.csv, index_col0) # df 的每一行是一个样本每一列是一个特征 # 1. 查看缺失比例 missing_ratio df.isnull().mean() # 2. 删除缺失超过 20% 的特征 df df.loc[:, missing_ratio 0.2] # 3. 删除方差为 0 的常量特征 constant_cols df.columns[df.nunique(dropnaFalse) 1] df df.drop(columnsconstant_cols) # 4. 仍存在的少量缺失用该列中位数填补 df df.fillna(df.median())关于缺失值处理有一个容易犯的错直接dropna()会把整行样本删掉样本量本来就少这样操作等于自断手臂。用中位数填充是一种保守且稳妥的策略不会让某列特征的分布产生明显偏移也避免了均值填充被离群值带偏的问题。2.2 极端离群值要不要剔、怎么剔影像组学特征值往往比自然语言领域的特征更“野蛮”。同一个特征在不同患者之间可以差三个数量级偶尔还会出现一个样本比其他样本大几十倍的情况这种样本对后续相关矩阵和标准化都是巨大的干扰源。我常用的方式是先把特征做一次 Z-score 变换绝对值超过 4 的当作可疑离群值把对应值截断到 4而不是直接删除整行。from scipy.stats import zscore # 对每个特征计算 Z-score超出阈值的值压缩到边界 def cap_outliers(X, threshold4.0): X X.copy() z np.abs(zscore(X, axis0, nan_policyomit)) mask z threshold # 这里用边界值替换而不是删样本 for col in X.columns: col_z zscore(X[col], nan_policyomit) upper X[col].median() threshold * X[col].std() lower X[col].median() - threshold * X[col].std() X.loc[col_z threshold, col] upper X.loc[col_z -threshold, col] lower return X df cap_outliers(df)为什么不直接删除这些样本因为样本量太珍贵。60 个病例每剔除一个离群值都意味着损失完整信息。截断能在保留样本的同时削弱极端值的杠杆作用在临床上这也说得通特征极端到超出物理意义范围时大概率是分割边界出问题、灰度归一化异常等外部因素造成的而不是待研究病灶本身真的有这么极端。2.3 标准化必须放进交叉验证内部这一节的内容是整个流程里最重要的一段因为不说清楚后面代码全都白搭。特征筛选开始之前通常要先标准化很多人习惯先在整份数据上做StandardScaler().fit_transform()然后再去筛选、建模。这个动作在预测任务的流程里属于严重错误。想象一下你现在用所有样本算出了均值和标准差然后对全量数据做标准化再用交叉验证评估分类器。交叉验证的测试集样本在标准化时已经“见”过训练集的信息了因为训练集的均值和标准差进入过全局统计量。这样得到的 AUC 是偏高的——它不是模型在未知数据上的真实表现。正确的做法是把标准化当作模型训练的一部分。每一折交叉验证里只用训练折的数据计算均值和标准差再应用到验证折。后面给出的代码会严格遵循这个原则。这个细节在特征筛选场景里影响更大因为筛选过程本身涉及大量基于全局统计量的步骤稍不留神就会让下游验证结果虚高。3. 第二关稳定性与相关性把“不可靠”和“重复”的特征请出去3.1 用 ICC 评估重复测量的稳定性影像组学特征提取有一个不稳定的来源同一个患者的影像换了 ROI 勾画、换了图像重采样参数提取出的特征值会略有变化。如果某个特征在两次重复勾画中数值剧烈波动说明它本质上是噪声不该进入后续筛选。ICC组内相关系数衡量的是同一批对象多次重复测量的一致性。范围在 0 到 1 之间越接近 1 表示测量越稳定。实际工作中常见的做法是对一部分样本做两次 ROI 勾画并提取特征然后计算每个特征的 ICC保留 0.75 以上的特征。如果你手头没有重复测量数据至少也要在原始数据中想办法模拟——比如对同一原始图像做两次不同重采样再提取特征看哪些特征不受重采样参数影响。# 方案一使用 pingouin 一行计算出每个特征的 ICC # pip install pingouin import pandas as pd # long_df 需要包含三列subject、rater、value # subject 是患者编号rater 是重复测量的次数编号value 是对应特征值 def calc_icc(long_df): import pingouin as pg icc pg.intraclass_corr( datalong_df, targetssubject, ratersrater, ratingsvalue ) return icc.loc[icc[Type] ICC2, ICC].values[0] # 对每个特征重复上面的调用 stable_features [] for feat in feature_list: tmp feature_long_df[[subject, rater, feat]].rename(columns{feat: value}) icc_val calc_icc(tmp) if icc_val 0.75: stable_features.append(feat)提示ICC 的完整计算有多个变体。影像组学里最常用的是 ICC(2,1)绝对一致性上述 Pingouin 返回的 ICC2 对应的就是这个类型。如果不想引入额外依赖也可以用单因素方差分析的组间均方和组内均方手写一个简化版但 Pingouin 经过了严格测试更推荐直接用。这里有一个实战提示ICC 筛选的阈值不要设得太高。早期我把阈值定成 0.9结果大批特征被淘汰剩下的特征组在后续 LASSO 里选不出足够的预测因子。后来在多个数据上都验证过0.75 到 0.85 之间是合理的带既过滤了不稳定特征又保留了足够的信息量。3.2 相关性去冗余谁说特征之间是独立的先做 ICC再做相关矩阵。原因很直接一个特征如果自身测量都不稳定那它和别的特征即使相关系数很高这层关系也可能只是巧合。先剔除不稳定的再删除冗余的逻辑上才站得住脚。相关系数的计算建议用 Spearman 而非 Pearson。影像组学特征的分布大多数不是正态的少数极端值就会扭曲 Pearson 相关系数。Spearman 基于秩次对单调变换不敏感更适合这种偏态分布的数据。# 输入 X 已经完成数据清洗和 ICC 筛选 def remove_redundant_features(X, corr_threshold0.85): corr_mat X.corr(methodspearman) # 记录每个特征与其他特征的平均绝对相关系数 avg_abs_corr corr_mat.abs().mean(axis1) # 找出所有相关度超过阈值的特征对 upper_tri corr_mat.where( np.triu(np.ones(corr_mat.shape), k1).astype(bool) ) to_drop set() for col in upper_tri.columns: if col in to_drop: continue high_corr upper_tri.index[upper_tri[col] corr_threshold] for partner in high_corr: if partner not in to_drop: # 保留平均绝对相关更小的特征 if avg_abs_corr[col] avg_abs_corr[partner]: to_drop.add(partner) else: to_drop.add(col) X_red X.drop(columnslist(to_drop)) return X_red, list(to_drop) X_red, dropped remove_redundant_features(df)这段代码的逻辑是两个特征相关系数超过 0.85 时谁是“更冗余”的那个就看谁跟所有其他特征的平均相关系数更大。平均相关更大的特征在信息上更容易被其他特征覆盖先删它损失最小。这里说一下为什么阈值设在 0.85 而不是 0.9。影像组学特征之间的相关性不是简单的“二选一”而是一组特征彼此纠缠。阈值越高留下的冗余特征越多下游 LASSO 的系数稳定性就越差。我做过对比阈值从 0.9 调到 0.8 时特征数量下降 30% 左右但最终模型的验证 AUC 几乎没有损失甚至在小样本上更稳。所以保守一点取 0.85 是比较折中的选择。4. 第三关单变量检验、LASSO 与随机森林的重要性排序4.1 单变量检验快速给每个特征定个“初筛成绩”经历了数据清洗和去冗余特征数量一般会从 800 左右降到 200 到 300。这一阶段的任务是找出“与目标标签有关的特征”。对二分类结局最常用的单变量检验是 Mann-Whitney U 检验它不要求特征服从正态分布对离群值也相对稳健。from scipy.stats import mannwhitneyu X X_red y labels # 0/1 二分类标签 p_values [] for col in X.columns: group0 X.loc[y 0, col] group1 X.loc[y 1, col] stat, p mannwhitneyu(group0, group1, alternativetwo-sided) p_values.append(p) p_series pd.Series(p_values, indexX.columns)这里有一个关键操作多重检验校正。同时对两百个特征做假设检验光靠运气就可能出现十几个“显著”结果。必须做 Benjamini-Hochberg FDR 校正然后把 q 值小于 0.05 的特征留下。如果 q 值偏大可以把阈值放宽到 0.1甚至不做硬性截断只把它作为后续 LASSO 的候选排序依据。实际项目中我通常不会完全依赖 p 值门槛而是用它淘汰掉明显不可能有区分度的特征比如 q 值大于 0.5 的那些。# 手动实现 BH-FDR避免为了一个步骤多装一套库 def bh_fdr(pvals): pvals np.asarray(pvals) n len(pvals) order np.argsort(pvals) ranked_p pvals[order] adjusted np.zeros(n) cumulative 1.0 for i in range(n - 1, -1, -1): cumulative min(cumulative, ranked_p[i] * n / (i 1)) adjusted[i] cumulative # 确保单调性 # ... return pd.Series(adjusted, indexpvals.index) q_series bh_fdr(p_series) passed_p q_series[q_series 0.05].index.tolist()4.2 LASSO 选特征不只是给个系数单变量检验只衡量每个特征单独与标签的关系完全不在乎特征之间的关系。这就需要一个多变量方法补位。影像组学里最经典的是 LASSO也就是加了 L1 正则化的线性模型。L1 正则化会把不重要的系数压成 0留下来的特征就是模型认为有独立贡献的特征。我用的是带有交叉验证调参的 L1 逻辑回归而不是 LassoCV 回归。很多影像组学任务最终是分类问题逻辑回归形式的系数能直接对应概率解释也更方便。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline # 请注意这里输入的 X 是已经做完单变量初筛后的特征 X_cand X[passed_p] # 在每一折交叉验证内部做标准化防止信息泄漏 pipe Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression( penaltyl1, solverliblinear, random_state42, max_iter1000 )) ]) param_grid {lr__C: np.logspace(-3, 1, 50)} search GridSearchCV( pipe, param_gridparam_grid, cv5, scoringroc_auc, n_jobs-1 ) search.fit(X_cand, y) best_C search.best_params_[lr__C]筛选标准很直接取最优 C 下系数不为 0 的特征。lasso_feats X_cand.columns[ np.abs(search.best_estimator_.named_steps[lr].coef_[0]) 0 ]有一点要提醒LASSO 在小样本上的选择很不稳定换一组折就可能换一组特征。所以现在主流的做法不是跑一次 LASSO 就罢休而是重复跑多次比如重复 100 次并每次都做随机交叉验证最后统计每个特征被选中的频率留下频率超过 0.8 的特征。这个思路有点像稳定性选择它比单次 LASSO 给出的特征名单可靠得多。4.3 随机森林重要性另一种角度做交叉验证LASSO 是线性模型只能捕捉线性的、加性的关系。它的“决策边界”是全体特征空间里的一个超平面。而随机森林能捕捉非线性交互比如“特征 A 大、特征 B 小”组合出现时才有意义的情况。用随机森林的特征重要性做一次平行筛选再和 LASSO 取交集能留下更稳的特征。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators1000, random_state42, n_jobs-1 ) rf.fit(X_cand, y) importance pd.Series(rf.feature_importances_, indexX_cand.columns) # 保留重要性排在前 K 的特征 # K 的取值通常和样本量相关我一般取 min(30, n_samples // 5) 左右 K min(30, len(X_cand) // 5) rf_feats importance.sort_values(ascendingFalse).head(K).index.tolist()随机森林重要性有一个坑当两个特征高度相关时重要性会在这两个特征之间分摊导致两个特征的重要性都偏低。这正是为什么在前一步已经做了相关性去冗余只有先去掉冗余特征随机森林的重要性排名才有可信度。最终这一轮筛出的特征就是set(lasso_feats) set(rf_feats)。这个交集操作等于把两条线都认可的特征看做强候选。如果你发现交集太小比如只剩两三个特征可以放宽条件改成并集或者把“LASSO 选中 且 RF 排名前 50”作为组合条件。4.4 为什么是“单变量 LASSO RF”而不是随便选一个单变量检验效率高计算简单但它对“协同效应”视而不见——一个单独 p 值不显著但和另一个特征组合后很有区分度的特征会被误杀。LASSO 能看协同但不擅长非线性交互。RF 能看非线性但特征重要性受冗余影响大。单独用任何一者都可能漏掉重要特征或留下不可靠特征。把三者组合起来本质上是让不同角度的证据互相纠偏。在实际项目里这套组合拳选出的特征通常都能在外部数据上保持稳定性能而单独使用 LASSO 时则经常出现换了一组数据就失效的情况。5. 验证环节嵌套交叉验证才能避免“筛选过拟合”5.1 先说说交叉验证为什么也会骗人很多人把特征筛选看作是模型训练之前的“预处理”所以在整个数据集上先筛选特征、再做交叉验证。这其实把验证过程污染了。特征筛选本身也是一种训练——它见过所有样本的标签它选择的特征天然是朝着“最大化全量样本可分性”的方向走。交叉验证评估的是“筛选后固定特征集上的模型”而不是“未来真实使用时的完整流程”。举个例子假设数据里有 300 个特征其中纯噪声特征完全和标签无关但因为样本量只有 60你总能在全量数据上挑出十几个“碰巧”有区分能力的特征。这时候用同一批数据交叉验证AUC 可能高达 0.9。换一批新病人来测试这十几个特征大概率会全部失效因为它们的区分能力建立在随机噪声上。如果不在每次验证时重新筛选就永远发现不了这个问题。5.2 嵌套循环把“筛选”放进每一折测试里要得到对“完整流程”的诚实评估必须做嵌套交叉验证外层循环负责划分训练/验证内层循环负责在每一折的训练数据上重新执行一遍“数据清洗 - 去冗余 - LASSO/RF 筛选 - 建模”的完整流水线。from sklearn.model_selection import KFold from sklearn.metrics import roc_auc_score outer_cv KFold(n_splits5, shuffleTrue, random_state42) aucs [] selected_feature_sets [] for fold, (tr_idx, va_idx) in enumerate(outer_cv.split(X_full, y)): X_train_full, X_val X_full.iloc[tr_idx], X_full.iloc[va_idx] y_train, y_val y.iloc[tr_idx], y.iloc[va_idx] # ---------- 在 X_train_full 内部重新走整个筛选流程 ---------- # 1. 缺失、方差、离群处理 # 2. ICC 筛选如果重复测量信息存在 # 3. 相关性去冗余 # 4. Mann-Whitney U FDR # 5. LASSO RF 取交集 # 得到 X_train_selected, X_val_selected # ---------- 使用筛选后的特征训练一个简单分类器 ---------- scaler StandardScaler().fit(X_train_selected) X_train_s scaler.transform(X_train_selected) X_val_s scaler.transform(X_val_selected) model LogisticRegression(Cbest_C_inside_fold) model.fit(X_train_s, y_train) auc roc_auc_score(y_val, model.predict_proba(X_val_s)[:, 1]) aucs.append(auc) print(fNested CV AUC: {np.mean(aucs):.3f} - {np.std(aucs):.3f})这段代码的核心理念是验证集永远不能参与筛选。上述第 1 到 5 步里的均值、标准差、相关系数、p 值、LASSO 系数全部只基于训练折计算。验证集只在最后一步“见”一次模型。嵌套交叉验证的代价是计算量变大——原来跑一遍筛选现在要跑 5 到 10 遍。但影像组学特征的样本量通常不大跑完整流程也就几十分钟。这个时间成本必须付因为只有这样才能得到不掺水的模型性能估计。5.3 一种更贴近真实临床的做法重复嵌套交叉验证单外层的 5 折嵌套交叉验证仍然受划分方式影响。我通常会把整个外层循环重复 20 次每次用不同的随机种子重新划分折然后把所有 AUC 汇总成一个分布。从分布可以看出流程稳定性到底如何。如果 20 次重复里的 AUC 中位数是 0.70、P25 到 P75 是 0.65 到 0.75那说明这个流程在不同样本划分下都稳定如果分布宽到 0.45 到 0.90那说明整个特征筛选对样本划分太敏感即使交叉验证 AUC 好看也不能对模型抱太高的期待。注意重新做特征筛选不等于在同一份验证集上反复调参。如果每次重复都保留外层验证集样本不动只重新划分训练折那验证集样本确实从未参与过筛选和训练评估结果是有意义的。6. 完整流程代码骨架与可直接调整的参数清单6.1 一张可复制的流程骨架把前文内容汇总成一个可执行骨架。实际使用时只需要把feature_csv_path和label_series替换成你自己的数据。里面省略了 ICC 部分因为重复测量数据并不是每个项目都有把 ICC 作为可选项更符合大多数情况。import pandas as pd import numpy as np from scipy.stats import zscore, mannwhitneyu from scipy.stats import spearmanr from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.model_selection import KFold, GridSearchCV from sklearn.metrics import roc_auc_score def bh_fdr(pvals): n len(pvals) order np.argsort(np.asarray(pvals)) ranked np.asarray(pvals)[order] adjusted np.zeros(n) prev 1.0 for i in range(n - 1, -1, -1): prev min(prev, ranked[i] * n / (i 1)) adjusted[i] prev result np.zeros(n) result[order] adjusted return result def feature_selection_pipeline(X, y, corr_threshold0.85, fdr_threshold0.05, lasso_freq_threshold0.8): # 1) 缺失和零方差处理 X X.loc[:, X.isnull().mean() 0.2] X X.loc[:, X.nunique(dropnaFalse) 1] X X.fillna(X.median()) # 2) 离群值截断 X cap_outliers(X, threshold4.0) # 3) 相关性去冗余 corr_mat X.corr(methodspearman) avg_abs_corr corr_mat.abs().mean(axis1) upper corr_mat.where(np.triu(np.ones(corr_mat.shape), k1).astype(bool)) to_drop set() for col in upper.columns: if col in to_drop: continue partners upper.index[upper[col] corr_threshold] for p in partners: if p not in to_drop: if avg_abs_corr[col] avg_abs_corr[p]: to_drop.add(p) else: to_drop.add(col) X X.drop(columnslist(to_drop)) # 4) 单变量检验 FDR pvals [] for col in X.columns: a, b X.loc[y 0, col], X.loc[y 1, col] st, p mannwhitneyu(a, b, alternativetwo-sided) pvals.append(p) qvals bh_fdr(np.array(pvals)) keep [col for col, q in zip(X.columns, qvals) if q fdr_threshold] X X[keep] # 5) 重复 LASSO 选特征 count_selected {col: 0 for col in X.columns} for seed in range(50): inner KFold(5, shuffleTrue, random_stateseed) for tr_idx, va_idx in inner.split(X): X_tr, y_tr X.iloc[tr_idx], y.iloc[tr_idx] pipe Pipeline([ (sc, StandardScaler()), (lr, LogisticRegression(penaltyl1, solverliblinear)) ]) grid GridSearchCV(pipe, {lr__C: np.logspace(-3, 1, 30)}, cv3, scoringroc_auc) grid.fit(X_tr, y_tr) coef grid.best_estimator_.named_steps[lr].coef_[0] for i, c in enumerate(coef): if abs(c) 0: count_selected[X.columns[i]] 1 total 50 * 5 lasso_feats [k for k, v in count_selected.items() if v / total lasso_freq_threshold] # 6) 随机森林重要性排序 rf RandomForestClassifier(n_estimators500, random_state42) rf.fit(X, y) imp pd.Series(rf.feature_importances_, indexX.columns) K min(30, len(X) // 5) rf_feats imp.sort_values(ascendingFalse).head(K).index.tolist() final_feats list(set(lasso_feats) set(rf_feats)) return final_feats这段骨架把前面几节的逻辑压缩成几个函数实际运行后返回一个特征名单。建议拿到名单之后不要直接拿去训练模型而是先把这个名单里的特征和临床意义对照一遍——有没有明显不符合病理生理特征的异常特征有没有特征本质上是同一个算子的不同参数版本这些人工检查虽然不能自动化但能挡住最明显的翻车。6.2 参数清单每一步该调什么、调多少参数默认值调整方向缺失值列删除阈值0.2样本少时放宽到 0.3但别超过 0.5离群值截断 Z-score 阈值4.0特征分布特别重尾时可降到 3.0ICC 保留阈值0.75特征太少了降到 0.7特征太多可提到 0.8Spearman 去冗余阈值0.85目标是保留 50~150 个特征时调整FDR q 值阈值0.05特征所剩无几时放宽到 0.1LASSO 重复次数50追求稳定可加到 100LASSO 保留频率0.8特征太少降到 0.7RF 排名 K 值min(30, n/5)根据最终特征数量灵活调整6.3 两个我踩过很多次才学到的实用提醒第一不要把 LASSO 的 C 值固定。有的教程喜欢直接用C1或C0.1但不同数据上最优 C 值差异很大。一个标准化后特征量级差不多的数据集最优 C 可能在 0.01另一个数据集可能跑到 1.0。用GridSearchCV搜索对数空间才是最稳的做法。第二筛选流程的内部要处理类别不平衡。如果两组样本比例不是接近 1:1最好在分类器里设置class_weightbalanced否则 LASSO 和 RF 的特征重要性会偏向样本量多的那一类。这个问题在不平衡数据上尤其隐蔽——特征单独看有区分度但模型会忽略少数类的高价值特征。最后的经验体会整套流程跑下来最重要的体会不是哪个算法多高明而是“每一步都要记得自己是为了什么才这么做”。数据清洗是为了不让离群值干扰后面的相关矩阵ICC 是为了不让测量噪声进入模型去冗余是为了让 LASSO 选出的特征有可解释意义嵌套交叉验证是为了让最后的 AUC 不掺水。这四件事单独拎出来都不难难的是每次都记得把它们串起来。再分享一个小技巧最终选定特征后我会额外跑一次仅用这些特征的简单逻辑回归把系数和方向整理成表。这样能回答“这个特征升高预测是向好还是向坏”的问题写论文、做临床沟通都方便得多。影像组学做得好不好也不完全看 AUC 有多高更看这组特征能不能被临床解释、能不能在下一批数据上复用——这才是筛选流程真正要交付的东西。