1. 为什么 Statsmodels 的一元线性回归值得单独拿出来讲
很多人第一次接触线性回归,都是从 scikit-learn 的LinearRegression开始的。几行代码拟合完,.coef_和.intercept_一取,预测一跑,感觉事情就结束了。但真正做过数据分析项目的人都知道,业务方或者审稿人从来不会只问“斜率是多少”,他们更关心的是:这个斜率显著吗?置信区间多宽?残差是否满足假设?R² 调整后还剩多少解释力?
这些问题,scikit-learn 默认是不回答的。它把回归当成一个纯粹的预测工具,而不是一个统计推断工具。Statsmodels 则完全不同,它的设计哲学更接近计量经济学和经典统计学,OLS结果对象里塞满了各种检验统计量、p 值、置信区间、诊断指标。对于一元线性回归这种看似最简单的模型,Statsmodels 反而能让你把“简单”背后的东西看得最清楚。
这篇文章就围绕 Statsmodels 做一元线性回归展开,重点不是教你调包,而是把结果表里每一列的含义、每个诊断量的用途、以及实际项目中容易踩的坑讲透。适合已经会用 Python 做基本数据处理、想从“跑通模型”进阶到“读懂模型”的读者。哪怕你之前只用过 sklearn,看完也能无缝切换过来。
2. Statsmodels 做一元线性回归的整体思路与选型考量
2.1 为什么一元线性回归还要用 Statsmodels 而不是 sklearn
一元线性回归的形式很简单:y = β₀ + β₁x + ε。sklearn 的LinearRegression通过最小二乘法求出 β₀ 和 β₁,目标是最小化残差平方和。它不假设误差分布,也不输出推断结果。Statsmodels 的OLS同样用最小二乘法,但它建立在经典线性回归模型假设之上:误差项独立、均值为零、同方差、正态分布。基于这些假设,它才能给出系数标准误、t 统计量、p 值和置信区间。
换句话说,如果你只想要一条拟合直线去做预测,sklearn 足够;如果你想知道这条直线的斜率在统计上是否显著不为零、这个模型的解释力有多强、有没有违反假设的地方,Statsmodels 才是正确工具。一元线性回归是理解这些推断概念的绝佳载体,因为变量少、公式简单,所有注意力都能放在统计含义上。
2.2 Statsmodels 两种 API 风格的选择
Statsmodels 提供两套接口:sm.OLS和smf.ols。前者接收数组,后者接收公式字符串。对于一元线性回归,我强烈建议用公式接口smf.ols('y ~ x', data=df),原因有三点。
第一,公式接口自动处理截距项。sm.OLS默认不包含截距,你必须手动add_constant,新手极容易漏掉,导致结果完全错误。第二,公式接口直接引用 DataFrame 列名,结果表里显示的就是变量名,不用再对照位置猜哪个系数对应哪个变量。第三,公式接口支持C()分类变量语法、I()表达式运算,后续扩展到多元回归或加入交互项时几乎不用改代码结构。
当然,如果你在做循环批量拟合、变量名动态生成,数组接口sm.OLS更灵活。两种方式的结果对象完全一致,选哪个取决于你的使用场景。
2.3 一元线性回归在 Statsmodels 中的完整流程
一个规范的 Statsmodels 一元线性回归流程应该包含以下环节:数据准备与缺失值检查、散点图目视检查线性关系、拟合 OLS 模型、读取结果摘要、残差诊断、异常点识别、以及必要时的变量变换。很多人只做第三步就结束了,但真正决定分析质量的是后面几步。下面我会按这个流程逐步展开,把每一步的操作细节和判断标准都讲清楚。
3. 核心细节解析与实操要点
3.1 数据准备阶段的三个关键检查
在拟合之前,有三件事必须做。第一是缺失值检查。Statsmodels 的OLS默认使用列表删除法处理缺失值,也就是说只要某一行在 y 或 x 上有缺失,整行都会被丢弃。如果缺失比例较高,样本量会大幅缩水,结果代表性下降。用df[['y','x']].isnull().sum()先看一眼,缺失多的话要考虑插补或换变量。
第二是数据类型检查。确保 y 和 x 都是数值型。如果 x 是字符串形式的数字,Statsmodels 会报错或者把它当分类变量处理。用df.dtypes确认,必要时用astype(float)转换。
第三是异常值初筛。一元线性回归对异常值非常敏感,一个极端的点就能把斜率拉偏。用箱线图或者简单的描述统计df[['y','x']].describe()看最大最小值是否远离四分位范围。注意,这里只是初筛,不要急着删,后面还有基于残差的正式判断。
3.2 散点图:拟合之前必须做的目视检查
在跑任何回归之前,先画散点图。这一步不能省。线性回归假设 y 和 x 之间是线性关系,如果散点图显示明显的曲线、U 型或者聚集,直接套线性模型就是错的。用 matplotlib 或 seaborn 都行:
import matplotlib.pyplot as plt import seaborn as sns sns.scatterplot(data=df, x='x', y='y') plt.xlabel('自变量 x') plt.ylabel('因变量 y') plt.title('x 与 y 散点图') plt.show()看什么?看整体趋势是否大致呈直线、有没有明显的离群点、方差是否随 x 增大而扩大(异方差迹象)。如果散点图显示非线性,可以考虑对 x 或 y 做对数变换、平方变换,或者直接换模型。这一步花两分钟,能省掉后面大量返工。
3.3 拟合 OLS 模型的标准写法
用公式接口拟合一元线性回归的代码非常简洁:
import statsmodels.formula.api as smf model = smf.ols('y ~ x', data=df) results = model.fit() print(results.summary())这里有几个细节值得注意。smf.ols的第一个参数是公式字符串,~左边是因变量,右边是自变量。data参数传入 DataFrame。fit()执行实际拟合,返回RegressionResults对象。summary()打印完整结果表。
如果你用数组接口,写法是:
import statsmodels.api as sm X = sm.add_constant(df['x']) model = sm.OLS(df['y'], X) results = model.fit()再次强调,数组接口必须手动add_constant,否则模型没有截距,结果会严重偏差。公式接口则自动包含截距,除非你显式写y ~ x - 1。
3.4 结果摘要表逐项解读
summary()输出的表格信息量很大,我按区块拆解。第一部分是模型概览:Dep. Variable 是因变量名,Model 是 OLS,Method 是最小二乘,No. Observations 是有效样本量,Df Residuals 是残差自由度(n-2,因为估计了两个参数),Df Model 是自变量个数(一元回归为1)。
第二部分是拟合优度:R-squared 是决定系数,表示 y 的方差中被 x 解释的比例。Adj. R-squared 是调整后 R²,考虑了自变量个数,一元回归中两者差别不大。F-statistic 是整体显著性检验,对应 Prob (F-statistic) 是 p 值。一元回归中 F 检验和斜率的 t 检验等价,p 值应该一致。
第三部分是系数表:coef 是系数估计值,std err 是标准误,t 是 t 统计量(coef/std err),P>|t| 是双侧 p 值,[0.025, 0.975] 是 95% 置信区间。截距项 const 是 β₀,x 行是 β₁。判断斜率是否显著,就看 x 行的 P>|t| 是否小于 0.05。
第四部分是残差诊断:Omnibus、Prob(Omnibus)、Jarque-Bera、Skew、Kurtosis 检验残差正态性。Durbin-Watson 检验残差自相关,取值 0-4,接近 2 表示无自相关。Cond. No. 是条件数,检测多重共线性,一元回归中通常不用太担心。
3.5 提取关键结果的编程方式
实际项目中不会每次都打印摘要,而是要把关键指标提取出来做后续处理。常用属性:
results.params # 系数,Series,索引为变量名 results.bse # 标准误 results.tvalues # t 统计量 results.pvalues # p 值 results.conf_int() # 置信区间,DataFrame results.rsquared # R² results.rsquared_adj # 调整 R² results.fvalue # F 统计量 results.f_pvalue # F 检验 p 值 results.resid # 残差 results.fittedvalues # 拟合值比如要判断斜率显著性,可以写results.pvalues['x'] < 0.05。要拿斜率置信区间,results.conf_int().loc['x']。这些属性在批量分析时非常有用。
4. 实操过程与核心环节实现
4.1 完整可复现的一元线性回归示例
下面用一个模拟数据集走完整流程。假设我们研究某产品广告投入(x,万元)与销售额(y,万元)的关系。
import numpy as np import pandas as pd import statsmodels.formula.api as smf import matplotlib.pyplot as plt import seaborn as sns np.random.seed(42) n = 100 x = np.random.uniform(1, 20, n) y = 3 + 2.5 * x + np.random.normal(0, 5, n) df = pd.DataFrame({'x': x, 'y': y}) # 散点图 sns.scatterplot(data=df, x='x', y='y') plt.title('广告投入与销售额散点图') plt.show() # 拟合 model = smf.ols('y ~ x', data=df) results = model.fit() print(results.summary())运行后你会看到斜率接近 2.5,截距接近 3,R² 应该很高。这个例子故意造得干净,是为了先跑通流程。真实数据往往没这么理想。
4.2 残差诊断的四个必查项
拟合完不能直接下结论,残差诊断是必须的。第一,残差 vs 拟合值图:看是否随机散布在零线附近,有没有漏斗形(异方差)或曲线(非线性)。第二,残差 QQ 图:看残差是否近似正态分布,点是否大致落在对角线上。第三,残差 vs 自变量图:看残差是否与 x 有关联。第四,Durbin-Watson 值:看残差是否自相关。
import scipy.stats as stats resid = results.resid fitted = results.fittedvalues fig, axes = plt.subplots(1, 3, figsize=(15, 4)) axes[0].scatter(fitted, resid) axes[0].axhline(0, color='red', linestyle='--') axes[0].set_xlabel('拟合值') axes[0].set_ylabel('残差') axes[0].set_title('残差 vs 拟合值') stats.probplot(resid, plot=axes[1]) axes[1].set_title('残差 QQ 图') axes[2].scatter(df['x'], resid) axes[2].axhline(0, color='red', linestyle='--') axes[2].set_xlabel('x') axes[2].set_ylabel('残差') axes[2].set_title('残差 vs x') plt.tight_layout() plt.show() from statsmodels.stats.stattools import durbin_watson print('Durbin-Watson:', durbin_watson(resid))如果残差图显示漏斗形,说明存在异方差,可以考虑对 y 取对数,或者使用稳健标准误results = model.fit(cov_type='HC3')。如果 QQ 图严重偏离,说明正态性假设不满足,小样本下会影响 p 值可信度,大样本下中心极限定理会缓解这个问题。
4.3 异常点与强影响点的识别
异常点分两种:一种是 y 方向偏离大的(离群点),一种是 x 方向极端的(高杠杆点)。Statsmodels 提供了影响诊断工具:
influence = results.get_influence() summary_frame = influence.summary_frame() # 学生化残差绝对值大于 3 视为离群点 outliers = summary_frame[abs(summary_frame['student_resid']) > 3] print('离群点索引:', outliers.index.tolist()) # Cook 距离大于 4/n 视为强影响点 cooks_d = summary_frame['cooks_d'] influential = summary_frame[cooks_d > 4/len(df)] print('强影响点索引:', influential.index.tolist())识别出来后不要急着删。先检查是不是数据录入错误,如果是错误就修正;如果是真实极端值,要分析它为什么极端,是否代表特殊业务场景。删点必须在报告中说明,不能偷偷删。
4.4 稳健标准误的使用场景
当残差诊断发现异方差时,普通标准误会被低估或高估,导致 p 值不可靠。这时用稳健标准误:
results_robust = model.fit(cov_type='HC3') print(results_robust.summary())HC3 是常用的异方差稳健标准误,对小样本表现较好。对比普通结果和稳健结果,如果系数显著性结论一致,说明异方差影响不大;如果结论翻转,就要谨慎下结论。注意,稳健标准误只修正标准误和 p 值,不改变系数估计值。
4.5 预测与置信区间
Statsmodels 做预测需要传入 DataFrame,且列名要和公式中一致:
new_data = pd.DataFrame({'x': [5, 10, 15]}) predictions = results.get_prediction(new_data) pred_summary = predictions.summary_frame(alpha=0.05) print(pred_summary)summary_frame返回 mean、mean_se、mean_ci_lower、mean_ci_upper、obs_ci_lower、obs_ci_upper。mean 是预测均值,mean_ci 是均值的置信区间,obs_ci 是单个观测值的预测区间。后者比前者宽,因为它包含了误差项的不确定性。报告预测结果时要说清楚用的是哪个区间,两者含义不同。
5. 常见问题与排查技巧实录
5.1 结果表里出现 nan 或报错怎么办
最常见的原因是数据里有缺失值或无穷值。Statsmodels 遇到缺失值会丢弃整行,如果丢弃后样本量太小或者某变量全空,结果就会出现 nan。用df[['y','x']].isnull().sum()和np.isinf(df[['y','x']]).sum()检查。另一个原因是 x 是常量,方差为零,无法估计斜率,这时会报错或给出 nan。检查df['x'].std()是否为零。
5.2 R² 很高但斜率不显著是怎么回事
这种情况通常说明样本量太小,或者 x 的变异范围太窄。R² 高只说明拟合线离点近,但如果 x 本身变化不大,斜率的估计不确定性就大,标准误大,t 值小,p 值就大。解决办法是扩大样本量或扩大 x 的观测范围。还有一种可能是异常点主导了拟合,删掉后关系消失,这需要结合影响诊断判断。
5.3 截距项没有业务意义要不要去掉
截距项在 x=0 时有意义。如果 x=0 在业务上不可能(比如身高、价格),截距只是数学上的定位参数,不用强行解释。但不要因为“没意义”就删掉截距。去掉截距会强制回归线过原点,除非理论上有充分理由认为 x=0 时 y 必须为 0,否则会导致斜率估计偏差。公式接口写y ~ x - 1可以去掉截距,但要慎用。
5.4 残差正态性检验不通过怎么办
先看样本量。大样本下(n>100),中心极限定理保证系数估计近似正态,p 值仍然可用,不必过度担心。小样本下正态性重要,可以考虑对 y 做 Box-Cox 变换或对数变换。另外,正态性检验本身对异常值敏感,先处理异常点再看检验结果。如果只是轻微偏离,通常不影响主要结论。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| 结果出现 nan | 缺失值、无穷值、常量 x | isnull/isinf/std 检查 | 清洗数据或换变量 |
| 斜率 p 值大 | 样本小、x 变异窄、异常点 | 看样本量、x 范围、影响诊断 | 扩样本、扩范围、处理异常点 |
| 残差漏斗形 | 异方差 | 残差 vs 拟合值图 | 对数变换或稳健标准误 |
| QQ 图弯曲 | 残差非正态 | QQ 图、Jarque-Bera | 变换或增大样本 |
| DW 值远离 2 | 残差自相关 | Durbin-Watson 值 | 检查数据顺序、加滞后项 |
| R² 异常高 | 过拟合、数据泄漏 | 检查变量来源 | 重新审视数据流程 |
5.6 几个容易忽略的实操心得
第一,公式接口中变量名如果包含特殊字符(空格、横杠),要用Q()包裹,比如Q("变量 名")。第二,summary()的输出宽度可能不够,可以设置pd.set_option('display.width', 200)。第三,拟合结果对象可以保存为 pickle,方便后续加载,不用每次重新拟合。第四,批量做多个一元回归时,用循环加字典收集结果,比一个个手动跑高效得多。第五,报告结果时不要只给 R² 和 p 值,置信区间和样本量同样重要,缺一个都不完整。
6. 从一元到多元的平滑过渡建议
一元线性回归是起点,不是终点。掌握了 Statsmodels 的公式接口和结果解读后,扩展到多元回归只需要在公式右边加变量:y ~ x1 + x2 + x3。结果表的解读逻辑完全一致,只是多了几个系数行。但要注意多重共线性问题,这时 Cond. No. 和 VIF 就派上用场了。另外,多元回归中调整 R² 比 R² 更重要,因为 R² 会随变量增加而虚高。
我个人的习惯是,任何多元回归之前,先把每个自变量和因变量单独做一元回归,看看各自的关系方向和强度。这一步能帮你发现变量变换的需求、异常值的影响、以及哪些变量可能交互。一元回归跑起来快,结果直观,是探索性分析的利器,不要因为它简单就跳过。
最后分享一个小技巧:Statsmodels 的结果对象有save()和load()方法,可以把拟合结果序列化到文件。做报告或者需要反复查看结果时,存一份省得重跑。另外,results.summary2()返回的是 DataFrame 格式的摘要,方便导出到 Excel 或做进一步处理,比summary()的文本格式更适合程序化使用。