大数跨境

导师皱眉:“Claude Code帮你把回归模型写完了,就以为项目结束了?”我:“指标都提升了呀!”导师:“不会F检验,提升可能只是偶然!”

导师皱眉:“Claude Code帮你把回归模型写完了,就以为项目结束了?”我:“指标都提升了呀!”导师:“不会F检验,提升可能只是偶然!” 机器学习和人工智能AI
2026-07-13
4

哈喽,大家好~

今儿和大家分享一个关于,F 检验的一些理论和实现的内容~

我们在机器学习、统计建模里,经常会遇到一个问题:我新添了一个变量(或一组变量),模型性能提升是真实存在的、还是只是“偶然更好”呢?

F 检验(F-test)就是帮我们回答这个问题的一个经典工具,一起来看看~

F 检验核心逻辑

比如,你在做回归预测,有两个人在比模型 A 和模型 B。

模型 B 比模型 A 多用了一些变量(所以模型 B 更“复杂”)。

现在我们要判断:模型 B 的改进是真实有用的,还是仅仅因为它更复杂、在这个数据集上“恰好”更适合(过拟合)?

其实,F 检验思路很直白:

  • 先计算两个模型的残差平方和(Residual Sum of Squares, ),也就是模型没解释掉的误差的总量。
  • 如果加入新变量真的有用,那么带新变量的模型的   应该显著变小。
  • 把 “  减少的量” 和 “模型剩余噪声的估计” 做个比值(标准化),这个比值在“无效”(新变量其实没用)的前提下会服从 F 分布。
  • 根据 F 值和对应的 F 分布,我们可以得到一个 p 值,判断这个改进是否显著。

更数学化一点,假设两个模型是嵌套(nested)的:简单模型(reduced)参数个数是  ,复杂模型(full)参数个数是  ,样本量是 

记两个模型的残差平方和分别为     ,则 F 统计量为:

分子表示每新增一个参数带来的平均 RSS 改进;分母则是复杂模型每个自由度的残差估计。若   很大,说明改进不太可能来自随机波动,新增变量显著;对应的 p 值小于显著性阈值(比如 0.05),我们就拒绝“新增变量无效”的原假设。

另外一种常见情况是方差分析(ANOVA):比较多组均值是否相等,也可以转成回归框架(用哑变量),最后得到同样的 F 检验结论。F 检验在回归模型里是非常基础也很直观的“整体显著性 / 增量显著性”检验工具。

代码实现

我们生成一个数据集,包含两个连续变量  ,以及一个 3 类类别变量 group(A/B/C)。

生成目标变量  :真实关系只与   和 group 有关,  为无关噪声。

我们希望用 F 检验来验证:  是否有显著贡献?group 是否显著?

import torch
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats

palette = sns.color_palette("bright")

# 1. 数据
torch.manual_seed(0)
np.random.seed(0)

n = 300
# 连续变量 X1 与 X2
X1 = torch.randn(n) * 2.0 + 1.0        # 有信息
X2 = torch.randn(n) * 1.5              # 干扰变量,无效
# 类别变量 3 类
groups = np.random.choice(['A''B''C'], size=n, p=[0.40.350.25])

# 真正的生成机制: y = 2.0 + 1.5*X1 + group_effect + noise
group_effect_map = {'A'0.0'B'1.0'C'-0.8}
group_effects = np.array([group_effect_map[g] for g in groups])
noise = np.random.randn(n) * 1.0

y = 2.0 + 1.5 * X1.numpy() + group_effects + noise
y = torch.tensor(y, dtype=torch.float32)

# 把类别变量编码成哑变量(one-hot,但丢弃一列避免多重共线性)
# 使用 A 作为基准,不做显式基准列
group_labels = np.array(groups)
dummies = {}
for g in ['B''C']:  # 只创建两列
    dummies[g] = (group_labels == g).astype(float)
G = torch.tensor(np.vstack([dummies['B'], dummies['C']]).T, dtype=torch.float32)

# 准备设计矩阵 X(含截距)
# We'll try several models:
# Model Full: intercept + X1 + X2 + group(B) + group(C)
# Model without X2: intercept + X1 + group(B) + group(C)
# Model no-group: intercept + X1 + X2
ones = torch.ones(n, 1)
X_full = torch.cat([ones, X1.view(-1,1), X2.view(-1,1), G], dim=1)  # shape: (n, 5)
X_noX2 = torch.cat([ones, X1.view(-1,1), G], dim=1)                 # shape: (n, 4)
X_nogroup = torch.cat([ones, X1.view(-1,1), X2.view(-1,1)], dim=1)  # shape: (n, 3)

# 简单的最小二乘解函数(正规方程)
def ols_solution(X, y):
    # beta = (X^T X)^{-1} X^T y
    XtX = X.t().mm(X)
    Xty = X.t().mv(y)
    beta = torch.linalg.solve(XtX, Xty)
    y_hat = X.mv(beta)
    resid = y - y_hat
    RSS = resid.pow(2).sum().item()
    return beta, y_hat, resid, RSS

# 拟合三个模型
beta_full, yhat_full, resid_full, RSS_full = ols_solution(X_full, y)
beta_noX2, yhat_noX2, resid_noX2, RSS_noX2 = ols_solution(X_noX2, y)
beta_nogroup, yhat_nogroup, resid_nogroup, RSS_nogroup = ols_solution(X_nogroup, y)

n = float(n)
p_full = X_full.shape[1]   # 参数个数(含截距)
p_noX2 = X_noX2.shape[1]
p_nogroup = X_nogroup.shape[1]

print("RSS (full):", RSS_full)
print("RSS (no X2):", RSS_noX2)
print("RSS (no group):", RSS_nogroup)

# 2. F 检验:检验 X2 是否显著(full vs noX2)
df1 = p_full - p_noX2   # 新增参数个数
df2 = n - p_full
F_X2 = ((RSS_noX2 - RSS_full) / df1) / (RSS_full / df2)
pval_X2 = stats.f.sf(F_X2, df1, df2)

# 3. F 检验:检验 group 是否显著(full vs nogroup)
df1_g = p_full - p_nogroup
df2_g = n - p_full
F_group = ((RSS_nogroup - RSS_full) / df1_g) / (RSS_full / df2_g)
pval_group = stats.f.sf(F_group, df1_g, df2_g)

print(f"Test X2: F = {F_X2:.4f}, df1={int(df1)}, df2={int(df2)}, p = {pval_X2:.6f}")
print(f"Test group: F = {F_group:.4f}, df1={int(df1_g)}, df2={int(df2_g)}, p = {pval_group:.6f}")

# 4. 可视化分析
import pandas as pd
df = pd.DataFrame({
    'X1': X1.numpy(),
    'X2': X2.numpy(),
    'y': y.numpy(),
    'yhat_full': yhat_full.numpy(),
    'resid_full': resid_full.numpy(),
    'group': group_labels
})

# 画 1:X1 与 y 的散点图
plt.figure(figsize=(10,6))
sns.scatterplot(data=df, x='X1', y='y', hue='group', palette=palette, s=60, alpha=0.8)
# 整体回归线(用拟合得到的系数)
coef = beta_full.numpy()
# y = coef[0] + coef[1]*X1 + ...
xs = np.linspace(df['X1'].min(), df['X1'].max(), 100)
# 用 group 为 0 (A) 情况画线 (B/C 为 0)
ys = coef[0] + coef[1]*xs
plt.plot(xs, ys, color='black', linewidth=2, label='fitted (group=A)')
plt.title('Scatter: X1 vs y (colored by group). Black line: fitted for group A')
plt.legend()
plt.show()

# 画 2:特征相关矩阵的热力图
plt.figure(figsize=(8,6))
corrmat = df[['X1','X2','y']].corr()
sns.heatmap(corrmat, annot=True, cmap='coolwarm', vmin=-1, vmax=1, linewidths=0.5)
plt.title('Correlation matrix (X1, X2, y)')
plt.show()

# 画 3:残差 vs 拟合值
plt.figure(figsize=(10,6))
sns.scatterplot(x=df['yhat_full'], y=df['resid_full'], hue=df['group'], palette=palette, s=50, alpha=0.8)
plt.axhline(0, color='black', linestyle='--')
plt.xlabel('Fitted values')
plt.ylabel('Residuals')
plt.title('Residuals vs Fitted (full model)')
plt.show()

# 画 4:残差分布(直方图 + KDE)
plt.figure(figsize=(10,6))
sns.histplot(df['resid_full'], kde=True, color='orange', stat='density', bins=30)
plt.title('Residual distribution (full model)')
plt.show()

# 画 5:按 group 的均值比较(含置信区间 / 标准误)
plt.figure(figsize=(8,6))
grouped = df.groupby('group')['y'].agg(['mean','count','std']).reset_index()
grouped['se'] = grouped['std'] / np.sqrt(grouped['count'])
plt.bar(grouped['group'], grouped['mean'], yerr=1.96*grouped['se'], color=['#FF6F61','#6B5B95','#88B04B'], alpha=0.8)
plt.title('Group means with 95% CI (for y)')
plt.ylabel('Mean y')
plt.show()

# 画 6:F 分布与观测 F 值(以检验 group 为例)
plt.figure(figsize=(10,6))
x = np.linspace(0, stats.f.ppf(0.995, df1_g, df2_g), 500)
yvals = stats.f.pdf(x, df1_g, df2_g)
plt.plot(x, yvals, color='purple', lw=2)
# 标出观测到的 F
plt.axvline(F_group, color='red', linestyle='--', lw=2, label=f'observed F = {F_group:.3f}')
plt.fill_between(x, 0, yvals, where=(x>=F_group), color='red', alpha=0.25, label=f'p = {pval_group:.4e}')
plt.title('F distribution under H0 (testing group). Shade = p-value area')
plt.legend()
plt.show()

Scatter: X1 vs y,并画出以 group=A 为基准的拟合直线

可以直观看 X1 与 y 的关系,以及不同 group 的样本是否集中(是否存在组间偏移)。因为真实模型中 X1 与 y 是正相关的,我们应当看到斜率为正。不同 group 的点云应该沿垂直方向发生偏移(因为 group 有偏移量)。黑色直线是模型对 A 组(基准组)时的拟合线,方便直观比较。

相关矩阵热力图(X1, X2, y):

可以快速查看特征与目标之间的线性相关性。由于我们构造中 X1 与 y 有较强相关,X2 与 y 的相关性应接近 0。

残差 vs 拟合值图:

检查模型假设(线性关系、同方差、异常值)。理想情况下,残差应随机分布在 0 附近,不依赖于拟合值。若出现明显的模式或异方差(残差随拟合值放大),说明模型存在问题。

残差分布(直方图 + KDE):

检查残差是否近似正态分布(很多统计推断的前提)。若残差偏离正态,p 值解释可能需谨慎。

按 group 的均值柱状图(含 95% CI):

直接展示不同组的平均 y 值和置信区间。若组间均值显著不同,则 group 变量理应是显著的(这与 ANOVA 本质一致)。

F 分布与观测 F 值:

将观测到的 F 置于理论 F 分布下,阴影表示我们观测到或更极端的区域(即 p 值所代表的概率)。这有助于理解 p 值的几何意义:在原假设成立时,观测到如此大的 F 的概率有多小。

总结

总的来说,F 检验是比较嵌套模型是否有显著改进的经典工具,把“RSS 的改进”标准化后与 F 分布比较。

在回归框架下,检验单个变量或一组变量是否显著都可以用 F 检验(组检验其实就是 ANOVA 思路)。

最后

最近,我们的机器基础课程,已经逐步完善了~

我的定位是,《机器学习基础:从问题定义到可复现交付》是一门面向有一定 Python 基础学习者的系统入门课程。

直接先看目录:

这个内容的核心价值,就是帮你建立一套稳定的机器学习工作流。学完后,我们希望你不只是“知道几个模型”,而是能判断一个问题是否适合建模,能把场景翻译成清楚的机器学习任务,能搭建不泄漏的 Pipeline,能选择合理指标,能做公平比较,并能用实验卡片把整个过程记录下来。

如果大家觉得有需要,加入一起学习~

永久有效、永久答疑~

大家可以随时提问,我会在平台给大家整理好,让你清晰、快速搞懂。

笔记,可以在平台直接记笔记,甚至可以在课程中进行记笔记,一套流程,非常高效完成~

【声明】内容源于网络
0
0
机器学习和人工智能AI
让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
内容 355
粉丝 0
机器学习和人工智能AI 让我们一起期待 AI 带给我们的每一场变革!推送最新行业内最新最前沿人工智能技术!
总阅读3.1k
粉丝0
内容355