ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kaggle房价预测实战:特征工程与模型融合完整指南

Kaggle房价预测实战:特征工程与模型融合完整指南

简介:这份房价预测竞赛代码是一套面向数据科学初学者与竞赛参与者的完整项目源码,聚焦房屋价格预测这一经典回归问题,清晰展示了从数据清洗、特征工程到模型训练与评估的整体流程。压缩包内共有十三个文件,其中七个表格文件分别存放训练集、测试集及提交样例,五个脚本文件实现了随机森林、提升机、堆叠集成和神经网络等多种建模方法,另有一个说明文档介绍项目结构与运行方式,整个压缩包仅有二百七十四千字节,非常轻量。目前已有二百五十一人学习下载,适合希望快速入门数据竞赛或系统提升回归建模能力的开发者。逐行阅读源码,可以掌握缺失值处理、特征构造、交叉验证与参数调优等实操技巧,还可参考多模型融合与堆叠的集成策略,学会针对房价数据设计有效特征并选择合适算法。这套代码提供了从数据到提交结果的一站式方案,是准备面试或参与同类比赛时值得反复钻研的实用参考。

1. Kaggle 房价预测:一份能直接跑通的比赛级源码

如果你参加过房价预测这类回归赛,应该体会过那种「模型跑完、提交分数奇差」的挫败感。这份源码包我拆完第一感受是:它不是为了演示算法,而是把一场真实比赛从数据清洗到模型融合的完整链路都装进了目录里。HousePrice_predict-master下分了random_forest、ensem+boosting、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, columns=cat_cols, dummy_na=False)

dummy_na=False比较容易忽略,它决定缺失值是否单独生成一列。因为前面已经填充了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和ensem+boosting之后,重要性排序通常排在总平方英尺和质量分后面,进前二十没问题。组合特征的目的不是说它们一定比你原特征强,而是给树模型提供更直接的切分维度,减少模型自己去组合深度的消耗。

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_estimators=500、max_depth=15,然后看 OOB 分数。

from sklearn.ensemble import RandomForestRegressor rf = RandomForestRegressor( n_estimators=500, max_depth=15, min_samples_leaf=3, n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train)

这里n_estimators=500是树的数量,我一般先设 500 起步,观察 OOB 是否还有下降趋势再调;min_samples_leaf=3防止叶子节点过细导致过拟合。随机森林在这个项目里定位是基线,把RMSE先打到 0.14 左右,后面集成才能有参照物。如果你的机器性能一般,n_estimators降到 200 也能看到趋势。

4.2 ensem+boosting:XGBoost/LightGBM 的调参重心

ensem+boosting目录下的代码用XGBRegressor或LGBMRegressor单独跑,再用算术平均融合。这是比赛的中坚力量。Boosting 家族的短板在于对噪声敏感,异常值没清干净,分数会明显变差。常见参数组合:learning_rate=0.01、max_depth=4、n_estimators=2000配早停。

import xgboost as xgb xgb_model = xgb.XGBRegressor( learning_rate=0.01, max_depth=4, n_estimators=2000, subsample=0.8, colsample_bytree=0.8, early_stopping_rounds=50, random_state=42 ) xgb_model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)

learning_rate=0.01配合大n_estimators是 GBM 的经典组合,学得慢但更稳;subsample=0.8和colsample_bytree=0.8是每棵树随机用部分样本和部分特征,降低过拟合。early_stopping_rounds=50必须是验证集上做,如果只在训练集上跑到 2000 轮,基本必过拟合。LightGBM 版本一般把learning_rate提到 0.02,num_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_folds=5): oof_pred = np.zeros(len(X)) test_pred = np.zeros(len(X_test)) kf = KFold(n_splits=n_folds, shuffle=True, random_state=42) 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——它用每一折的验证集预测填满整个训练集,相当于每个样本的预测都来自没见过它的模型,这就是 OOF(Out-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做transform

fit_transform只调用一次,之后对验证集、测试集全部用transform。神经网络在这个项目里的实际提升有限,但融合时它和树模型的相关性低,经常能给 Stacking 第二层带来边际增益,所以它不是拿来单打的,是拿来补集成的。

5. 避坑:这份源码最容易翻车的 5 个位置

坑 1:sample_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切。

坑 4:LabelEncoder在类别字段上单独 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(strategy='median')), ('scaler', StandardScaler()) ]), num_features), ('cat', Pipeline([ ('imputer', SimpleImputer(strategy='constant', fill_value='None')), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]), cat_features) ])

ColumnTransformer的num和cat两条管道互不干扰,数值列走中位数填充加标准化,类别列走常量填充加 one-hot。handle_unknown='ignore'是必须的,测试集里如果出现训练集没见过的类别,不会报错而是全 0 向量,这正好应对比赛里测试集类别分布和训练集不完全一样的情况。这套管道在每个 fold 里重复使用,预处理参数永远只来自训练折,从机制上杜绝了泄漏。

至于本地分数和线上分数的差距,我的判断标准是:本地 OOF 分数和线上分数差值在 0.02 以内属于正常波动,超过 0.05 就说明有泄漏或验证划分不当。这个资源包的价值在于它让我对比了四套模型的 OOF 相关性——RF 和 GBM 相关性高,神经网络和树模型相关性低,Stacking 第二层输入里加入 NN 的预测,本地 CV 大约能再降 0.005 左右。从那以后我每次做这类回归赛,都强制走一遍「OOF 预测对齐验证集、提交前按 Id 重排、反向变换最后做」这三步,不再凭本地分数盲提交。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进