
简介这是一套在Kaggle房价预测比赛中打入Top10的Python实现源码面向计算机相关专业学生与机器学习爱好者尤其适合需要完成课程设计、毕业设计或想在数据竞赛中快速上手的读者。压缩包共8个文件以6个Python脚本为主体覆盖数据清洗、并行计算、CatBoost模型训练与结果分析等完整流程另含说明文档与项目介绍整体仅26KB代码量精简、模块边界清晰方便逐一对标学习。源码按功能拆分出数据预处理、特征建模、训练预测等脚本真实呈现了缺失值处理、并行加速、超参数调优等竞赛实操技巧可以帮助学习者完整理解房价预测题的落地思路作者也提供私信答疑与远程指导遇到环境或运行问题可及时求助项目内置说明文档解压后按提示重命名即可顺利跑通。已有105人学习无论是复现Top10方案、完成课设毕设还是做二次开发提升能力这份源码都能提供不错的参考。1. Kaggle房价预测为什么一份Top10源码值得逐行拆解第一次打房价预测比赛的人常以为排名Top10靠的是某个神秘模型。实际拆开一份排名Top10的Kaggle房价预测Python源码你会发现特征工程占了七成篇幅模型只是最后一段。这个比赛的评分指标是均方根误差RMSE每0.001分的差距都对应几百个样本的预测精度。我拿到这类源码时会按“数据清洗→特征衍生→交叉验证→模型集成”的顺序逐段读而不是先跑模型。这篇笔记就照这个顺序把关键代码、参数和坑位放在每个环节里。适合已经会pandas基本操作、想冲一把前排的从业者。2. 数据清洗与特征工程房价预测的胜负手房价预测的数据集不大但脏得很有代表性。训练集不到1500行却有70多个原始特征三分之一存在缺失值。这些缺失不是随机的很多列缺失本身就代表一种状态没有泳池、没有地下室、没有车库。所以第一步不是粗暴删列而是把“缺失”变成可用的信息。2.1 缺失值处理从“能跑”到“有竞争力”的分水岭看数据的第一步永远是统计缺失率。训练集和测试集都要看因为测试集很可能在同样的列上缺得更多。import pandas as pd import numpy as np train pd.read_csv(train.csv) test pd.read_csv(test.csv) # 先看缺失率 missing train.isnull().mean().sort_values(ascendingFalse) missing missing[missing 0] print(missing.head(20))这段代码的作用是快速定位缺失率高的列。排序后前面那些接近0.9以上的列比如泳池、栅栏、巷道的相关字段并不是真的数据丢了而是“没有这个设施”。这类缺失如果直接删掉会丢掉“有没有泳池”这个强信号如果填0又会让线性模型误以为这是一个数值等级。我一般会做一个分类映射把缺失本身作为“None”类别同时对连续型缺失列额外生成一个“是否缺失”的指示列。# 缺失本身有意义的列统一填 None for col in [PoolQC, MiscFeature, Alley, Fence, FireplaceQu, GarageType, GarageFinish, GarageQual, GarageCond, BsmtQual, BsmtCond, BsmtExposure, BsmtFinType1, BsmtFinType2]: train[col] train[col].fillna(None) test[col] test[col].fillna(None) # 连续型缺失列补一个占位值再加缺失标志 for col in [LotFrontage, GarageYrBlt, MasVnrArea]: train[col _NA] train[col].isnull().astype(int) test[col _NA] test[col].isnull().astype(int) train[col] train[col].fillna(-1) test[col] test[col].fillna(-1)这里的关键参数是fillna(-1)而不是fillna(0)。0在某些特征里是有真实含义的比如车库建成年份为0会让人误解成“1900年”-1则明确表示“缺失或不存在”。树模型对单调变化的占位值不敏感但线性模型介意所以后面线性模型应该优先使用中位数填充这一行在复现源码时容易被忽略。提示训练集和测试集要按同一套规则处理。不要拿测试集中位数回填训练集那是泄漏后面专门展开。2.2 特征衍生与编码把分类变量用出花来房价预测里的分类变量很多是有序等级比如ExterQual外部材料质量、KitchenQual厨房质量取值范围是Po、Fa、TA、Gd、Ex。直接用One-Hot会把等级顺序抹掉等于告诉模型“好”和“差”没有任何距离关系。我处理这类有序分类的常见做法是用字典映射成数值。# 有序等级转数值 quality_map {Po: 1, Fa: 2, TA: 3, Gd: 4, Ex: 5} for col in [ExterQual, ExterCond, BsmtQual, BsmtCond, HeatingQC, KitchenQual, FireplaceQu, GarageQual, GarageCond]: train[col] train[col].map(quality_map).fillna(0) test[col] test[col].map(quality_map).fillna(0)fillna(0)在这里表示“没有这项设施”评级从差到好是1到50天然就是None。这样处理以后模型能直接比较“TA”和“Gd”之间的差距而不是把两者拆成互斥的一堆0和1。无顺序的分类变量比如Neighborhood街区、MSZoning区域划分还是One-Hot更稳妥。但这里有个坑One-Hot后训练集和测试集的列数可能不一致。某些低频类别只出现在训练集或测试集直接get_dummies会让最后的矩阵无法对齐。常见做法是先按频次合并稀有类别再加一个dummy_na。# 低频类别合并成 Other def rare_to_other(df, col, thresh10): vc df[col].value_counts() rare vc[vc thresh].index df[col] df[col].replace(rare, Other) for col in [MSZoning, Neighborhood, Condition1, Condition2, SaleType, Exterior1st, Exterior2nd]: rare_to_other(train, col, thresh10) rare_to_other(test, col, thresh10) train pd.get_dummies(train, dummy_naTrue) test pd.get_dummies(test, dummy_naTrue) # 对齐列缺的列补0 train, test train.align(test, joinouter, axis1, fill_value0)thresh10的意思是这个类别在整列里出现小于等于10次就并成Other。阈值太小会留下大量低频列One-Hot后矩阵太稀疏阈值太大又会把有信息的地区类别吞掉。房价比赛里10到20是比较稳的范围。align这一步是保命操作它保证两边列顺序一致多出来的列自动填0避免后续predict时报feature mismatch。除了编码特征衍生才是Top10源码里最花时间的地方。我一般先做面积聚合# 常见面积衍生 train[TotalSF] train[TotalBsmtSF] train[1stFlrSF] train[2ndFlrSF] test[TotalSF] test[TotalBsmtSF] test[1stFlrSF] test[2ndFlrSF] # 总质量分交叉 train[OverallGrade] train[OverallQual] * train[OverallCond] test[OverallGrade] test[OverallQual] * test[OverallCond]这些衍生特征的核心逻辑是原始特征里已经把地下室、一层、二层分开但买家心里装的是“总面积”。把维度合并掉相当于提前帮模型做了特征组合尤其对深度浅的树模型很有帮助。2.3 偏态目标变量为什么先做log1p再谈模型房价SalePrice的分布严重右偏少数豪宅会把RMSE拉得很大。如果直接回归模型会为了压低那几个豪宅的误差牺牲掉普通房屋的精度。所以Top10源码里几乎无一例外地先对目标变量做log变换。train[SalePrice] np.log1p(train[SalePrice])这里用log1p而不是log因为log1p对0值更友好虽然房价没有0但预测值在极端情况下可能接近0逆变换时也更好算。训练结束后再np.expm1回来pred np.expm1(pred)如果你想验证自己有没有漏掉这一步看本地RMSE和提交分数的量级就行。如果本地RMSE在0.1附近提交回来却是1.2那基本就是把log目标当成了原始目标在训练或提交。3. 模型选型与集成Top10背后的预测引擎特征工程之后模型部分反而显得“常规”。拿Top10源码看单模型很少能直接进前排几乎都是“线性模型打底 两个梯度提升树抓非线性 加权或Stacking收尾”。3.1 基模型组合Lasso、Ridge、XGBoost与LightGBM的定位房价数据的特征数量经过One-Hot和衍生后从70多个膨胀到200甚至300个其中大量是0/1变量。这种结构下正则化线性模型依然有存在感它能提供稳定的基线不容易过拟合。from sklearn.linear_model import Ridge, Lasso from sklearn.ensemble import RandomForestRegressor import xgboost as xgb import lightgbm as lgb from sklearn.model_selection import cross_val_score models { ridge: Ridge(alpha20.0), lasso: Lasso(alpha0.005), xgb: xgb.XGBRegressor( learning_rate0.01, n_estimators1000, max_depth4, subsample0.8, colsample_bytree0.8, reg_lambda1.5, random_state42 ), lgb: lgb.LGBMRegressor( learning_rate0.01, n_estimators1000, max_depth5, subsample0.8, colsample_bytree0.8, reg_lambda1.0, random_state42 ) }alpha是正则强度。Ridge的alpha在我这个项目里试过10到5020附近CV分数最稳Lasso的alpha要小得多因为Lasso会用L1惩罚把大量无关系数压成0alpha太大会把整体预测方差都砍掉反而欠拟合。XGBoost和LightGBM的learning_rate0.01很关键它决定每棵树只贡献一点点配合1000棵树能慢慢逼近结构复杂的目标。有了模型字典我习惯先跑一遍5折CV看每个单模型的基础分# X和y已经做过特征工程和log1p for name, model in models.items(): scores cross_val_score(model, X, y, cv5, scoringneg_mean_squared_error) rmse_scores np.sqrt(-scores) print(name, rmse_scores.mean(), rmse_scores.std())neg_mean_squared_error是sklearn里“越大越好”的评分方向所以前面加负号。打印结果通常会发现XGB和LGB差不多Ridge略差但差距不大。这时候不要丢掉Ridge因为线性模型可以作为Stacking里的“老实人”帮最后的融合结果压住方差。3.2 集成策略加权平均与Stacking的取舍Top10源码里最常见的集成不是Stacking而是加权平均。原因很简单数据量小Stacking容易在第二层过拟合加权平均只有一个超参数按CV调起来很直接。# 假设已经分别得到四个模型的预测 pred_final (0.1 * pred_ridge 0.1 * pred_lasso 0.4 * pred_xgb 0.4 * pred_lgb)这组权重不是拍脑袋。先跑出每个模型的CV RMSE然后按“成绩越好权重越大”设初始值再手动微调。0.1/0.1/0.4/0.4的意思是两个树模型平分主要权重两个线性模型各留一点权重做补偿。之所以给线性模型留权重是因为树模型对训练集的极端值更敏感线性模型能在这些样本上给出更保守的估计。如果你有精力Stacking可以再往下走一层。关键是用K折交叉验证产生“袋外预测”作为第二层特征而不是直接拿测试集预测值去训练。from sklearn.model_selection import KFold from sklearn.linear_model import Ridge kf KFold(n_splits5, shuffleTrue, random_state42) oof_xgb np.zeros(len(X)) for tr_idx, va_idx in kf.split(X): model xgb.XGBRegressor( learning_rate0.01, n_estimators1000, max_depth4, subsample0.8, colsample_bytree0.8, reg_lambda1.5, random_state42 ) model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) oof_xgb[va_idx] model.predict(X.iloc[va_idx])这里每一个va_idx上的预测都是模型在没见过的折叠上生成的不会像“先用全量预测再塞进第二层”那样乐观。第二层用Ridge拟合oof_xgb、oof_lgb、oof_ridge这些列时需要把y也传进去。注意第二层的输入特征一旦超过10个在这个数据量下就很容易过拟合所以一般只用5个以内的基模型预测列。3.3 提交格式与预测对齐别在最后一步丢分我见过不止一次模型和特征都做得很漂亮最后提交文件因为Id顺序问题被直接判零。房价比赛的提交只认Id和SalePrice两列但Id顺序必须和原始sample_submission保持一致。sub pd.DataFrame({ Id: test_original[Id], SalePrice: np.expm1(pred_final) }) sub.to_csv(submission.csv, indexFalse)test_original[Id]要取特征工程之前那个test表不能取经过One-Hot和对齐之后的X_test的index。因为在对齐过程中如果发生过dropna或reset_index索引顺序早就变了。np.expm1则是把log目标转回原量纲漏掉这一步分数会差出一个量级。4. 交叉验证与调参让分数稳定在Top10区间很多人以为Top10源码里的模型参数是集中式搜出来的。实际上大多数前排方案是靠一套稳定的交叉验证流程把参数收敛到一个小范围而不是追求全局最优。4.1 交叉验证策略KFold vs KFold(shuffle) 对房价数据的影响房价比赛的数据不存在严格时间排序但如果不shuffle默认KFold会按原始行的顺序切分。数据按某种规律排列时比如相同街区连着出现某一折就可能全是高端房导致每折分数波动很大。from sklearn.model_selection import KFold # 错误的示范没shuffle kf1 KFold(n_splits5) # 正确的示范shuffle 固定种子 kf2 KFold(n_splits5, shuffleTrue, random_state42)shuffleTrue会让每一折的样本分布更接近整体random_state42保证复现。这个看似琐碎的设置直接影响你判断“哪个模型更好”。如果某组参数在非shuffle的KFold上跑出来RMSE 0.118换一个seed后变成0.130那说明这个分数不可信。Top10方案一般会跑3到5个种子取平均RMSE来选参数而不是单次结果定胜负。4.2 关键参数XGBoost/LightGBM的3个必调参数两个梯度提升树家族在房价任务上的最佳参数区间很接近。我梳理成一张对照表方便复制到自己的实验里。参数XGBoostLightGBM作用learning_rate0.0050.020.0050.02学习率越低越稳代价是树更多max_depth3546深度太深容易记住异常房价subsample0.70.90.70.9每棵树抽样比例防过拟合colsample_bytree0.70.90.70.9每棵树用到的特征比例reg_lambda1.03.01.03.0L2正则压制极端预测learning_rate和n_estimators是一对。学习率0.01时树的数量通常在800到2000之间才收敛。如果你把学习率降到0.005别忘同时加大n_estimators。subsample和colsample_bytree对房价这类中等维度数据0.8附近是甜点。reg_lambda默认值是1.0但Top10源码里常会调到1.5或2.0因为房价里少数豪宅样本会带来很大的梯度L2正则能明显把提交分数往上拉。4.3 早停与学习率如何判断模型真的拟合了确定参数后不要直接固定树的数量而是用验证集做早停。XGBoost的原生接口比XGBRegressor包装类更直观import xgboost as xgb d_train xgb.DMatrix(X_train, y_train) d_valid xgb.DMatrix(X_valid, y_valid) params { learning_rate: 0.01, max_depth: 4, subsample: 0.8, colsample_bytree: 0.8, reg_lambda: 1.5, objective: reg:squarederror, eval_metric: rmse } bst xgb.train( params, d_train, num_boost_round5000, evals[(d_valid, valid)], early_stopping_rounds100, verbose_eval100 )num_boost_round5000是最大允许轮数early_stopping_rounds100的意思是连续100轮验证集RMSE没有下降就停止。这样做的好处是树的数量跟着数据走而不是拍脑袋。每次训练完看bst.best_iteration和bst.best_score如果best_iteration在1000轮附近说明学习率和最大深度配得还行如果长期停在100轮左右说明学习率太低或者特征噪音太大。提示早停只做参数检查。正式训练或者生成提交预测时我一般会拿完整训练集重新训练并用早停得到的最佳迭代次数继续训练到那个轮数避免少用一截数据。5. 避坑与排查房价预测实战中的5个典型翻车现场这一节是我反复踩过的坑汇总也是拿到任何前排源码后最值得先对照检查的地方。每一条都按“现象→原因→解决”展开。5.1 现象预测值普遍偏低提交后分数暴涨提交的SalePrice全部比test里的中位数小一大截本地CV分数却挺好。原因训练时对SalePrice做了log1p但预测完没有用np.expm1逆变换回去等于提交了log尺度的房价。log值自然比原始房价小很多RMSE当然爆炸。解决提交前统一执行pred np.expm1(pred)并检查输出分布直接对pred.describe()看最大值和平均值原始房价均值应该在10万到20万美元量级而不是几到几十。5.2 现象交叉验证分数好线上分数差本地5折CV RMSE非常稳定一提交却掉出Top100而且每次提交分数波动很大。原因最常见的是交叉验证没有shuffle或者随机种子没有固定。另外如果特征工程里用了全量数据做标准化/中位数填充测试集的信息已经泄漏到训练过程CV会虚高。解决交叉验证统一使用KFold(n_splits5, shuffleTrue, random_state42)而且特征工程的fit过程只允许在训练折内做。比如中位数填充应该在每一折的训练部分算中位数再填到验证部分而不是全程用一个从全量train算好的数字。虽然这样做代码变麻烦但线上分数会诚实很多。5.3 现象特征里出现泄漏验证分数虚高有一类特征做完后CV RMSE突然从0.12跌到0.05看起来很爽实则危险。原因可能是把目标变量相关的高级统计量直接当特征了。常见误操作是计算某个Neighborhood的平均SalePrice然后作为模型输入或者用整个数据集的缺失率来生成特征。这些操作都让模型在训练时“偷看”了验证集的目标信息。解决如果要使用类别目标编码必须在交叉验证每折内部计算均值而且要加噪声或平滑。房价比赛里我一般尽量不用目标编码因为Neighborhood等类别本身已经可以通过One-Hot被树模型学走。看到CV异常好时先怀疑泄漏而不是庆祝。5.4 现象One-Hot后内存暴涨训练卡死对Exterior1st、Neighborhood等列直接pd.get_dummies后内存占用从几百MB跳到几个GBXGB训练卡到无法结束。原因没有做低频类别合并类别基数过高One-Hot生成大量稀疏列。另一个常见原因是把数值列也误转成了字符串再One-Hot。解决参照2.2的先合并低频类别阈值可以降到5到10。如果还想再省内存用sklearn的OneHotEncoder并开启稀疏输出。实际操作中房价特征做到300列以内是正常的超过600列就要回头检查是不是产生了过多冗余0/1列。5.5 现象跑完代码提交却报“预测值数量不一致”代码不报错但提交文件行数比sample_submission少或者Id顺序对不上。原因特征工程过程中对test做过删除缺失行、reset_index或者align时没有保持原始Id。一旦某行被dropna后面to_csv的长度自然不对。解决在读取原始数据后立刻把Id单独保存所有处理都只围绕特征列最后提交时用test_original[Id]做Key而不是用处理后的DataFrame索引。养成习惯在所有processing函数里不drop行只fill或transform。6. 从Top20到Top10源码之外的四个进阶习惯把特征工程和模型集成跑通后很多人卡在Top20以内进不了Top10。这时候拼的已经不是单点技术而是几个容易被忽略的习惯。第一个习惯是观察残差而不是只盯RMSE。我会把验证集预测值和真实值相减然后按Neighborhood分组看残差。如果某个街区的残差异常大说明模型没学到那个区域的价格规律常见解法是单独给这个街区加一个交互特征比如“该街区是否在新房”的交叉项。只看全局RMSE永远发现不了这种结构化误差。第二个习惯是对预测值做后处理检查。把np.expm1之后的预测结果和真实房价分位数对比如果预测值出现负值说明训练目标或者特征组合出了问题。负房价在这个数据集里本身不应该存在但我见过因为过度使用Ridge且alpha太大导致的负预测。遇到这种情况可以给负值截断到训练集最低价但这只是兜底根治方法还是降低alpha。第三个习惯是固定种子纪律。模型随机种子、交叉验证种子、数据shuffle种子每次实验必须一致。我在做参数对比时会提前定义一组全局变量SEED 42所有可能用到随机的地方都传这个值。否则你今天调lr明天调max_depth结果里混杂了随机性所有结论都是玄学。第四个习惯是提交节奏。不要模型一跑完就提交先在本地跑3个不同种子的CV把分数波动范围记录下来。只有当线上分数落在本地CV均值的±0.005以内时这个模型才值得继续迭代。如果多次提交和本地对齐再开始尝试集成融合。这些习惯听起来不酷但Top10和Top20之间往往就差这零点几个RMSE的稳定性。我自己的经验是每多坚持一个这样的检查提交分数就稳定一档。希望帮到你。本文还有配套的精品资源点击获取