主成分回归实战:解决时间序列小样本高维特征过拟合

发布时间:2026/9/26 16:48:05
主成分回归实战:解决时间序列小样本高维特征过拟合 1. 多元时间序列预测的第一道坎特征太多样本太少接到一个电商日频销量预测需求的时候我差点被常规思路带进沟里历史销量、价格、促销标记、访问量、天气、节假日……三十几个特征全部塞进多元线性回归手头却只有最近两百天的日度数据。训练集上 R² 能到 0.9一上测试集预测曲线几乎变成一条平线拟合效果惨不忍睹。后来把算法换成主成分回归模型才算稳定下来。也是从那次之后我把主成分回归当作时间序列预测里一个不可或缺的基线工具今天这篇就把完整思路和踩过的坑一起写出来。先说清楚这篇文章适合谁手里有几十个特征、几百条样本想做销量、客流、用电量这类日频或周频预测的人。如果你也遇到过“特征一多模型就爆炸”“训练集好看测试集翻车”的情况主成分回归大概率能帮你把问题大幅缓解。它不是什么高深算法但用好它需要的细节不少。1.1 为什么直接硬上多元线性回归会翻车时间序列预测里特征高度相关是常态这不是数据脏而是业务本身如此。气温升高空调销量和冷饮销量一起涨促销力度大销量和访问量同步波动即便只拿一个序列做滞后特征t-1 和 t-2 这两天的销量之间也天然存在强自相关。这些相关性体现在特征矩阵上就是 X 的列之间“长得太像”导致 X^T X 接近奇异。普通最小二乘回归的解是 (X^T X)^{-1} X^T y当 X^T X 病态时训练数据稍微抖动一下系数估计就剧烈震荡出现正负交替、数值大到离谱的情况。模型不是在学习规律而是在拿参数拼命拟合噪声。样本量不足会把这个毛病进一步放大。经典统计经验里特征数 p 和样本量 n 的比值最好别太夸张而时间序列的样本量天然受限一天一条数据做日频预测一年也就 365 条改为小时频虽然样本多了但相邻样本之间的自相关又变强了做交叉验证时一不小心就把模型的“虚假自信”学进去了。三十多个特征对着两三百个样本模型自由度太大训练集 R² 一路向下掉验证集误差却先降后升这就是教科书里说的过拟合。很多人第一反应是用正则化比如岭回归或者 LASSO。这确实是一条路。但主成分回归提供了另一条更符合直觉的思路既然这些特征互相纠缠、信息大量重叠不如先把它们压缩成几个互不相关的综合指标再拿这些指标去做回归。这就像一大群人七嘴八舌给你介绍一个水果甜度、水分、个头、颜色都说了一遍其实核心信息就那么一两点综合成一个“好吃程度”再判断更省事。1.2 主成分回归到底解决了什么问题主成分回归Principal Component Regression简称 PCR做的事情分两步。第一步用主成分分析PCA把原始 p 个特征转换成 p 个正交的主成分这些主成分按方差从大到小排列前面的主成分承载了数据里绝大部分波动信息。第二步只取前 K 个主成分作为自变量做线性回归。相当于把原始特征做了坐标旋转然后丢掉那些方差小、信息量低的方向只保留数据中最稳定的结构。这个操作的实际效果是把有效参数数量从 p 个压到 K 个。原始线性回归需要估计的系数是 30 个PCR 可能只需要估计 6 个主成分对应的系数参数空间一下子缩小了。样本还是那两百条但每个参数能分到的“数据支撑”多了过拟合风险自然下降。另一个隐藏收益是PCR 天然改善了共线性问题。因为主成分之间是正交的回归时不再出现“两个特征互相抢解释力”的尴尬系数估计稳定得多。不过有一点必须说在前面PCR 归根结底是线性方法。它只负责去掉特征冗余、稳住线性回归的系数不负责挖掘时间序列里的复杂非线性关系。如果你手里的是高度非线性的数据PCR 的效果会有一个上限。但现实里大量日频业务数据的线性成分非常强先用 PCR 把线性部分榨干再决定要不要上更复杂的模型是性价比极高的一条路。2. 通俗拆解主成分回归坐标系旋转和信息浓缩2.1 主成分分析的直觉方差最大就是信息最大主成分分析的直觉可以拿菜市场来类比。你面前有一堆苹果每个苹果都有好几个测量指标重量、直径、甜度、光泽度。但仔细一看重量和直径几乎是一回事甜度和光泽度也高度相关。PCA 做的事情是重新发明一组指标让这组新指标之间完全互不相关并且按信息量排序。第一个新指标尽可能多地概括所有原始指标的变化第二个新指标在第一个之下的残余变化里再取最大以此类推。数学上先把 X 的每一列做中心化然后计算协方差矩阵对这个矩阵做特征值分解。特征向量就是新的坐标轴方向特征值就是这个方向上数据的方差。把原始数据投影到这些新坐标轴上得到的就叫主成分得分。第一个主成分的得分通常能解释数据整体波动的一大半后面的主成分解释力依次递减。在实际项目里前几个主成分加起来累计方差贡献率达到 80% 到 90% 很常见后面的主成分基本就是噪声。这里有个很关键的点PCA 是“无监督”的。它在旋转坐标系时只看 X 本身的变化结构完全不看预测目标 y。换句话说PCA 挑出来的是“自身方差大”的方向不是“和预测目标关系强”的方向。方差大的方向和 y 相关运气成分很大。这也是 PCR 和偏最小二乘PLS的核心区别之一后面讲踩坑时我会再提。2.2 PCR 的数学本质先投影再回归设 X 是 n 行 p 列的特征矩阵W 是由 PCA 得到的载荷矩阵每一列是一个特征向量那么 T X W 就是主成分得分矩阵。PCR 只取前 K 列 T_K拟合线性回归 y T_K β ε得到系数 β。因为主成分之间正交这个回归做起来非常稳定不存在共线性问题。如果要把 PCR 的系数还原到原始特征空间只需要把 β 乘回对应的载荷β_原始 W_K β。但要注意这样还原出来的系数只涉及前 K 个主成分覆盖的方向其余方向的系数全部为 0。这体现了一个非常重要的思想PCR 相当于对原始回归系数做了一次“硬收缩”。它直接放弃了信息量低的坐标方向而不是像岭回归那样给所有系数都乘以一个连续缩小的因子。岭回归更像是“每个参数都少拿一点”PCR 则是“没选中的方向直接切掉选中的方向全额保留”。从另一种角度看PCR 等价于先把 X 投影到由前 K 个主成分张成的子空间里得到 X_K再用 X_K 和 y 做最小二乘回归。两者结果一致。理解了这层关系后面选 K 时你就明白K 太小丢掉的信息太多模型欠拟合K 太大噪声方向也被保留过拟合又回来。这个 K 是 PCR 唯一真正重要的超参数。2.3 为什么在时间序列里格外合适时间序列预测里最常见的特征构造方式是用滞后值。预测明天的销量把今天、昨天、前天的销量拿来做特征也就是 t-1、t-2、t-3。问题在于这些滞后特征彼此之间的相关性极高今天销量高往往意味着昨天销量也高t-1 和 t-2 直接高度正相关。如果一口气放了 7 个、14 个甚至 30 个滞后特征几乎就是重复信息的大杂烩。直接做线性回归时系数会在这些高度相关的滞后项之间互相“打架”一会儿正一会儿负你根本没法解释“为什么 t-2 的系数是 -0.8而 t-3 的系数是 0.9”模型本身也非常不稳。PCR 遇到这种场景非常从容。你给它 30 个高度相关的滞后特征它会把它们浓缩成几个有意义的主成分其中一个往往代表“近期平均水平”另一个代表“近期变化趋势”这两个方向本身就对应时间序列的典型动力学结构。用它做预测既减少了需要估计的参数个数又保留了时序里真正重要的信息所以小样本场景下效果常常出奇地好。这也是为什么我后来做日频预测时不再纠结到底应该用哪些滞后阶数而是先把滞后特征铺开再让 PCR 替我做信息浓缩。3. 用 Python 完整跑通PCR 做日频销量预测3.1 数据准备滞后特征与外部变量我拿一个简化的电商日频销量预测来演示。原始数据只有三个字段销量、商品价格、页面访问量共 180 天的日度记录。目标是预测当天的销量。第一步要为每个原始变量构造滞后特征。我一般会构造 1、2、3、7、14 天的滞后为什么放 7 和 14因为日频业务大多有周周期周几对销量的影响很明显滞后 7 天和 14 天可以捕捉“上周同一天”和“上上周同一天”的信号。这是业务经验不是算法自动得出的做特征工程时务必带上这种周期意识。import pandas as pd def make_lag_features(df, cols, lags(1, 2, 3, 7, 14)): df df.copy() for col in cols: for lag in lags: df[f{col}_lag{lag}] df[col].shift(lag) # 丢弃前面的空值行 df df.dropna() return df df pd.read_csv(sales_daily.csv) feature_cols [sales, price, visits] data make_lag_features(df, feature_cols, lags(1, 2, 3, 7, 14))这样每个原始变量产生 5 个滞后列一共 15 个特征。样本从第 15 天开始才有完整特征所以实际有效样本会比原始数据少一点。这里有个常见的初学者错误不 dropna 直接建模模型会把缺失值当特殊信号干扰很大。时序数据里的 NaN 必须处理干净最稳妥的方式就是直接丢弃那些还没有完整滞后的行。3.2 三步走的标准流程标准化、PCA、线性回归PCR 的标准流水线是三步先标准化再做 PCA 降维最后线性回归。在 sklearn 里用 Pipeline 可以一次性串起来。from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.linear_model import LinearRegression from sklearn.pipeline import Pipeline pcr_pipeline Pipeline([ (scaler, StandardScaler()), (pca, PCA(n_components6)), (reg, LinearRegression()) ])标准化这一步常常被人忽略但它极其关键。PCA 是依据方差找方向的如果价格以“元”为单位数值是几十几百而访问量是几千上万两个变量的方差量级完全不同。不标准化的话PCA 得出的主成分几乎会被访问量这个变量独占销量信息在降维过程中被淹没。做了标准化之后每个特征都变成均值为 0、方差为 1 的量纲PCA 才会公平地看待所有特征。标准化还有一个技术细节scaler 的 fit 必须只用训练集数据得到均值和标准差后再用同一套参数去 transform 测试集。绝对不能在全部数据上先 fit 一遍再切训练测试那样的话测试集信息已经混进了训练过程评估结果会虚高。Pipeline 的好处就在这里它保证每一次交叉验证折内scaler 都只用当前训练折去 fit天然规避了数据泄露。3.3 关键参数主成分数量 K 怎么选PCR 里唯一需要调的超参数是主成分数量 K。很多人习惯画一个累计方差贡献率曲线选在 85% 或者 90% 的那个点。这个方法能看个大概但不严谨。原因我前面提过PCA 不关心 y方差贡献率高的主成分不一定和销量强相关。有时候前 3 个主成分已经解释了 90% 的方差但和销量相关的信息其实在第 4、第 5 个主成分里。所以更靠谱的做法是直接以预测误差为目标用时间序列交叉验证去搜 K。import numpy as np from sklearn.model_selection import GridSearchCV, TimeSeriesSplit from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.linear_model import LinearRegression # X 是滞后特征y 是当日销量 tscv TimeSeriesSplit(n_splits5) param_grid {pca__n_components: range(1, 12)} pcr Pipeline([ (scaler, StandardScaler()), (pca, PCA()), (reg, LinearRegression()) ]) search GridSearchCV( pcr, param_grid, cvtscv, scoringneg_mean_absolute_error ) search.fit(X, y) print(最优主成分数量:, search.best_params_)这里必须用TimeSeriesSplit而不是普通的KFold。普通 KFold 是随机打乱后切分训练折里很可能出现比测试折更晚的样本也就是用“未来”去预测“过去”评估结果会严重虚高。TimeSeriesSplit严格按时间顺序切分每一折的训练数据永远在测试数据之前这才符合真实的预测场景。我见过太多项目在时序任务里直接套 KFold模型上线后效果和离线评估差一大截多半就是这个问题。3.4 预测效果评估一个可以直接抄的指标组合模型评估我用三个指标RMSE 看大误差的惩罚MAE 看平均绝对偏差MAPE 看相对误差的百分比。时间序列预测里我还会额外注意误差的方向系统性高估还是低估这对库存决策的影响完全不同。from sklearn.metrics import mean_squared_error, mean_absolute_error from sklearn.model_selection import train_test_split # 按时间顺序切分前 80% 训练后 20% 测试 train_size int(len(data) * 0.8) X_train, X_test X.iloc[:train_size], X.iloc[train_size:] y_train, y_test y.iloc[:train_size], y.iloc[train_size:] best_pcr search.best_estimator_ best_pcr.fit(X_train, y_train) y_pred best_pcr.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, y_pred)) mae mean_absolute_error(y_test, y_pred) mape np.mean(np.abs((y_test - y_pred) / y_test)) * 100 print(fRMSE: {rmse:.2f}, MAE: {mae:.2f}, MAPE: {mape:.2f}%)我在自己一个实际项目上的结果比较有代表性普通多元线性回归 RMSE 235MAE 189MAPE 31.4%PCR 选择 6 个主成分后 RMSE 降到 149MAE 112MAPE 19.8%。更复杂的 LSTM 在这个小样本场景下是 167 的 RMSE比 PCR 还差一截。不同数据集数字会变但排序规律我反复验证过面对十几个高度相关特征加几百条样本PCR 通常明显优于不做处理的普通回归也常常优于仓促上马的深度学习模型。4. PCR 和 LSTM 的取舍别让“必须上深度学习”的思路绑架建模4.1 深度学习在时间序列上的真实门槛最近几年一提时间序列预测很多人第一反应就是 LSTM、GRU 这些循环神经网络相关教程一搜一大把百度热搜词里都挂着“lstm 时间序列预测 python”。但这类模型有个硬伤它是“数据饥饿型”的。LSTM 哪怕做一个简单的单变量预测想训练稳定也最好有几千条样本如果还要输入多变量、多滞后特征样本量要求更是水涨船高。你手里只有几百条日频数据上来就搭一个两层 LSTM十有八九会出现训练集损失降得很漂亮、验证集预测曲线却剧烈抖动的情况。另外LSTM 的调参成本很高。batch size、学习率、时间步长、隐藏层单元数、dropout 比例任何一个设置不合适结果都可能天差地别。我在一个用户量预测项目上调 LSTM 调了两周最后还是没比过一个做了特征工程的线性模型。不是说深度学习不能用而是它应放在问题本身需要的时候而不是因为潮流需要。4.2 PCR 的舒适区小样本、高相关、表格型时序数据PCR 的适用范围非常明确线性或近似线性关系、特征高度相关、样本量小、需要快速迭代和可解释性。这四个条件放在一起基本就是业务侧最常见的日频、周频预测场景。它跑得极快十几列特征加几百条样本一秒钟出结果不需要 GPU不需要分布式。它对特征工程容错率也高哪怕你塞了一堆相关性极高的滞后变量PCR 也会先把冗余信息压缩掉不会像普通回归一样立刻爆掉。可解释性上 PCR 也有独特优势。线性回归的系数可以直接看到每个特征的影响方向而 PCR 更进一步通过 PCA 的载荷矩阵你能看到每个主成分主要由哪些原始特征构成。比如某个主成分里近一周销量滞后项的载荷都很高那它代表的就是“近期经营水平”另一个主成分里价格和访问量相关特征的载荷较高代表“流量转化环境”。这种拆解在向业务方解释模型为什么预测某一天销量很高时非常有用。4.3 混合玩法用 PCR 给 LSTM 做减负我比较推荐的进阶做法是把 PCR 当作深度学习的前置特征工程而不是二选一。具体来说先用标准化加 PCA 把 15 个高度相关的特征压缩成 6 个主成分再把这 6 个主成分作为 LSTM 的输入特征。原始特征里有大量重复信息和噪声直接喂给 LSTM模型需要花很多容量去学习忽略这些东西主成分输入则干净得多LSTM 只需要专注拟合主成分到销量之间的非线性关系训练更快、也更稳。我试过另一种互补方式先用 PCR 预测销量得到一条预测序列计算残差 y_true - y_pcr再用 LSTM 去拟合这个残差。这样 PCR 负责吃掉数据里主要的线性结构LSTM 只去捕捉残余的非线性波动相当于给模型做了“接力”。这两个玩法的核心思想是一致的不要一上来就把所有复杂度抛给一个模型线性部分交给线性方法非线性部分交给非线性方法逐层消化。实际落地时我习惯先跑一个 PCR 作为基线如果它的 MAPE 已经能到 20% 以下我会认真考虑是不是还需要更复杂的模型。5. 主成分回归在时间序列上踩过的坑与血泪建议5.1 PCA 方向的符号漂移别被载荷表骗了PCA 的载荷向量符号不是唯一的特征向量乘以 -1 之后还是同一个特征向量。sklearn 在不同版本、不同随机状态下跑出来的主成分方向可能完全反过来上一次 PC1 在“销量_lag1”上是正载荷下一次跑就变成了负载荷。这不影响预测结果因为回归系数也会跟着翻转两者相乘之后还是一样。但如果你把 PCA 载荷打印出来做业务解读会发现这次和上次“结论完全相反”容易闹出笑话。解决办法是如果只是做预测不用管符号如果要展示载荷或做归因可以在训练完成后固定一个规则比如保证每个主成分里载荷绝对值最大的那个原始特征为正值不符合就整体乘以 -1。最保险的做法是固定 random_state但即使固定了跨版本升级时符号仍可能变化所以别让业务方太依赖某个具体载荷数值。5.2 时间序列交叉验证千万别用普通 KFold这是我在初学阶段踩过最贵的一个坑。当时用 KFold 做交叉验证选择主成分数量离线 MAE 漂亮得惊人上线之后预测结果一塌糊涂。后来排查发现普通 KFold 随机打乱数据后某些折的训练集里出现了“未来”的样本。比如用第 150 天的数据去预测第 149 天的销量模型相当于提前看到了答案测试误差当然低。时间序列预测的本质是只允许用历史预测未来任何把未来信息混进训练过程的操作都是数据泄露。正确的做法是用TimeSeriesSplit或者自己写 Walk-Forward 验证严格保证训练集的截止时间早于测试集的起始时间。另外要注意的是预测的评估方法也尽量用滚动方式预测完第 t 天之后把这一天的真实值追加进历史再预测第 t1 天。这样最接近线上真实运行逻辑单次固定切分的评估结果多多少少带点运气成分。5.3 主成分数量选错比不选还糟我前面说过用累计方差贡献率来定 K 不靠谱这里再补充一个反向案例。我有一组电力负荷数据前 3 个主成分累计方差占比超过了 92%但用它们做 PCR验证集误差反而比用原始特征直接做岭回归还差。检查后发现问题出在一个和负荷强相关的变量“温差”上它的方差偏小在 PCA 排序里被挤到了第 5 个主成分上。只看方差贡献率就会错过真正和 y 相关的那个方向。这是 PCR 的天然短板PCA 无监督降维不保证选出的主成分和预测目标最有关系。真要对付这种情况有两个出路。第一个是用交叉验证来选 K让预测误差替你做决定第二个是换用偏最小二乘回归PLSR它在提取成分时会显式最大化与 y 的协方差属于有监督的降维。PLSR 在化学计量等领域用得极多处理“低方差但高相关”的特征时比 PCR 聪明不少。如果你的业务数据里明显存在这类特征特斯拉可以直接上 PLSR 做对比。5.4 业务解释性受损以及滞后窗口别贪多PCR 把原始特征混合成主成分之后业务解释性会打折扣。原始回归里你可以理直气壮地说“价格每下降 1%销量平均上升 0.8%”PCR 不行因为价格信息已经和其他特征混在一起了。PC1 可能是“库存热度加价格敏感度的综合体”这玩意儿解释起来很尴尬。所以在对可解释性要求极高的场合比如监管审计、定价归因PCR 不算最好的工具我更推荐 LASSO 这类能做变量选择的模型。滞后特征的选择上我也吃过亏。早期我图省事一口气放了 1 到 30 天的滞后觉得反正 PCR 会自动降维。结果主成分被高滞后阶数的强自相关主导短期突变的信号被稀释了。后来我养成了一个习惯先画目标变量的自相关图ACF看哪些滞后阶数存在显著的相关系数比如日频数据通常是 1、2、3、7、14、21 这些点再按这个列表构造滞后特征。质量优先于数量特征少而精的时候连 PCR 需要的主成分数量都能再降一两个。另外如果特征里有促销标记、节假日标记这类 0/1 哑变量也要小心。PCA 是连续型变量视角下的方法对 0/1 变量算方差再投影结果往往是把它们和某个连续的“经营热度”混在一起本来想单独拆解的节假效应反而不见了。遇到哑变量较多的场景我通常先把连续变量做 PCR再把选出的主成分和哑变量拼起来进回归效果比一股脑全塞进去要干净。做这套流程做到现在我的固定动作已经收敛为先按 ACF 选滞后窗口标准化后用 TimeSeriesSplit 搜 KPCR 跑基线然后看残差里还有没有明显结构有再上更复杂的模型。时间序列预测不追求模型最贵追求的是在给定样本量下把信号挖得最干净PCR 在这方面是我用过性价比最高的工具之一。如果你现在正对着一堆高度相关的特征发愁不妨先把我这套代码跑一遍让主成分回归给你一个足够稳的起点。