
用 Python 揭秘均值回归策略:你的收益从何而来?
如果你正在学习 Python,又对金融数据分析、量化交易感兴趣,那么「回归模型」几乎是绕不开的第一课。它既是统计学的经典工具,也是机器学习的入门基石。很多人一听到「线性回归」就觉得简单,但当它被用来预测股票收益、构建因子模型、评估交易信号时,背后其实藏着不少值得细细品味的工程细节。
本文将带你用 Python 走一遍量化场景下的回归建模全流程:从最基础的一元线性回归,到多元回归、SGD 回归,再到因子模型、特征工程、时间序列交叉验证、Ridge/Lasso 正则化,最后落到用逻辑回归预测股价涨跌。所有代码都配有中文注释,力求让你「看得懂、跑得起、用得上」。
线性回归的核心思想很朴素:用一个连续的输出(响应变量 / 因变量)去对应一个或多个输入(特征 / 自变量),并假设它们之间是「线性」关系。换句话说,在其他特征固定时,某个特征对响应的影响是一条直线,且每个特征的影响是「相加」组合起来的。
数学上,一元线性模型可以写成:
其中 是截距, 是斜率, 是误差项,代表观测值与「完美直线」之间的偏差。模型拟合到真实数据后,这些偏差就被称为「残差」。
我们先造一批带噪声的模拟数据,再用 statsmodels 来拟合:
import numpy as np
import pandas as pd
import statsmodels.api as sm
# 生成 100 个均匀分布的 x 值,范围从 -5 到 50
x = np.linspace(-5, 50, 100)
# 按 y = 50 + 2x 的关系生成 y,并叠加均值为 0、标准差为 20 的正态噪声
y = 50 + 2 * x + np.random.normal(0, 20, size=len(x))
data = pd.DataFrame({'X': x, 'Y': y})
# add_constant 会给特征矩阵加上一列常数 1,用来估计截距项
# 如果不加这一列,回归会被强制过原点
X = sm.add_constant(data['X'])
# OLS 即普通最小二乘法,fit() 会找到使残差平方和最小的参数
model = sm.OLS(data['Y'], X).fit()
print(model.summary())运行后,你会看到一份完整的统计报告。在我们的例子里,截距约为 49.54、斜率约为 1.99,几乎完美还原了数据生成时用的 50 和 2。R-squared 约为 0.739,意味着这条直线解释了大约 74% 的 Y 变化;两个系数的 p 值都非常小,说明它们在统计上显著。
小贴士:OLS 其实有闭式解(解析解),你完全可以用矩阵运算手动求出系数,结果会和 fit() 一模一样:
# OLS 闭式解公式:beta = (X^T X)^(-1) X^T y
beta = np.linalg.inv(X.T.dot(X)).dot(X.T.dot(y))
pd.Series(beta, index=X.columns)
# 输出:const ≈ 49.54, X ≈ 1.99当有两个(或更多)相互独立的特征时,模型就扩展为:
拟合方式和一元几乎一样,只是特征矩阵多了几列:
# 构造两个特征的网格数据
size = 25
X_1, X_2 = np.meshgrid(np.linspace(-50, 50, size),
np.linspace(-50, 50, size), indexing='ij')
data = pd.DataFrame({'X_1': X_1.ravel(), 'X_2': X_2.ravel()})
# 真实关系:y = 50 + 1*X_1 + 3*X_2 + 噪声
data['Y'] = 50 + data.X_1 + 3 * data.X_2 + np.random.normal(0, 50, size=size**2)
X = data[['X_1', 'X_2']]
y = data['Y']
# 同样先加常数项,再拟合
X_ols = sm.add_constant(X)
model = sm.OLS(y, X_ols).fit()
print(model.summary())结果中截距约 52.28、X_1 系数约 0.95、X_2 系数约 2.86,与真实的 50、1、3 非常接近。
这里要特别关注报告底部的几个诊断指标,它们是回归质量的「体检报告」:
除了直接求解析解,我们还能用随机梯度下降(SGD)来「迭代逼近」最优参数。scikit-learn 提供了 SGDRegressor。但有一个关键易错点:梯度优化对特征的量纲非常敏感,所以训练前必须先标准化特征,否则量纲大的特征会主导整个优化过程。
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import SGDRegressor
# StandardScaler 会在 fit 阶段计算每个特征的均值和标准差
# 在 transform 阶段做「减均值、除标准差」的中心化与缩放
scaler = StandardScaler()
X_ = scaler.fit_transform(X)
# 配置 SGD 回归器
sgd = SGDRegressor(loss='squared_loss', # 用平方损失,对应最小二乘
fit_intercept=True, # 估计截距
shuffle=True, # 每轮训练前打乱样本顺序
random_state=42, # 固定随机种子,结果可复现
learning_rate='invscaling', # 学习率随迭代逐渐减小
eta0=0.01,
power_t=0.25)
sgd.fit(X=X_, y=y) # 训练模型在数据规模合适的情况下,SGD 与 OLS 会得到几乎一致的结果。比如对比两者的均方根误差(RMSE),往往是同一个数值,这从侧面印证了它们在拟合同一个底层关系。
在量化领域,线性因子模型用来衡量一只资产的收益与背后风险因子之间的关系。每个风险因子都有自己的「风险溢价」,资产的整体收益可以看成这些溢价的加权组合。
经典的 Fama-French 五因子包括:
为了解决残差相关带来的推断难题,Fama 与 MacBeth 提出了一个两步法:
下面是两步法的核心代码骨架:
from statsmodels.api import OLS, add_constant
# 第一步:估计每个行业组合的因子暴露
betas = []
for industry in ff_portfolio_data:
step1 = OLS(endog=ff_portfolio_data.loc[ff_factor_data.index, industry],
exog=add_constant(ff_factor_data)).fit()
betas.append(step1.params.drop('const')) # 去掉常数项,只保留因子系数
betas = pd.DataFrame(betas,
columns=ff_factor_data.columns,
index=ff_portfolio_data.columns)
# 第二步:在每个时间点做横截面回归,估计风险溢价
lambdas = []
for period in ff_portfolio_data.index:
step2 = OLS(endog=ff_portfolio_data.loc[period, betas.index],
exog=betas).fit()
lambdas.append(step2.params)
lambdas = pd.DataFrame(lambdas,
index=ff_portfolio_data.index,
columns=betas.columns.tolist())
# 把每个因子的溢价在时间上取平均,作为最终估计
lambdas.mean()在 2010 至 2017 年的样本里,市场因子(Mkt-RF)的平均溢价显著为正(约 1.22),而其他几个风格因子的平均溢价偏小甚至略为负。值得一提的是,linearmodels 库提供了现成的 LinearFactorModel,可以一行搞定这套两步法,结果与手写版本一致。
要让模型预测未来收益,光有价格还不够,需要构造一批有信息量的「特征」。常见做法是基于 OHLCV(开高低收量)数据,借助 TA-Lib 计算技术指标。
from talib import RSI, BBANDS, MACD, ATR
# RSI(相对强弱指标):衡量近期涨跌动量,按股票分组分别计算
prices['rsi'] = prices.groupby(level='ticker').close.apply(RSI)
# MACD:由两条移动平均线构造的动量指标,这里做了标准化处理
def compute_macd(close):
macd = MACD(close)[0] # 取 MACD 主线
return (macd - np.mean(macd)) / np.std(macd) # 中心化 + 缩放
prices['macd'] = prices.groupby('ticker', group_keys=False).close.apply(compute_macd)除了技术指标,还会构造多周期的历史收益特征,并对极端值做「缩尾」处理(winsorize),避免个别异常值或数据错误主导模型:
lags = [1, 5, 10, 21, 42, 63] # 1 天到约 3 个月的多个回看窗口
q = 0.0001 # 极端分位数阈值
for lag in lags:
prices[f'return_{lag}d'] = (
prices.groupby(level='ticker').close
.pct_change(lag) # 计算 lag 天的收益率
# 对最极端的 0.01% 做裁剪(缩尾),减小离群值影响
.pipe(lambda x: x.clip(lower=x.quantile(q), upper=x.quantile(1 - q)))
# 把多日收益换算成「日均」收益,便于不同周期间比较
.add(1).pow(1 / lag).sub(1)
)
# 构造未来收益作为预测目标(注意 shift 用负数表示「向未来看」)
for t in [1, 5, 10, 21]:
prices[f'target_{t}d'] = prices.groupby(level='ticker')[f'return_{t}d'].shift(-t)易错点提醒:滚动指标(如 RSU、MACD)和滞后特征在每只股票序列开头会产生缺失值,未来收益目标在序列末尾也会缺失,这是正常现象,建模前记得妥善处理。
普通的随机交叉验证在金融数据上是「致命」的,因为它会把未来的信息泄露到训练集里。正确做法是按时间顺序划分训练集和测试集,并留出一个「前瞻间隔」来防止标签重叠泄露。
下面是一个自定义的多资产时间序列交叉验证器的思路:
class MultipleTimeSeriesCV:
"""生成 (训练索引, 测试索引) 对,假设 MultiIndex 含有 'symbol' 和 'date' 两级,
并会清除重叠的结果,避免未来信息泄露"""
def __init__(self, n_splits=3, train_period_length=126,
test_period_length=21, lookahead=None, shuffle=False):
self.n_splits = n_splits
self.lookahead = lookahead
self.test_length = test_period_length
self.train_length = train_period_length
self.shuffle = shuffle
def split(self, X, y=None, groups=None):
# 取出所有不重复的日期,并按时间倒序排列
unique_dates = X.index.get_level_values('date').unique()
days = sorted(unique_dates, reverse=True)
split_idx = []
for i in range(self.n_splits):
# 测试窗口在前,训练窗口在更早的时间,中间留出 lookahead 间隔
test_end_idx = i * self.test_length
test_start_idx = test_end_idx + self.test_length
train_end_idx = test_start_idx + self.lookahead - 1
train_start_idx = train_end_idx + self.train_length + self.lookahead - 1
split_idx.append([train_start_idx, train_end_idx,
test_start_idx, test_end_idx])
dates = X.reset_index()[['date']]
for train_start, train_end, test_start, test_end in split_idx:
train_idx = dates[(dates.date > days[train_start])
& (dates.date <= days[train_end])].index
test_idx = dates[(dates.date > days[test_start])
& (dates.date <= days[test_end])].index
yield train_idx, test_idx这套机制保证了:测试期始终紧跟在训练期之后,且二者之间因为 lookahead 而被「净化」,互不重叠。
当特征很多时,普通线性回归容易过拟合。Ridge(L2 正则)和 Lasso(L1 正则)通过给系数加惩罚来缓解这个问题:
在量化里,我们常用「信息系数」(IC,即预测值与真实收益的 Spearman 秩相关)来评估模型,因为我们更关心排序对不对,而非数值精度。
from sklearn.linear_model import Ridge, Lasso
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from scipy.stats import spearmanr
from sklearn.metrics import mean_squared_error
# 注意:正则化模型对量纲敏感,所以用 Pipeline 把标准化和模型串起来
model = Ridge(alpha=1.0, fit_intercept=False, random_state=42)
pipe = Pipeline([
('scaler', StandardScaler()), # 先标准化
('model', model) # 再拟合 Ridge
])
# 在每个时间切分上训练并预测
for train_idx, test_idx in cv.split(X):
X_train, y_train = X.iloc[train_idx], y[target].iloc[train_idx]
X_test, y_test = X.iloc[test_idx], y[target].iloc[test_idx]
pipe.fit(X=X_train, y=y_train)
y_pred = pipe.predict(X_test)
# 按天计算信息系数(IC)和均方根误差(RMSE)
preds = y_test.to_frame('actuals').assign(predicted=y_pred)
preds_by_day = preds.groupby(level='date')
ic = preds_by_day.apply(lambda s: spearmanr(s.predicted, s.actuals)[0] * 100)
rmse = preds_by_day.apply(
lambda s: np.sqrt(mean_squared_error(s.actuals, s.predicted)))实战对比中,三种模型的整体 IC 都不高(普通线性回归约 1.5%,Ridge 约 1.55%,Lasso 在合适的正则强度下能达到约 3.6%)。这其实非常符合金融数据的特性:信号弱、噪声大,能稳定地「跑赢随机」就已经不容易。同时也说明 Lasso 的特征筛选在这里带来了额外收益。
如果我们不关心收益的具体数值,只想预测「涨」还是「跌」,那就该逻辑回归出场了。它把回归输出通过 logistic 函数映射成 0 到 1 之间的概率,非常适合二分类。
先把连续的未来收益转成 0/1 标签:
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
# 未来收益 > 0 标记为 1(涨),否则为 0
y.loc[:, 'label'] = (y[target] > 0).astype(int)
# 同样用 Pipeline 串起标准化和逻辑回归
model = LogisticRegression(C=1.0, fit_intercept=True,
random_state=42, n_jobs=-1)
pipe = Pipeline([
('scaler', StandardScaler()),
('model', model)
])
for train_idx, test_idx in cv.split(X):
X_train, y_train = X.iloc[train_idx], y.label.iloc[train_idx]
pipe.fit(X=X_train, y=y_train)
X_test, y_test = X.iloc[test_idx], y.label.iloc[test_idx]
# 取预测为正类(涨)的概率
y_score = pipe.predict_proba(X_test)[:, 1]
# 用 AUC 评估分类质量(0.5 表示与随机猜测无异)
auc = roc_auc_score(y_score=y_score, y_true=y_test)逻辑回归同样可以用 statsmodels 做统计推断。在宏观经济数据的例子里(用 GDP 增长率构造涨跌标签),模型的伪 (Pseudo R-squared)能达到约 0.50,似然比检验 p 值极小,说明这些宏观变量整体上确实有解释力。
关键概念解释:
走完这一整套流程,希望你对量化场景下的回归建模有了系统的认识。我们从一元线性回归出发,逐步深入到多元回归、SGD 优化、因子模型、特征工程、时间序列交叉验证、正则化模型,最后落到逻辑回归分类。
几条值得反复回味的要点:
对于学习 Python 的你来说,这些代码不仅是量化的入门砖,更是把 NumPy、pandas、statsmodels、scikit-learn 串联起来的绝佳练习。建议动手把每段代码跑一遍,再尝试换数据、调参数,慢慢就能体会到「数据分析」与「工程实现」结合的乐趣。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐