在做机器学习建模的时候,经常遇到一个让人头疼的问题:手上有几十甚至上百个特征,到底哪些该留、哪些该丢?
之前两篇介绍了通过 方差阈值 和 相关性分析 来过滤特征,今天这篇介绍一种新的过滤特征的方式,也就是用统计检验来判断一个特征到底跟目标变量有没有关系。
核心思路很简单
说白了,统计检验做特征选择的核心逻辑就一句话:
对每个特征单独做一次假设检验,看看它跟目标变量之间到底是"真有关系"还是"纯靠运气"。
统计检验的思路 :
- 原假设 :该特征与目标变量无关
- 计算 值 :如果 值很小(,通常 ),说明"特征与目标无关"这个假设大概率不成立→ 拒绝 → 保留该特征
- 值越大 → 越没有证据表明特征有用 → 剔除
就这么简单。
本质上就是给每个特征打一个"显著性分数",然后卡个阈值做筛选。
实现方式
实现这个基于统计显著性检验的特征选择器 ,采用了三层架构。
_CorrectionStrategy(校正策略):处理多重检验的 p 值校正_TestAdapter(检验方法适配器):统一不同统计检验的调用接口StatisticalTestSelector(核心选择器):对外暴露的 API
校正策略
对于校正策略部分,代码中封装了 3 种校正策略:
| 策略 | 类名 | 思路 |
|---|---|---|
| 不校正 | _NoCorrection | 直接用原始 p 值,不做任何处理 |
| Bonferroni | _BonferroniCorrection | 最保守:p值 × 检验次数,严格控制假阳性 |
| FDR (BH) | _BenjaminiHochbergCorrection | 折中方案:控制错误发现率,比 Bonferroni 更宽松 |
其中:
Bonferroni 校正(最简单):
adjusted_p = original_p × 检验总次数 m
比如检验 100 个特征,p=0.01 会被校正为 1.0(不再显著)。
Benjamini-Hochberg (FDR)(最常用):
步骤:
1. 把 p 值从小到大排序
2. 第 k 个 p 值的临界阈值 = (k / m) × α
3. 找到最大的 k,使得 p_(k) ≤ 阈值
4. 前 k 个特征全部保留
5. 反推校正后的 p 值(保证单调性)
检验方法适配器
其实 sklearn 库中提供了多种统计检验方法,但它们的接口不统一:
f_classif,chi2→ 返回(scores, pvalues)mutual_info_classif→ 只返回scores,没有p值
所以,这里封装了_TestAdapter, 统一了调用接口,所有方法都返回 (scores, pvalues) 。
| 方法 | 适用场景 | 说明 |
|---|---|---|
anova | 分类任务 | ANOVA F 检验,最常用 |
f_regression | 回归任务 | F 回归检验 |
chi2 | 分类任务 | 卡方检验,要求特征非负 |
mutual_info | 分类/回归 | 互信息,不返回 p 值,用归一化得分构造伪 p 值 |
auto | 自动选择 | 分类用 anova,回归用 f_regression |
互信息的伪 p 值处理:
# 互信息没有 p 值,用得分归一化后反推
scores = mutual_info_func(X, y)
normalized = scores / max(scores) # 归一化到 [0, 1]
pvals = 1.0 - normalized # 得分越高 → p 值越小 → 越显著
核心选择器
StatisticalTestSelector 是对外暴露的主类,兼容 sklearn 的 fit / transform 接口。
核心流程(fit 方法)
输入: 特征矩阵 X, 目标变量 y
│
├─ 1. 输入校验(类型、缺失值、样本数一致性)
│
├─ 2. 选择检验方法 → _build_test_adapter()
│ 根据 test_type 和 task 构建适配器
│
├─ 3. 执行统计检验 → adapter.run()
│ 得到每个特征的 (检验得分, p值)
│
├─ 4. 多重检验校正 → strategy.apply()
│ 根据 correction 参数选择校正策略
│ 得到 (是否保留的掩码, 校正后的p值)
│
└─ 5. 存储结果
_scores: 各特征的检验得分
_raw_pvals: 原始 p 值
_pvals: 校正后 p 值
_selected: 被选中的特征名列表
transform 方法
只做一件事:从 X 中取出 _selected 中的列,返回筛选后的 DataFrame。
使用示例:
selector = StatisticalTestSelector(
alpha=0.05, # 显著性水平
correction="fdr", # FDR 校正
test_type="auto", # 自动选择检验方法
)
# 拟合并转换
X_filtered = selector.fit_transform(X, y, task="classification")
# 查看详细报告
report = selector.get_report()
print(report)
# 输出类似:
# 特征名称 检验得分 原始P值 校正P值 是否选中
# 特征A 15.3 0.0001 0.002 ✓ 保留
# 特征B 2.1 0.15 0.30 ✗ 剔除
代码实现
完整的代码和模拟数据的测试用例(用统计检验筛选「体检指标」中的关键特征,预测糖尿病风险)共享在:
url11.ctfile.com/d/45455611-… (访问密码: 6872)
相关文件是:statistical_test_selector.py 和 test_statistical_test_selector.py。
测试结果:
数据形状: (300, 115),目标分布: {1: 157, 0: 143}
患病率: 52.3%
【方案A】全部 30 个特征
5折交叉验证准确率: 0.5467 ± 0.0245
【方案B】统计检验筛选后(保留 4 个特征)
5折交叉验证准确率: 0.8400 ± 0.0564
==================================================
准确率变化: +0.2933 (提升 0.2933)
特征数量: 115 → 4
==================================================
共检验 115 个特征,保留 4 个,剔除 111 个 (α=0.05, 校正=fdr, 检验=auto)
感兴趣的朋友最好能将代码下载运行试试看,机器学习最好的理解方式就是多尝试。
几点注意事项
几个需要注意的地方:
统计检验是"单变量"方法
这类方法每次只看一个特征和目标变量的关系,不会考虑特征之间的交互作用。比如特征 A 单独看没用,但 A + B 组合起来可能很有用——这种情况统计检验发现不了。所以它更适合做初步筛选,后面可以再配合其他方法(比如基于树模型的特征重要性)做进一步选择。
先看看数据再跑检验
跑之前最好先做一些基本的探索性分析。比如:
- 特征里有没有缺失值?大多数检验方法不接受缺失值
- 卡方检验要求特征非负,如果特征有负数,需要做平移
- 如果特征的尺度差异很大,建议先做标准化
不要盲目信赖 p 值
p 值小于 0.05 不代表这个特征"非常重要",只是说"有统计证据表明它跟目标有关系"。在大样本情况下,即使非常微弱的关系也可能得到很小的 p 值。所以我一般会结合检验得分(比如 F 值的大小)一起看,而不是只看 p 值。
校正方法的选择很重要
如果不做任何校正,直接用原始 p 值筛选的话,在特征少(比如十几个)的时候问题不大,但特征一旦上到几百几千,假阳性问题就很严重了。
我个人的经验:
- 特征数 < 20:不做校正也行,但最好心里有数
- 特征数 20 ~ 几百:用 FDR 校正
- 特征数上千:用 FDR 校正,并且可以考虑更严格的 alpha(比如 0.01)
总结
最后画个简单的流程图,帮自己理清思路:
原始特征集(N 个特征)
│
▼
对每个特征做统计检验
(ANOVA F / 卡方 / 互信息 / ...)
│
▼
得到每个特征的 p 值
│
▼
多重检验校正
(Bonferroni / FDR)
│
▼
校正后 p 值 < α ?
┌────┴────┐
│ 是 │ 否
▼ ▼
保留 剔除
│
▼
筛选后的特征集(K 个特征,K ≤ N)
统计检验做特征选择,优点很明显:原理清晰、计算快速、结果可解释。
它不能解决所有特征选择的问题,但作为建模流程中的第一步筛选,我觉得是非常实用的。