Kaggle房价预测实战:特征工程与模型融合完整指南
简介这份房价预测竞赛代码是一套面向数据科学初学者与竞赛参与者的完整项目源码聚焦房屋价格预测这一经典回归问题清晰展示了从数据清洗、特征工程到模型训练与评估的整体流程。压缩包内共有十三个文件其中七个表格文件分别存放训练集、测试集及提交样例五个脚本文件实现了随机森林、提升机、堆叠集成和神经网络等多种建模方法另有一个说明文档介绍项目结构与运行方式整个压缩包仅有二百七十四千字节非常轻量。目前已有二百五十一人学习下载适合希望快速入门数据竞赛或系统提升回归建模能力的开发者。逐行阅读源码可以掌握缺失值处理、特征构造、交叉验证与参数调优等实操技巧还可参考多模型融合与堆叠的集成策略学会针对房价数据设计有效特征并选择合适算法。这套代码提供了从数据到提交结果的一站式方案是准备面试或参与同类比赛时值得反复钻研的实用参考。1. Kaggle 房价预测一份能直接跑通的比赛级源码如果你参加过房价预测这类回归赛应该体会过那种「模型跑完、提交分数奇差」的挫败感。这份源码包我拆完第一感受是它不是为了演示算法而是把一场真实比赛从数据清洗到模型融合的完整链路都装进了目录里。HousePrice_predict-master下分了random_forest、ensemboosting、ensem_stacking、Neural Networks四个模型目录加上input/下的train.csv、test.csv、sample_submission.csv和一份README.md结构清楚得不像比赛代码更像一份工程模板。对正准备入门 Kaggle 或者想复盘特征工程和模型融合的人来说这份资源最大的价值不是某个单模型指标而是它把「数据预处理 → 特征工程 → 多模型对比 → 集成 → 提交」的完整链路摆在你面前你能看到参赛者每一步做了什么决定、为什么这么选。接下来我从工程落地的角度逐个目录拆开讲并把最容易翻车的位置标出来。2. 数据预处理把 79 个特征理顺比调参重要十倍2.1 缺失值处理数值型、类别型分开填但别忽略「缺失本身」打开train.csv你会看到 1460 行乘 81 列的典型结构其中SalePrice是目标列。缺失值最严重的集中在LotFrontage、Alley、PoolQC、Fence、MiscFeature这些字段上。常见做法是分两类处理数值型字段用中位数填充类别型字段填充为None字符串再单独保留一列xxx_missing标记是否缺失。# 数值型中位数填充 缺失标记 num_cols df.select_dtypes(include[int64, float64]).columns for col in num_cols: if df[col].isnull().sum() 0: df[col _missing] df[col].isnull().astype(int) df[col] df[col].fillna(df[col].median()) # 类别型填充 None同样留标记 cat_cols df.select_dtypes(include[object]).columns for col in cat_cols: if df[col].isnull().sum() 0: df[col] df[col].fillna(None)这段逻辑有两个地方值得抄第一_missing标记列把缺失当成一种信息而不是简单抹掉。第二类别型统一填None等于告诉模型「这个房子没有泳池/没有围栏」而不是用众数猜一个错误答案。我都用中位数而非均值因为房价相关字段偏态严重中位数对异常值不敏感。注意train.csv和test.csv要放在一起做同样的填充单独处理会出现特征分布不一致后面模型分数会很怪。2.2 偏态分布对数和 Box-Cox 变换为什么是必选项SalePrice的分布明显右偏长尾拖得很远。而这份源码的评估标准是 RMSE均方根误差如果直接对原始价格建模模型会把大量精力花在拟合那几个 50 万美元以上的豪宅普通房子的误差反而没人管。常见做法是log1p变换把偏态压回来。import numpy as np train[SalePrice] np.log1p(train[SalePrice]) # 预测时用 np.expm1(pred) 还原做完变换后目标分布接近正态RMSE 的数值其实对标了 Kaggle 官方常用的 RMSLE均方根对数误差。对GrLivArea这类面积字段我一般也会看分布如果有明显偏态就对数值型特征加np.log1p但不建议 79 列全做那会把稀疏特征压得太平。2.3 数据类型转换one-hot 之前先统一类型原始表里MSZoning、KitchenQual这类是纯字符串但YearBuilt、MoSold虽然是数字语义上是类别而不是连续量。直接用get_dummies会把年份炸成上百列意义不大。我在这份源码里看到类似的处理逻辑时想到的第一步是年份字段转成int保留月份MoSold转类别。# 语义类别字段转 object避免被当连续量 for col in [MSZoning, KitchenQual, MoSold]: df[col] df[col].astype(str) # 数值型与类别型分别 one-hot df pd.get_dummies(df, columnscat_cols, dummy_naFalse)dummy_naFalse比较容易忽略它决定缺失值是否单独生成一列。因为前面已经填充了None这里不用再生成缺失哑变量。转换完的特征维度大约在 250~300 之间配合后面的模型能直接吃。3. 特征工程从原始字段到能放进模型的组合特征3.1 原始字段分组理解每个特征背后的物理意义房价特征是有强先验的。把 79 列拆成几组就清楚该组合什么了面积组GrLivArea、TotalBsmtSF、1stFlrSF、2ndFlrSF、LotArea、质量评分组OverallQual、OverallCond、ExterQual、年份组YearBuilt、YearRemodAdd、YrSold、地下室与车库组BsmtUnfSF、GarageCars、GarageArea。做特征工程前先把列归组能避免盲目组合产生的噪声特征。3.2 组合特征总平方英尺、房龄、质量分总量代码包里src目录下的预处理脚本一般会构造出这类新特征。我拆这份源码时比较认可的一组组合是TotalSF TotalBsmtSF 1stFlrSF 2ndFlrSF直接反映房子可用总面积HouseAge YrSold - YearBuilt反映房龄TotalQual OverallQual * OverallCond把材料质量和当前状况相乘。这些组合不是拍脑袋而是贴合「买家看房时会问什么」来设计的。新特征名构造公式业务含义TotalSFTotalBsmtSF 1stFlrSF 2ndFlrSF可用总面积HouseAgeYrSold - YearBuilt出售时房龄TotalQualOverallQual × OverallCond质量与现状综合分TotalBathFullBath 0.5×HalfBath BsmtFullBath×0.5折算卫生间数PorchSFOpenPorchSF EnclosedPorch 3SsnPorch WoodDeckSF户外空间合计这些特征放进random_forest和ensemboosting之后重要性排序通常排在总平方英尺和质量分后面进前二十没问题。组合特征的目的不是说它们一定比你原特征强而是给树模型提供更直接的切分维度减少模型自己去组合深度的消耗。3.3 异常值不删几个点模型会被带偏GrLivArea和SalePrice上有几个典型异常大面积但低价售出的样本通常是数据录入错误或特殊交易。这类点在散点图上极其扎眼——面积 4000 平方英尺、价格却不到 20 万美元和同面积区间价格差了 2~3 倍。# 剔除面积 4000 且价格 300000 的异常样本 train train.drop(train[(train[GrLivArea] 4000) (train[SalePrice] 300000)].index)这里的阈值不是固定规则我看分布后通常会先画散点图确认再定。注意做训练集划分前剔除不要在交叉验证折内剔除否则每折样本量不一致验证分数会有轻微偏差。这种异常不剔除线性模型和随机森林都会被迫拟合那几个点后期融合时 stacking 的第二层也会吸收这个偏差。4. 模型选型与融合RF、GBM、Stacking、神经网络四个目录各解决什么4.1 随机森林那份「基线模型」的定位random_forest目录下的代码是整份资源里最容易读懂的。随机森林对异常值和缺失值相对鲁棒缺点是外推能力弱——训练集价格范围的边界外预测会趋向均值。常见做法是设n_estimators500、max_depth15然后看 OOB 分数。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators500, max_depth15, min_samples_leaf3, n_jobs-1, random_state42 ) rf.fit(X_train, y_train)这里n_estimators500是树的数量我一般先设 500 起步观察 OOB 是否还有下降趋势再调min_samples_leaf3防止叶子节点过细导致过拟合。随机森林在这个项目里定位是基线把RMSE先打到 0.14 左右后面集成才能有参照物。如果你的机器性能一般n_estimators降到 200 也能看到趋势。4.2 ensemboostingXGBoost/LightGBM 的调参重心ensemboosting目录下的代码用XGBRegressor或LGBMRegressor单独跑再用算术平均融合。这是比赛的中坚力量。Boosting 家族的短板在于对噪声敏感异常值没清干净分数会明显变差。常见参数组合learning_rate0.01、max_depth4、n_estimators2000配早停。import xgboost as xgb xgb_model xgb.XGBRegressor( learning_rate0.01, max_depth4, n_estimators2000, subsample0.8, colsample_bytree0.8, early_stopping_rounds50, random_state42 ) xgb_model.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse)learning_rate0.01配合大n_estimators是 GBM 的经典组合学得慢但更稳subsample0.8和colsample_bytree0.8是每棵树随机用部分样本和部分特征降低过拟合。early_stopping_rounds50必须是验证集上做如果只在训练集上跑到 2000 轮基本必过拟合。LightGBM 版本一般把learning_rate提到 0.02num_leaves控制在 40 以内效果接近但速度更快。4.3 ensem_stacking第二层模型的工作方式ensem_stacking目录是这份源码包含金量最高的部分。Stacking 的思路不是把几个模型的预测结果直接平均而是把第一层模型的输出作为特征喂给第二层模型。这里最常见的坑是数据泄漏如果第一层模型在训练集上直接预测并用来训练第二层第二层看到的都是开卷答案。from sklearn.model_selection import KFold import numpy as np def stacking_pred(model, X, y, X_test, n_folds5): oof_pred np.zeros(len(X)) test_pred np.zeros(len(X_test)) kf KFold(n_splitsn_folds, shuffleTrue, random_state42) for fold, (tr_idx, val_idx) in enumerate(kf.split(X)): model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) oof_pred[val_idx] model.predict(X.iloc[val_idx]) test_pred model.predict(X_test) / n_folds return oof_pred, test_pred这段逻辑重点关注oof_pred——它用每一折的验证集预测填满整个训练集相当于每个样本的预测都来自没见过它的模型这就是 OOFOut-of-Fold预测。第二层模型拿各模型的oof_pred当特征测试集拿各模型的test_pred平均结果当特征两边分布才一致。我见过不少人直接在训练集上fit再predict第二层 RMSE 虚低提交直接翻车。4.4 Neural Networks入行比赛里最容易过拟合的一块Neural Networks目录下的代码通常是一个简单的 MLP特征先标准化到均值为 0 方差为 1。房价预测这种 1460 行的数据量神经网络容易过拟合常见做法是加 Dropout 和早停。标准化必须只在训练集上算均值方差否则测试集信息会泄漏。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_val_s scaler.transform(X_val) # 注意对验证集和测试集都用训练集的scaler做transformfit_transform只调用一次之后对验证集、测试集全部用transform。神经网络在这个项目里的实际提升有限但融合时它和树模型的相关性低经常能给 Stacking 第二层带来边际增益所以它不是拿来单打的是拿来补集成的。5. 避坑这份源码最容易翻车的 5 个位置坑 1sample_submission.csv的顺序被改乱了。现象提交后分数异常高本地验证却很正常。原因很多人做特征工程时对测试集reset_index过提交文件的Id顺序和原始sample_submission对不上。解决提交前强制按sample_submission.csv的Id排序对齐submission pd.DataFrame({Id: test[Id], SalePrice: pred}) submission submission.set_index(Id).reindex( sample_submission.set_index(Id).index).reset_index()坑 2对SalePrice做了log1p忘了反向expm1。现象提交价格整体偏低一个数量级。原因训练时目标是np.log1p(train[SalePrice])预测出来还是对数需要np.expm1(pred)还原。解决在生成提交文件前的最后一步统一做一次np.expm1并在代码注释里写明单位是美元。坑 3训练集和测试集拼接后忘记记录索引位置。现象模型能跑但验证分数忽高忽低。原因把train和test合在一起做 one-hot 后没有保存原始各自的索引随机打乱后切分错误一部分训练样本混进了测试集。解决拼接前先记录n_train len(train)切分时用[:n_train]和[n_train:]不要靠train和test原来的index切。坑 4LabelEncoder在类别字段上单独 fit 导致类别编号不一致。现象训练集里MSZoning有 A、B、C测试集里多了 A、B、C、D编码后同一字母变成不同数字。原因对训练集和测试集分别做了fit_transform。解决先拼接两个数据集fit_transform之后再切回如果类别差异太大就用pd.Categorical统一类别再编码。坑 5验证集划分用随机 KFold分数虚高。现象本地 CV 分数漂亮线上分数差一大截。原因房价数据里同一街区、同一建造年份的样本高度相关随机划分让训练集里混进了验证集的「邻居」相当于泄题。解决按Neighborhood或YearBuilt分层划分验证集或者直接用GroupKFold按小区分组保证验证集里的房屋是训练集完全没见过的区域。6. 验证与提交本地分数和线上分数的差距控制技巧这里分享一个我拆完这份源码后一直沿用的习惯把所有模型放进同一个预处理管道里用KFold生成 OOF 预测再做第二层集成而不是每个模型单独划分训练集。具体做法是写一个Pipeline把缺失值填充、log1p、one-hot、标准化全部串进去每个 fold 里只用训练折的数据做fit验证折和测试折只做transform。from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.impute import SimpleImputer from sklearn.preprocessing import OneHotEncoder, StandardScaler preprocessor ColumnTransformer([ (num, Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()) ]), num_features), (cat, Pipeline([ (imputer, SimpleImputer(strategyconstant, fill_valueNone)), (onehot, OneHotEncoder(handle_unknownignore)) ]), cat_features) ])ColumnTransformer的num和cat两条管道互不干扰数值列走中位数填充加标准化类别列走常量填充加 one-hot。handle_unknownignore是必须的测试集里如果出现训练集没见过的类别不会报错而是全 0 向量这正好应对比赛里测试集类别分布和训练集不完全一样的情况。这套管道在每个 fold 里重复使用预处理参数永远只来自训练折从机制上杜绝了泄漏。至于本地分数和线上分数的差距我的判断标准是本地 OOF 分数和线上分数差值在 0.02 以内属于正常波动超过 0.05 就说明有泄漏或验证划分不当。这个资源包的价值在于它让我对比了四套模型的 OOF 相关性——RF 和 GBM 相关性高神经网络和树模型相关性低Stacking 第二层输入里加入 NN 的预测本地 CV 大约能再降 0.005 左右。从那以后我每次做这类回归赛都强制走一遍「OOF 预测对齐验证集、提交前按 Id 重排、反向变换最后做」这三步不再凭本地分数盲提交。希望帮到你。本文还有配套的精品资源点击获取