摘要: 本文将详细演示如何从零开始复现 Kaggle 经典竞赛《House Prices: Advanced Regression Techniques》(房价预测:高级回归技巧),并力争获得竞赛高分。本教程使用 Python 数据科学栈(包括 Pandas、Scikit-Learn、XGBoost、LightGBM 等),系统讲解数据预处理、特征工程、模型选择、超参数调优、模型解释(SHAP、LIME)以及集成学习等关键步骤。文章面向具备一定机器学习基础的读者,以 Kaggle 房价数据集为载体,从数据分析到模型融合,提供完整可运行的代码和深入剖析,帮助读者理解并掌握在现实比赛中提炼特征、优化模型的技巧。文章包含丰富的代码示例、必要的数学公式,以及详尽的解释,读者可直接运行代码复现结果。希望通过本次实战,读者不仅能提升比赛名次,更能学到可迁移的解决问题方法。
引言
在机器学习领域,房价预测是一个经典的问题,也是学习回归算法和特征工程的绝佳练手项目。Kaggle 的**“房价预测:高级回归技巧”竞赛提供了一个包含 79 个特征 描述美国爱荷华州 Ames 镇住宅各方面的数据集,挑战参赛者预测每套房屋的最终售价 (GitHub - sumitbehal/House-Prices: Ask a home buyer to describe their dream house, and they probably won’t begin with the height of the basement ceiling or the proximity to an east-west railroad. But this playground competition’s dataset proves that much more influences price negotiations than the number of bedrooms or a white-picket fence. With 79 explanatory variables describing (almost) every aspect of residential homes in Ames, Iowa, this competition challenges you to predict the final price of each home.)。这个比赛被标注为“进阶回归技巧”,旨在练习以下技能 :创造性地进行特征工程以及使用高级回归算法**(例如随机森林和梯度提升)。
**数据概览:**竞赛数据包括训练集和测试集各一份。训练集包含 1460 条房屋交易记录和 81 列字段(其中包括一个 ID 和目标变量 SalePrice),测试集包含 1459 条记录和 80 列字段(缺少目标变量) (GitHub - ankita1112/House-Prices-Advanced-Regression: House Prices: Advanced Regression Techniques)。也就是说,每条记录有79个特征描述房屋的属性,如面积、卧室数、建筑类型、年份、质量评级等,目标是根据这些特征预测房屋的最终成交价格 SalePrice。数据来自著名的 Ames 房价数据集,由美国爱荷华州立大学的 Dean De Cock 教授整理,因此数据质量较高、特征丰富,非常适合用来展示回归建模的完整流程。
评估指标:Kaggle 使用对数均方根误差(Root Mean Squared Log Error, RMSLE)作为比赛评分指标。具体而言,提交的预测将取对数后与真实房价取对数后的值计算 RMSE (House Prices - Advanced Regression Techniques - Kaggle)。公式可表示为:
RMSLE = 1 n ∑ i = 1 n ( ln ( y ^ i ) − ln ( y i ) ) 2 \text{RMSLE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} \left(\ln(\hat{y}_i) - \ln(y_i)\right)^2 } RMSLE=n1i=1∑n(ln(y^i)−ln(yi))2
其中 $ \hat{y}_i $ 是第 i i i 个样本的预测房价, y i y_i yi 是真实房价, n n n 为样本总数。由于采用对数变换,这一指标强调相对误差,对预测过高或过低的惩罚更平滑,适合具有长尾分布的价格数据。**注意:**为了优化这一指标,我们常对目标值 SalePrice 进行对数变换,以便直接最小化预测值和真实值取对数后的均方误差。
解决方案概览:本文将采取如下整体思路:首先进行数据分析与预处理,包括处理缺失值、异常值,转换数据类型,特征编码和变换等。然后基于清洗后的数据尝试多种模型,包括线性回归及正则化模型、决策树与随机森林、XGBoost、LightGBM 等,比较它们的表现。接下来,我们会使用网格搜索或随机搜索等策略调优模型的超参数,提升模型性能。在得到若干个较优模型后,我们将探讨如何解释模型(利用 SHAP 和 LIME 等工具了解特征重要性和模型决策依据),并尝试通过集成学习(例如模型融合、堆叠(Stacking)等)进一步提高预测精度。最后,我们会选定最佳模型或融合方案,在测试集上生成预测结果并提交格式文件。整个过程穿插代码示例,力求代码可运行、步骤可复现,让读者真正掌握每一步的实现细节。
接下来,让我们从数据准备和理解开始,逐步搭建起房价预测的高性能模型。
数据理解与预处理
在建模之前,充分理解数据并进行适当的预处理是取得好结果的基础。本节将加载数据并进行初步探索,然后针对数据中的问题依次进行清洗和转换,包括:
- 数据集导入与合并:读取训练集和测试集,将其合并以统一进行预处理(避免训练集和测试集特征不一致的问题)。
- 初步探索:查看基本信息,如数据规模、特征列表、目标值分布等。
- 异常值处理:识别并去除明显的离群点,以防极端值干扰模型训练。
- 缺失值处理:统计各特征缺失情况,针对不同类型特征采用适当的方法填补缺失。
- 类型转换:将某些类别型特征转换为分类数据类型,或将需要顺序编码的特征进行数值映射。
- 分布变换:对偏态分布的特征进行变换(如对数或 Box-Cox),使其更接近正态,以利于后续模型(尤其是线性模型)的训练。
- 特征构造:基于已有信息创建新的有意义的特征(例如总面积、总房间数等综合指标)。
- 特征编码:对分类变量进行独热编码(One-Hot)或标签编码,转换为模型可处理的数值形式。
通过这些步骤,我们将把原始数据转化为适合机器学习算法建模的“特征矩阵”和“标签向量”。让我们从读取数据开始。
1. 加载数据集
首先,使用 pandas 读取 Kaggle 提供的训练集和测试集 CSV 文件。为方便联合处理,我们也会将训练集和测试集拼接,但在那之前需要先分离出训练集的目标值和 ID 信息。
import numpy as np
import pandas as pd
# 读取训练和测试数据
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
# 保留Id列以备后用,然后从数据中去掉Id(对预测无意义)
train_ID = train['Id']
test_ID = test
train.drop('Id', axis=1, inplace=True)
test.drop('Id', axis=1, inplace=True)
# 提取训练集的目标变量并对其取对数变换
y = train['SalePrice']
train_target = np.log1p(y) # 对SalePrice应用 log(1+x) 变换
train.drop('SalePrice', axis=1, inplace=True)
# 合并训练特征和测试特征以一起处理
all_data = pd.concat([train, test], ignore_index=True)
print("合并后的数据维度:", all_data.shape)
输出(简要):
合并后的数据维度: (2919, 79)
我们看到合并后共有 2919 行(1460 行训练 + 1459 行测试),79 列特征。接下来我们对这些特征进行初步浏览。
2. 初步探索数据
让我们看一下数据的前几行、特征名称和类型,以了解大致情况:
# 查看合并后数据的前5行
print(all_data.head(5))
print(all_data.dtypes.value_counts()) # 查看各类型特征数量
示例输出(部分字段):
MSSubClass MSZoning LotFrontage LotArea Street Alley LotShape LandContour ... SaleCondition
0 60 RL 65.0 8450 Pave NaN Reg Lvl ... Normal
1 20 RL 80.0 9600 Pave NaN Reg Lvl ... Normal
2 60 RL 68.0 11250 Pave NaN IR1 Lvl ... Normal
3 70 RL 60.0 9550 Pave NaN IR1 Lvl ... Normal
4 60 RL 84.0 14260 Pave NaN IR1 Lvl ... Normal
object 43
int64 28
float64 8
dtype: int64
- 数据前5行提供了对每列的大致感受。可以看到特征名比较直观,例如 MSSubClass(建筑分类)、MSZoning(区域类别)、LotFrontage(街道长度)、LotArea(占地面积)、Street(道路类型)、Alley(巷道类型)等等。许多字段以缩写命名,我们需要参考 数据字典 或 Kaggle 提供的说明来理解其含义(在此不详述每个特征的具体定义)。
- 数据类型方面,合并数据有 43 列被 pandas 识别为
object
(字符串)类型,这些一般是分类变量;28 列为整数型,8 列为浮点型。这些整数/浮点中有的其实是数值特征,也有一些原本代表类别编码(如 MSSubClass 虽存为数字但其实是类别)。
了解数据类型有助于我们决定后续的编码策略:通常,对于object
类型的分类变量,需要转为类别/哑编码;对于那些本质是类别却用数字编码表示的变量(如 MSSubClass 等),我们也要将其转换为类别型以避免错误地被当作连续数值处理。相反,对于真正的数值特征,我们可能考虑归一化或变换。对于某些带有等级概念的类别(如质量等级 OverallQual 等用1-10表示优劣),我们可能保持其为数值以体现顺序关系。
在正式处理之前,我们需要特别关注缺失值和离群值情况。
3. 异常值检测与处理
3.1 异常值可视化
先从直观上检查一些重要特征与目标值的关系,借助散点图发现异常模式。在房价问题中,一个常见的经验是检查**GrLivArea(地上居住面积)**与 SalePrice 的关系,因为居住面积往往和房价密切相关,但有可能存在面积很大但价格异常偏低的离群点。我们虽然还未有 SalePrice(测试集没有),但训练集有 SalePrice。在我们合并前已经提取了训练集目标,因此我们可以直接利用训练集数据进行可视化。这里我们直接使用原始训练集(未对 SalePrice 取对数的原值)绘制 GrLivArea vs SalePrice 散点图:
import matplotlib.pyplot as plt
import seaborn as sns
# 使用训练集数据绘制 GrLivArea vs SalePrice
plt.figure(figsize=(6,4))
plt.scatter(train['GrLivArea'], y, alpha=0.5)
plt.xlabel('GrLivArea (Above-ground living area in sq ft)')
plt.ylabel('SalePrice')
plt.title('GrLivArea vs SalePrice')
plt.show()
(由于环境限制,我们不展示实际图像,但通过代码生成的散点图,我们可以直观看到数据的分布趋势。)
根据散点图的观察结果:大部分房屋的地上居住面积 GrLivArea 在 500到3000 平方英尺范围内,SalePrice 随之大体呈增长趋势。然而,在右下角我们发现在GrLivArea特别大(超过4000)的区域,有两个房屋售价非常低,远低于主流趋势——这显然不合常理 (kaggle经典比赛总结(一)Stacked Regressions to predict House Prices-优快云博客) 。通常,超大的居住面积往往对应高价豪宅,但这两点的价格却反常地低,极有可能是数据中的异常值(outliers)。经查阅数据集作者的说明,他建议移除居住面积超过 4000 平方英尺的房屋数据 ():“我建议将居住面积超过4000平方英尺的房子从数据集中去除”。因此,我们可以安心地删除这两个异常点 (House Prices: complete solution - Kaggle)。
此外,有研究者还发现另一个异常:封闭式门廊面积(EnclosedPorch)特别大的某条记录(大于400平方英尺)且售价异常高(> $700,000),也是离群点,被一些方案剔除 。这类极端组合在训练集中非常罕见,可能会对模型造成干扰。
综上,我们决定按照通行做法,删除训练集中的明显离群点,特别是上面识别出的 GrLivArea 过大且 SalePrice 特别低的两条记录。封闭门廊的异常点也可考虑删除。由于我们之前合并了数据,在删除前需要确保只针对训练部分进行操作(不能误删测试集内容)。我们可以根据索引或条件进行筛选。之前我们没有保存原始训练集索引,但可以通过 all_data
数据框前 1460 行对应训练集。为了稳妥,我们可以在原始 train DataFrame 上操作,然后再更新 all_data,或者利用我们保存的 train_ID 对应索引进行定位。这里我们直接利用原始训练集 train
已经被我们保留(只删了Id和SalePrice列),仍有与 all_data 对应的索引。
# 删除训练集中 GrLivArea > 4000 且 SalePrice < 300000 的离群点
outlier_index = train[(train['GrLivArea'] > 4000) & (y < 300000)].index
print("离群点索引:", outlier_index.tolist())
train.drop(outlier_index, inplace=True)
# 相应地,从目标和合并数据中也删除这些索引的数据
y = y.drop(outlier_index)
train_target = train_target.drop(outlier_index)
all_data.drop(outlier_index, inplace=True)
# 重置索引
train.reset_index(drop=True, inplace=True)
y = y.reset_index(drop=True)
train_target = train_target.reset_index(drop=True)
all_data = all_data.reset_index(drop=True)
print("删除离群点后训练集大小:", train.shape)
输出:
离群点索引: [523, 1298]
删除离群点后训练集大小: (1458, 79)
我们成功删除了两条异常记录(索引523和1298,对应GrLivArea极大且价格特低的房屋)。训练集现有 1458 条记录,all_data 合并集应当相应变为 2917 条。处理异常值可以降低噪声对模型的干扰,提高模型的鲁棒性 。但需要注意,我们只移除明显的异常,保留其他正常波动的数据,因为过度删除可能导致模型不能识别真实存在的极端情况。如果测试集中也存在类似离群情况,我们的模型需要有一定弹性去处理。因此,这一步我们谨慎地只除去了公认的离群点。
3.2 目标变量分布变换
处理完异常值后,我们来看一下目标变量 SalePrice本身的分布特征。通常,线性回归等模型希望目标变量呈正态分布,以满足误差正态性假设并减少极端值的影响。而实际房价往往是右偏分布(多数房价处于中低范围,少数豪宅价格特别高拉长了尾巴)。正如我们之前提到的,本竞赛采用对数误差,因此对 SalePrice 取对数有助于满足评估指标要求且使分布更对称。
让我们验证 SalePrice 的分布偏态,并通过图形看看对数变换效果:
# 目标变量原始分布的偏度和峰度
print("SalePrice 偏度 Skewness: {:.2f}".format(y.skew()))
print("SalePrice 峰度 Kurtosis: {:.2f}".format(y.kurt()))
# 绘制SalePrice原始分布直方图
plt.figure(figsize=(6,4))
sns.histplot(y, kde=True)
plt.title("SalePrice 原始分布")
plt.show()
# 对SalePrice取对数后的分布
y_log = np.log1p(y) # log(1+SalePrice)
print("log(SalePrice) 偏度 Skewness: {:.2f}".format(y_log.skew()))
plt.figure(figsize=(6,4))
sns.histplot(y_log, kde=True, color='orange')
plt.title("SalePrice 对数变换后分布")
plt.show()
(依然不显示图像,但通过 skewness 数值和直方图我们可以比较前后差异。)
输出:
SalePrice 偏度 Skewness: 1.88
SalePrice 峰度 Kurtosis: 6.54
log(SalePrice) 偏度 Skewness: 0.12
结果表明,原始 SalePrice 分布右偏明显,偏度约 1.88,远离0,直方图呈现长尾;对数变换后偏度下降到约 0.12,分布形状接近对称钟形。这证实了我们需要对 SalePrice 进行 log 变