Kaggle房价预测实战:特征工程与随机森林集成全解析

发布时间:2026/9/26 13:09:30
Kaggle房价预测实战:特征工程与随机森林集成全解析 简介《Kaggle房价预测比赛代码.zip》是一份面向数据科学初学者与竞赛爱好者的完整参赛源码对应Kaggle经典“房价预测”赛题涵盖从数据读取到结果输出的全流程。压缩包共13个文件主要包含7个csv数据文件提供训练集、测试集和提交样例、5个Python脚本覆盖随机森林、增强集成、堆叠、神经网络等模型方案以及1个Markdown说明文档整体大小约274KB结构清晰便于按模块阅读。目前已有250人学习浏览适合希望借助真实赛题快速上手机器学习流程的读者。代码从数据预处理出发依次展示特征工程、多种模型训练、交叉验证、超参数调优和结果融合等环节尤其侧重随机森林与集成增强的组合策略以及神经网络建模路径。通过研读源码可以理解Kaggle竞赛的标准操作流程包括数据清洗、缺失值处理、标准化、模型评估与提交文件生成等细节是一份能够直接运行并迁移到同类预测任务的实战参考。1. Kaggle房价预测比赛代码先看清这份比赛源码能给你什么再谈复现先说一个反直觉的结论拿到这份 Kaggle 房价预测比赛的源码包最值钱的不是最后一名的排名而是中间那几步“走弯路”的状态。这个 zip 里装的是完整的比赛项目结构input 目录放 train.csv、test.csv、sample_submission.csvsrc 里按随机森林、集成提升、堆叠融合、神经网络几个方向拆开外加一个 README。你要预测的只是 SalePrice 一个数值但真正要学的是面对 79 个特征哪些列要清洗、哪些特征要交叉、树模型和集成怎么配合、最后怎么组织一次不会被判错的提交。它适合两类人一是想完整跑通一场 Kaggle 比赛的初学者照着目录挨个读比看零散教程有效二是已经做过几轮房价预测、想拿别人的工程结构对比自己边界在哪的熟手。这篇笔记按“数据→特征→模型→融合→提交”的顺序把源码里最有肉的几条路径拆开讲。2. 数据预处理先把 train.csv 读透再做目标变换和缺失值兜底这一章的目的是先解决三个影响后面全部模型的问题训练集和测试集行数不一致时特征工程怎么共用一套代码SalePrice 严重右偏时目标变量要不要变换缺失值里面哪些是“缺数据”哪些是“本来就没有”。2.1 载入数据与目标变量分布检查拿到压缩包后我一般先不急着解压直接跑而是先看目录确认数据文件在 input 里然后动手写数据载入。train.csv 有 1460 行 80 列其中一列是 SalePricetest.csv 有 1459 行 79 列sample_submission.csv 只有 Id 和 SalePrice 两列。80 列里数值型和类别型大约各占一半这种规模的数据跑随机森林和 LightGBM单机内存完全不是瓶颈真正的瓶颈在特征构造的思路。第一件事是看 SalePrice 的分布。很多新手图省事直接 load 进去就开训练最后分数上不去又不知道问题在哪。标准做法是先打印描述统计和偏度import pandas as pd import numpy as np train pd.read_csv(input/train.csv) test pd.read_csv(input/test.csv) print(train.shape, test.shape) print(train[SalePrice].describe()) print(skew:, train[SalePrice].skew())输出里 SalePrice 的均值大约在 18 万美金中位数在 16 万左右最大值超过 75 万偏度在 1.8 上下。 提示当偏度超过 1.0 时我一般直接考虑对数变换。这个比赛的评估指标是均方根误差官方用的就是 log1p(SalePrice) 之后的目标你在本地算的 RMSE 要和排行榜对齐就得在同一个变换空间里比较。2.2 目标变量 log 变换与训练测试集合并我习惯把目标变量取出来单独存一份剩下的特征列全部拼到一个 DataFrame 里做特征工程。这样后面做缺失值填充、编码、组合特征时train 和 test 用的是同一套逻辑不会出现“train 填了中位数、test 忘了填”的尴尬。y np.log1p(train[SalePrice]) train train.drop(columns[SalePrice]) full pd.concat([train, test], axis0, ignore_indexTrue) print(full shape:, full.shape) print(full.isnull().sum().sort_values(ascendingFalse).head(20))为什么合并而不是 train、test 各做一套因为特征工程里所有 fit 操作比如中位数、类别编码映射都必须在全量数据上做否则 train 和 test 的分布会不一致。比如某些类别状态只在 test 里出现单独处理时就只能干瞪眼。合并后列名、数据类型、编码字典统一后续交叉验证也顺手。注意 concat 时一定要 ignore_indexTrue否则索引会带着 train 的尾巴后面切折时容易出位置错位。2.3 缺失值分清“缺数据”和“没有这项”合并后打印的缺失统计里PoolQC、MiscFeature、Alley、FireplaceQu 这些列缺失率很高LotFrontage、GarageYrBlt、MasVnrArea 也有不少缺失。这里最容易踩的坑是把所有缺失统一填成中位数或 0结果模型学到错误信号。我一般先确认缺失的含义。在房价比赛这个场景里PoolQC 缺失表示这个房子根本没有游泳池而不是“有游泳池但质量忘了填”Alley 缺失表示没有巷道入口FireplaceQu 缺失多数是没壁炉。处理代码如下num_cols full.select_dtypes(include[int64, float64]).columns cat_cols full.select_dtypes(include[object]).columns for c in num_cols: full[c] full[c].fillna(full[c].median()) for c in cat_cols: full[c] full[c].fillna(None) print(missing after fill:, full.isnull().sum().sum())数值列填中位数而不是均值原因是房价特征里常有极端值均值会被带偏而中位数对树模型分裂影响更稳定。类别列填字符串None为的是让它在编码后变成一个独立的“没有这一类设施”的分组而不是混进某个真实类别。填完后用 full.isnull().sum().sum() 确认没有遗漏这一步我在每个项目里都会跑一遍属于强迫症级别的检查。2.4 把“看着是数字”的列还原成类别这一步是很多人拿到源码包时才反应过来的。MSSubClass 是 20 到 190 的整数但它是房屋类型编码不是尺寸大小当数值列用就会产生“190 比 60 贵”这种无意义关系。MoSold 销售月份、OverallQual 等级同理。处理方式很简单full[MSSubClass] full[MSSubClass].astype(str) full[MoSold] full[MoSold].astype(str) obj_cols full.select_dtypes(include[object]).columns for c in obj_cols: full[c] full[c].fillna(None)先把 MSSubClass 和 MoSold 转成字符串再对全部类别列做一次兜底填充避免有些列里混入 NaN 导致后续编码报错。到这里数据清洗的骨架就出来了。下一步进入特征工程那才是拉开分数差距的地方。3. 特征工程不是堆特征而是构造有物理含义的组合变量特征工程做得好的源码包往往不是特征数量多而是每一列都能说出“为什么”。这个比赛里房价由面积、质量、区位、年份共同决定单独看某个字段很难表达出综合价值组合特征就是把这些维度拼起来。3.1 面积与年龄组合特征我复现这份源码时最先看的就是 src 里特征构建那一段。常见的做法是把一楼、二楼和地下室的面积合并成总居住面积把卫生间按全卫半卫折算成总数再把售出年份和建造年份做差得到房龄。代码大概是这样的full[TotalSF] full[TotalBsmtSF] full[1stFlrSF] full[2ndFlrSF] full[TotalBath] full[FullBath] 0.5 * full[HalfBath] \ full[BsmtFullBath] 0.5 * full[BsmtHalfBath] full[HouseAge] full[YrSold] - full[YearBuilt] full[RemodAge] full[YrSold] - full[YearRemodAdd] full[HasPool] (full[PoolArea] 0).astype(int) full[HasGarage] (full[GarageArea] 0).astype(int)TotalSF 把三个关联面积合成一个主变量树模型虽然能自己找交互但显式给出来会让分裂更早命中关键维度。TotalBath 用 0.5 折算半卫是这类比赛里默认的处理习惯。HouseAge 比直接丢 YearBuilt 和 YrSold 两列更直观而且减少了特征维度。HasPool 和 HasGarage 则是把面积类字段变成“有没有”的开关专门处理大量 0 值面积对模型造成的干扰。3.2 质量等级的序数编码源码里另一块关键内容是质量等级处理。ExterQual、BsmtQual、KitchenQual 这类字段的取值是 Ex、Gd、TA、Fa、Po本身是有序的但 Pandas 读进来是字符串直接 one-hot 会丢掉次序信息。正确的做法是给它们建一张序数映射表qual_dict {None: 0, Po: 1, Fa: 2, TA: 3, Gd: 4, Ex: 5} qual_cols [ExterQual, ExterCond, BsmtQual, BsmtCond, HeatingQC, KitchenQual, FireplaceQu, PoolQC, GarageQual, GarageCond] for c in qual_cols: full[c] full[c].map(qual_dict).fillna(0).astype(int)映射后缺失值天然落在 0也就是“没有该项设施”和前面缺失值填充的逻辑完全一致。这样处理比直接 LabelEncoder 靠谱因为 LabelEncoder 是按字母序给类别编号Fa、TA、Po 的顺序就乱了。对于 Neighborhood、MSZoning 这种没有大小关系的字段我一般保留字符串后续让模型库自己处理或者用频次编码避免 one-hot 把维度撑爆。3.3 特征放到什么程度就该停源码里还有个容易被忽略的细节并不是特征越多越好。我见过有人把 GrLivArea 和 LotArea 做比值又做差值再做乘积最后加了二十几个相关性极高的列树模型的分裂被冗余信息干扰分数反而下降。这个比赛里常见的做法是控制在二十个组合特征以内核心围绕面积、质量、房龄、地段四个维度展开。另外要留意的是特征工程做在 full 上还是做在 train 上结果完全不同。所有基于统计量的特征比如某个类别的 SalePrice 均值编码只能在 train 上计算后映射到 test直接用 full 算就是变相把 test 信息泄漏进训练过程。我一般在编码前把 full 按行号切回 train 和 test 两个部分分别处理段位统计特征其他全局特征才在 full 上做。 注意凡是需要从 y 里学统计量的特征都必须先切分再编码。4. 模型与集成方案random_forest、boosting 与 stacking 怎么配合这个源码包最值得读的是模型目录的命名方式random_forest、ensemboosting、ensem_stacking、Neural Networks 四个方向基本覆盖了树模型和融合的主流打法。单模型在这个比赛里很难进前排集成才是提分主力。4.1 随机森林基线先建立可对比的 RMSE 基准先跑随机森林的原因很简单参数少、收敛快、能快速验证特征工程有没有做错。我把特征矩阵准备好后用五折交叉验证建立基线from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error def run_cv(model, X, y, n_splits5): kf KFold(n_splitsn_splits, shuffleTrue, random_state2024) scores [] for tr_idx, va_idx in kf.split(X): X_tr, X_va X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va y.iloc[tr_idx], y.iloc[va_idx] model.fit(X_tr, y_tr) pred model.predict(X_va) scores.append(np.sqrt(mean_squared_error(y_va, pred))) return np.mean(scores), scores rf RandomForestRegressor( n_estimators400, max_depth12, min_samples_leaf5, random_state42 ) print(RF OOF RMSE:, run_cv(rf, X_train, y_train))这里 X_train 和 y_train 已经是 log 变换后的目标。n_estimators 取 400 是因为这个数据量下 400 棵树的收敛已经稳定再加大收益递减max_depth12 和 min_samples_leaf5 用来防止树过拟合到噪声特征。OOF RMSE 在 0.11 到 0.12 之间算正常如果跑出来低于 0.09我反而会怀疑是不是特征或目标泄漏。4.2 ensemboosting平均集成让单模型分数稳定LightGBM 是这类表格比赛的主力。它和随机森林的差异在于逐个学习残差对特征交互的拟合能力更强。源码里 ensemboosting 目录对应的典型实现是from lightgbm import LGBMRegressor lgb LGBMRegressor( n_estimators1500, learning_rate0.01, num_leaves31, colsample_bytree0.7, subsample0.8, random_state42 ) lgb_score run_cv(lgb, X_train, y_train) print(LGB OOF RMSE:, lgb_score)n_estimators 放到 1500 配 learning_rate0.01是树模型调参里最常见的组合学习率小一点树多一点精度高且不容易过拟合。num_leaves31 对应深度约 5 的二叉树复杂度对这个数据规模比较合适。colsample_bytree0.7 和 subsample0.8 让每棵树只看部分特征和部分样本增加多样性融合时效果更好。平均集成很简单把随机森林和 LightGBM 的预测值按权重相加。我一般先等权试再看 OOF 结果微调权重pred_rf rf.predict(X_val) pred_lgb lgb.predict(X_val) blend 0.3 * pred_rf 0.7 * pred_lgb权重 0.3 和 0.7 不是拍脑袋LightGBM 在 OOF 上通常比随机森林低 0.01 到 0.02 的 RMSE所以给它更高权重。每次调整后都重新打印 OOF对比确认提升。4.3 ensem_stackingOOF 预测作为 meta 模型的输入平均集成只用了预测值的线性组合stacking 则把基模型的 OOF 预测当成特征交给 meta 模型再学一层。源码里 ensem_stacking 目录做的就是这件事。核心代码如下def get_oof(model, X, y, n_splits5): kf KFold(n_splitsn_splits, shuffleTrue, random_state42) oof np.zeros(len(X)) for tr_idx, va_idx in kf.split(X): model.fit(X.iloc[tr_idx], y.iloc[tr_idx]) oof[va_idx] model.predict(X.iloc[va_idx]) return oof rf_oof get_oof(rf, X_train, y_train) lgb_oof get_oof(lgb, X_train, y_train) stack_X np.column_stack([rf_oof, lgb_oof]) meta LGBMRegressor(n_estimators300, learning_rate0.02) meta.fit(stack_X, y_train)关键在于 get_oof 里的 KFold 循环每个折的模型只在自己的训练部分拟合再预测验证部分整个过程不接触验证数据。如果图省事用全量 train 预测再丢给 meta就相当于 meta 模型看到了训练时见过的预测结果交叉验证分数会虚高提交后直接翻车。stacking 的输入是多个基模型的 OOF 预测所以基模型越多样越好。源码里放 Neural Networks 目录也基于这个原因神经网络和树模型的结构差异大融合时能提供树模型没有的视角。常见的做法是对特征做标准化后训练一个两层 MLP效果不一定比树模型好但放进 stack 里通常能再挤一点分数。5. 实战避坑五个让分数翻车的常见问题这一章全部来自真实跑比赛的血泪经验。每一条我都踩过或者看旁边的人踩过列出来省得你再交一遍学费。5.1 坑一log 变换的逆运算忘了做现象本地交叉验证 RMSE 很低提交后却提示格式错误或者分数比预期差很多。原因训练时 y 用的是 np.log1p(SalePrice)预测出来的也是 log 空间的值直接写到 sample_submission.csv数值会整体偏小和真实房价差一个指数级。解决提交前必须对预测值做逆变换pred_original np.expm1(pred_log)。 提示我每次写完提交脚本都会先打印预测值的 min、max、mean如果均值在 10 万到 30 万之间说明逆变换做对了均值只有几或者几十肯定是漏了 expm1。5.2 坑二预处理在交叉验证外面做完造成数据泄漏现象五折交叉验证 RMSE 低到离谱比如 0.05 以下但提交分数和本地对不上。原因在切分前就对整个 X 做了中位数填充、类别编码、标准化验证集的信息在训练时就已见到。树模型对填充不敏感真正泄漏的是统计类特征和标准化时用的均值方差。解决把预处理拆成两个阶段。全局的缺失值填充可以在 full 上做但所有基于 y 或基于全量 X 统计量的编码必须放到 KFold 循环内部每次只对训练部分 fit再 transform 验证部分。5.3 坑三类别缺失值统一填 0 或填众数现象模型对 PoolQC、FireplaceQu 等列的缺失值响应异常特征重要性里出现一堆不该出现的列。原因这些缺失在后端对应“没有这项设施”填 0 在数值上等于 Po 级填众数则把所有没游泳池的房子归进了最常见的质量等级语义全乱。解决按列语义区分处理。表示“有没有”的设施类字段缺失填None并编码成独立 class真正属于数据采集遗漏的数值字段比如 LotFrontage才用中位数填充。动手前花五分钟翻一下 data_description.txt比盲目 fillna 值钱得多。5.4 坑四年份和 MSSubClass 被当成数值特征现象特征重要性前十里出现 YearBuilt、MoSold但模型预测在部分区间上系统性偏低。原因把年份当连续数值模型会学出“年份越大房价越高”的线性关系而实际上有些年份的建材或风格反而影响房价MSSubClass 是 20 到 190 的房屋类型编码数值大小完全无意义。解决MSSubClass 和 MoSold 一律转成字符串类别列YearBuilt 和 YearRemodAdd 保留数值但配合 HouseAge 一起使用让模型同时看到绝对年份和相对房龄两个视角。5.5 坑五反复提交只看公共排行榜现象公共榜分数一直小幅波动最终私有榜出来时排名大幅下滑。原因公共榜只包含一部分测试数据反复根据公共榜调参等于在拿那部分数据做验证投票多了就会过拟合到公共榜噪声上。解决把本地 OOF 当作主要判断依据公共榜只用来确认提交格式和数据 pipeline 是否正常。同一份特征和模型OOF 和公共榜分数相差不大说明泛化正常相差太大就要回头查泄漏。 注意不要为了公共榜好看去专门调整某些样本的预测那是最典型的自欺欺人。6. 提交环节从 CV 到合格提交的习惯最后的提交不只是一个 to_csv 的事情它决定了前面所有工作能不能被正确评估。我见过太多人模型跑得很好最后因为 Id 顺序错位或者列名不匹配被判零分。这里把我固定使用的验证流程写出来你可以直接照着用。第一步确认 sample_submission.csv 的格式。比赛要求只有 Id 和 SalePrice 两列Id 顺序必须和 test.csv 完全一致SalePrice 是原始房价而不是 log 值。源码包里的 sample_submission.csv 可以直接作为模板读进来后替换 SalePrice 列保留 Id 列原顺序sub pd.read_csv(input/sample_submission.csv) sub[SalePrice] np.expm1(pred_final) sub.to_csv(submission.csv, indexFalse)第二步做三个硬性检查。第一len(sub) 必须等于 len(test)多一行少一行都会被判错第二sub[Id].values 与 test[Id].values 完全一致用 (sub[Id].values test[Id].values).all() 验证第三预测值的范围合理np.expm1 之后最大值在几十万美金量级。第三步记录这一版的参数和 OOF 分数。我一般会在提交目录里放一个 submission_notes.txt写清模型、特征版本、OOF RMSE、公共榜分数、提交时间。比赛进行到后期会有几十个提交版本没有记录根本分不清哪版是哪版。第四步关于融合的最终选择。stacking 和简单平均之间我最后的习惯是看 OOF 稳定性把同一个融合方案用三个不同 random_seed 各跑一遍如果三次 OOF 的标准差小于 0.002我才放心用它做最终提交。标准差大说明融合对随机性敏感需要回到基模型层面找原因。源码包里 ensem_stacking 和 ensemboosting 两个目录对应就是这两条路线前者上限高但风险大后者稳定但提升有限。从那以后我每次跑比赛都会强制自己走一遍这套流程解压先看目录结构数据先查偏度和缺失语义特征统计类编码必须在折内做提交前必查 Id 顺序和预测值量纲。这套习惯让我的翻车率降了很多也希望帮到你。本文还有配套的精品资源点击获取