简介这份PDF资料面向具备一定Python与机器学习基础、希望系统掌握高维数据分类与回归实战的读者以Kaggle经典房价预测赛题为载体重点讲解如何用Stacking思维融合多种模型的优势。内容围绕完整流程展开从pandas读取train与test数据、合并数据集到对SalePrice做log1p平滑处理再到MSSubClass等类别变量转字符串、get_dummies独热编码、缺失值均值填充与数值特征标准化最后引入Ridge岭回归与RandomForestRegressor随机森林配合cross_val_score交叉验证评估并演示将多模型预测结果作为新特征输入次级模型的堆叠思路同时提醒预测结果需用expm1逆变换还原原始尺度。资源包为1个PDF文件约131KB轻量便于随时查阅适合作为特征工程与模型融合的参考笔记。目前已有1068人学习可帮助读者理清高维数据预处理与集成建模的关键环节。1. 房价预测里的高维数据为什么特征一多线性回归就翻车Kaggle 房价预测这个比赛很多人第一次跑完线性回归会发现一个反直觉的现象特征从 10 个加到 80 个训练集 RMSE 一路下降但排行榜分数反而变差。这不是玄学是高维数据分类/回归问题的典型症状——特征维度上去之后共线性、噪声特征、量纲差异会同时放大普通最小二乘的系数估计方差急剧膨胀模型在训练集上背答案在测试集上就露馅。这篇笔记锁定的就是这个问题在 Kaggle 房价预测这类高维表格回归任务里怎么从数据清洗、特征工程、模型选型到调参把维度带来的麻烦压下去。适合已经跑过一遍 baseline、但分数卡在中游想往上推的人也适合刚接触高维回归、想知道到底该用岭回归还是 XGBoost的新手。下面按我实际做这个比赛的顺序拆开讲每一步都落到能直接抄的命令和参数上。2. 先把数据读进来高维表格的清洗与缺失值处理2.1 房价数据集的维度陷阱在哪Kaggle 房价预测House Prices - Advanced Regression Techniques的训练集是 1460 行、81 列其中 79 个是解释变量。79 维听起来不算高但里面混了大量类别型特征比如 Neighborhood 有 25 个取值、Exterior1st 有 15 个一旦做 one-hot 编码维度会瞬间膨胀到 200 以上。这就是标题里说的高维——不是原始列多而是编码后特征空间大。高维带来的第一个直接问题是缺失值。这个数据集里 PoolQC、MiscFeature、Alley 的缺失率都超过 90%但它们不是真的没有数据而是这个房子没有泳池/没有杂项设施。如果把缺失当缺失处理会丢掉语义正确做法是按业务含义填。常见做法是PoolQC 缺失填 NoPoolAlley 缺失填 NoAlleyGarage 相关缺失填 0 或 NoGarage。2.2 缺失值填充与异常值处理的可复现代码import pandas as pd import numpy as np train pd.read_csv(train.csv) test pd.read_csv(test.csv) # 按业务含义填充缺失往往代表没有该设施 none_cols [PoolQC, MiscFeature, Alley, Fence, FireplaceQu, GarageType, GarageFinish, GarageQual, GarageCond, BsmtQual, BsmtCond, BsmtExposure, BsmtFinType1, BsmtFinType2] for col in none_cols: train[col] train[col].fillna(None) test[col] test[col].fillna(None) # 数值型缺失车库面积、地下室面积缺失通常意味着没有填 0 zero_cols [GarageArea, GarageCars, BsmtFinSF1, BsmtFinSF2, BsmtUnfSF, TotalBsmtSF, MasVnrArea] for col in zero_cols: train[col] train[col].fillna(0) test[col] test[col].fillna(0) # 少量数值缺失用中位数兜底 train[LotFrontage] train[LotFrontage].fillna(train[LotFrontage].median()) test[LotFrontage] test[LotFrontage].fillna(train[LotFrontage].median()) # 异常值GrLivArea 超过 4000 且价格偏低的点是已知的离群样本 train train[~((train[GrLivArea] 4000) (train[SalePrice] 300000))] print(train.shape)这段代码的逻辑分三层第一层处理类别型缺失用 None 保留没有该设施的语义第二层处理数值型缺失车库和地下室面积填 0 符合实际第三层用中位数兜底 LotFrontage 这种随机缺失。最后剔除 GrLivArea 异常大的两个样本这是这个数据集里公认的离群点不删会拉偏回归系数。参数上要注意中位数用训练集算再应用到测试集不能各自算各自的否则会引入数据泄漏。train.shape打印出来应该是 (1458, 81) 左右如果行数没变说明异常值条件写错了。2.3 目标变量为什么要先取对数SalePrice 的分布是右偏的直接回归会让模型对高价房过度敏感。Kaggle 上主流做法是对 SalePrice 取 log1p预测完再 expm1 还原。这一步对 RMSE 的影响经常有 0.01 以上属于必做项。train[SalePrice] np.log1p(train[SalePrice])取对数后分布接近正态线性模型的残差假设更容易满足树模型对单调变换不敏感但也无害。还原时用np.expm1(pred)不要用np.exp因为 log1p 对应的是 log(1x)。3. 特征工程把 79 列变成模型真正吃得下的特征3.1 类别特征编码one-hot 还是 target encoding高维回归里类别编码方式直接决定维度膨胀程度。Neighborhood 25 个取值one-hot 后变 25 列如果所有类别特征都 one-hot总维度会到 250 以上。维度一高线性模型必须上正则树模型也会因为稀疏分裂而变慢。我的选择是低基数取值少于 10的类别用 one-hot高基数的用 target encoding 或直接按业务合并。比如 Neighborhood 可以按房价中位数分成 3 档而不是保留 25 个独立取值。这样既降维又保留了对价格有区分度的信息。from sklearn.preprocessing import LabelEncoder # 低基数类别one-hot low_card [c for c in train.select_dtypes(object).columns if train[c].nunique() 10] combined pd.concat([train, test], axis0) combined pd.get_dummies(combined, columnslow_card) # 高基数类别LabelEncoder 转成序号交给树模型处理 high_card [c for c in train.select_dtypes(object).columns if train[c].nunique() 10] for col in high_card: le LabelEncoder() combined[col] le.fit_transform(combined[col].astype(str)) print(combined.shape)这里把 train 和 test 拼在一起做编码是为了保证 one-hot 的列对齐。如果分开做测试集出现训练集没有的类别时列数会对不上。LabelEncoder 对高基数类别只是转成整数线性模型会误以为有序所以这类特征我只喂给树模型不喂给岭回归。3.2 构造组合特征与偏度修正房价预测里几个强特征往往是组合出来的TotalSF 地下室 一层 二层面积TotalBath 各卫生间数量加权。这类特征比原始列更能反映房屋整体价值。combined[TotalSF] combined[TotalBsmtSF] combined[1stFlrSF] combined[2ndFlrSF] combined[TotalBath] (combined[FullBath] 0.5 * combined[HalfBath] combined[BsmtFullBath] 0.5 * combined[BsmtHalfBath]) combined[Age] combined[YrSold] - combined[YearBuilt] combined[RemodAge] combined[YrSold] - combined[YearRemodAdd] # 偏度修正对偏度大于 0.75 的数值特征取 log1p from scipy.stats import skew numeric_feats combined.dtypes[combined.dtypes ! object].index skewed combined[numeric_feats].apply(lambda x: skew(x.dropna())) skewed skewed[abs(skewed) 0.75].index combined[skewed] np.log1p(combined[skewed])TotalSF 把三个面积加起来是公认的强特征Age 和 RemodAge 把年份转成房龄比原始年份更有解释力。偏度修正这一步对线性模型提升明显对树模型影响小但做了不亏。注意 log1p 只能用于非负特征如果有负数要先平移。3.3 特征选择高维下怎么砍掉噪声列编码完维度可能到 200 以上不是每一列都有用。我一般用两种方式交叉验证一是看与 SalePrice 的相关系数低于 0.05 的候选剔除二是用 Lasso 回归的系数系数压到 0 的特征直接删。from sklearn.linear_model import Lasso from sklearn.feature_selection import SelectFromModel X combined[:len(train)].drop([SalePrice, Id], axis1, errorsignore) y train[SalePrice].values lasso Lasso(alpha0.001, max_iter10000) lasso.fit(X.fillna(0), y) selector SelectFromModel(lasso, prefitTrue, threshold1e-5) X_selected selector.transform(X.fillna(0)) print(X.shape, X_selected.shape)Lasso 的 alpha 设 0.001 是经验值太大系数全压 0太小起不到选择作用。threshold1e-5 表示系数绝对值小于这个数的特征被删。跑完看维度从 200 降到 120 左右比较正常如果降得太狠说明 alpha 偏大。4. 模型选型岭回归、XGBoost、LightGBM 到底怎么选4.1 线性模型在高维下的正则化选择高维数据配线性模型核心就是正则化。普通最小二乘在特征共线性强时系数方差爆炸岭回归L2把系数往 0 压但不为 0适合特征都有用但相关性强的情况LassoL1会把部分系数压到 0适合做特征选择。房价数据里 TotalSF 和 1stFlrSF 高度相关这种场景岭回归更稳。from sklearn.linear_model import Ridge, Lasso from sklearn.model_selection import cross_val_score X_train X_selected for alpha in [1, 5, 10, 20, 50]: ridge Ridge(alphaalpha) scores cross_val_score(ridge, X_train, y, cv5, scoringneg_root_mean_squared_error) print(falpha{alpha}, RMSE{-scores.mean():.4f})alpha 越大正则越强。房价数据上 alpha 在 10 到 20 之间通常最好RMSE 能到 0.12 左右log 尺度。如果 alpha 加到 50 分数还在涨说明特征共线性极强要考虑先做 PCA 降维。4.2 树模型XGBoost 与 LightGBM 的参数差异树模型在高维表格上通常比线性模型强但参数更多。XGBoost 和 LightGBM 的核心区别在分裂策略XGBoost 用 level-wiseLightGBM 用 leaf-wise后者在高维稀疏数据上更快但更容易过拟合需要控制 num_leaves。import xgboost as xgb import lightgbm as lgb # XGBoost xgb_model xgb.XGBRegressor( n_estimators2000, learning_rate0.01, max_depth4, subsample0.7, colsample_bytree0.6, reg_alpha0.1, reg_lambda1.0, random_state42 ) # LightGBM lgb_model lgb.LGBMRegressor( n_estimators2000, learning_rate0.01, num_leaves16, max_depth4, subsample0.7, colsample_bytree0.6, reg_alpha0.1, reg_lambda1.0, random_state42 )关键参数说明learning_rate 设 0.01 配 n_estimators2000是慢学习率多棵树的经典组合max_depth4 控制树深高维下树太深必过拟合colsample_bytree0.6 表示每棵树只用 60% 特征这是高维数据防过拟合的核心参数reg_alpha 和 reg_lambda 分别是 L1 和 L2 正则。LightGBM 的 num_leaves 要小于 2^max_depth设 16 对应 depth4。4.3 用交叉验证对比三个模型的真实表现单次划分不可靠必须用 K 折交叉验证。我一般用 5 折看 RMSE 均值和标准差。from sklearn.model_selection import KFold import numpy as np def cv_rmse(model, X, y, n_splits5): kf KFold(n_splitsn_splits, shuffleTrue, random_state42) rmses [] for tr_idx, val_idx in kf.split(X): model.fit(X.iloc[tr_idx], y[tr_idx]) pred model.predict(X.iloc[val_idx]) rmses.append(np.sqrt(np.mean((pred - y[val_idx]) ** 2))) return np.mean(rmses), np.std(rmses) for name, model in [(Ridge, Ridge(alpha15)), (XGB, xgb_model), (LGBM, lgb_model)]: mean, std cv_rmse(model, X_selected, y) print(f{name}: RMSE{mean:.4f} ± {std:.4f})跑完通常 LightGBM 略优于 XGBoost岭回归落后 0.01 到 0.02。但岭回归训练快、可解释做 baseline 和特征筛选很合适。三个模型都保留后面做 stacking 融合。5. 避坑与排查高维房价预测里最容易翻车的 5 个点5.1 数据泄漏在划分训练测试前做了全局统计现象交叉验证分数很高提交到 Kaggle 却差一大截。原因在 concat 后的完整数据上算了均值、中位数或 target encoding测试集信息漏进了训练。解决所有依赖目标变量或全局分布的统计必须在每折训练集内单独算。target encoding 尤其危险要用 K 折内编码。5.2 one-hot 后维度爆炸导致内存溢出现象get_dummies 之后 DataFrame 变成几万列内存直接爆。原因高基数类别特征没做处理比如把 Id 或 Neighborhood 全 one-hot。解决先统计每列 nunique超过 10 的用 LabelEncoder 或 target encoding只对低基数做 one-hot。已经爆了的话用pd.get_dummies(..., sparseTrue)转稀疏矩阵。5.3 树模型在 log 尺度上训练但忘了还原现象本地 RMSE 只有 0.12提交后分数 0.3 以上。原因训练时对 SalePrice 取了 log1p预测后没做 expm1 就提交。解决提交前统一np.expm1(pred)并检查预测值范围是否在 5 万到 80 万之间如果都在 0 到 1 之间就是忘了还原。5.4 异常值没删导致回归系数被拉偏现象岭回归系数里 GrLivArea 的系数异常大或符号反了。原因GrLivArea 超过 4000 的两个样本价格却很低把回归线拉偏。解决训练前剔除这两个点或者用 HuberRegressor 这类鲁棒回归。树模型对异常值不敏感但线性模型必须处理。5.5 交叉验证用了 shuffle 但没固定 random_state现象每次跑 CV 分数波动 0.01 以上无法判断调参是否有效。原因KFold 的 shuffle 没设随机种子每折划分不同。解决KFold(n_splits5, shuffleTrue, random_state42)固定种子后再对比不同参数否则调参就是碰运气。6. 进阶技巧用 stacking 融合把分数再推一档单模型调到瓶颈后融合是性价比最高的提分手段。我一般用岭回归、XGBoost、LightGBM 三个模型做 stacking用 5 折 OOF 预测作为第二层的输入。from sklearn.model_selection import KFold from sklearn.linear_model import Ridge def get_oof(model, X, y, X_test, n_splits5): kf KFold(n_splitsn_splits, shuffleTrue, random_state42) oof_train np.zeros(len(X)) oof_test np.zeros(len(X_test)) for tr_idx, val_idx in kf.split(X): model.fit(X.iloc[tr_idx], y[tr_idx]) oof_train[val_idx] model.predict(X.iloc[val_idx]) oof_test model.predict(X_test) / n_splits return oof_train, oof_test X_test_final combined[len(train):].drop([SalePrice, Id], axis1, errorsignore) X_test_final X_test_final[X_selected.columns] oof_ridge, test_ridge get_oof(Ridge(alpha15), X_selected, y, X_test_final) oof_xgb, test_xgb get_oof(xgb_model, X_selected, y, X_test_final) oof_lgb, test_lgb get_oof(lgb_model, X_selected, y, X_test_final) stack_train np.column_stack([oof_ridge, oof_xgb, oof_lgb]) stack_test np.column_stack([test_ridge, test_xgb, test_lgb]) meta Ridge(alpha1.0) meta.fit(stack_train, y) final_pred np.expm1(meta.predict(stack_test))这段代码的核心是 OOFout-of-fold预测每个模型在 5 折里用 4 折训练、1 折预测拼出完整的训练集预测避免第二层过拟合。第二层用 Ridge 而不是线性回归是因为三个基模型预测高度相关需要正则。meta 的 alpha 设 1.0 是保守值如果第二层过拟合可以加大。融合后通常比最好的单模型再降 0.002 到 0.005 RMSE。别小看这点Kaggle 房价预测排行榜上前 100 名和 500 名的差距往往就在 0.005 以内。最后说个我自己的习惯每次提交前一定把预测值还原后打印 describe()看 min 和 max 是否合理。有次我忘了 expm1预测值全在 0.1 到 0.9 之间提交上去分数惨不忍睹这个后悔药我吃过一次就再没犯过。希望帮到你。本文还有配套的精品资源点击获取