Bootstrap

(3万字深度解析)Kaggle《房价预测:高级回归技巧》详解复现攻略

摘要: 本文将详细演示如何从零开始复现 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=1n(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-CSDN博客) 。通常,超大的居住面积往往对应高价豪宅,但这两点的价格却反常地低,极有可能是数据中的异常值(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 变换的必要性 。我们已经在前面读取数据时用 train_target = np.log1p(y) 实现了这一变换。后续训练模型我们都会使用对数后的房价作为标签值进行拟合,最终预测结果再通过指数还原。

**提示:**在 Kaggle 竞赛提交时,提交的是预测的 SalePrice原始值。因此,我们最后会对模型输出的对数预测值取 expm1(即 e x − 1 e^x - 1 ex1)反变换回实际价格。采用对数变换不仅是为了评估指标,也是常见处理偏态目标的一种策略。

3.3 缺失值分析

现在,重点来到缺失值。房屋数据集拥有丰富的字段,难免有缺失。一些缺失可能有特殊含义(比如“没有此项设施”),我们需要合理填补。首先统计每个特征的缺失数量:

# 统计缺失值
total_missing = all_data.isnull().sum().sort_values(ascending=False)
percent_missing = (total_missing / all_data.shape[0] * 100).sort_values(ascending=False)
missing_df = pd.DataFrame({'缺失数量': total_missing, '缺失率(%)': percent_missing})
print(missing_df.head(20))

输出(前20个缺失最严重的特征):

              缺失数量  缺失率(%)
PoolQC          2909   99.7
MiscFeature     2814   96.5
Alley           2721   93.3
Fence           2348   80.5
FireplaceQu     1420   48.7
LotFrontage      486   16.7
GarageCond       159    5.4
GarageType       157    5.4
GarageYrBlt      157    5.4
GarageQual       157    5.4
GarageFinish     157    5.4
BsmtExposure      82    2.8
BsmtCond          82    2.8
BsmtQual          81    2.8
BsmtFinType2      80    2.7
BsmtFinType1      79    2.7
MasVnrType        23    0.8
MasVnrArea        23    0.8
MSZoning           4    0.1
Utilities          2    0.07

可以看出:泳池质量(PoolQC)、杂项特征(MiscFeature)、巷道类型(Alley)这三个字段缺失率超过90%,绝大多数房屋没有这些记录。栅栏(Fence)缺失也高达80%。壁炉质量(FireplaceQu)缺失接近一半。然后是 LotFrontage(临街长度)16.7%缺失。车库相关的一系列字段(车库条件GarageCond、车库类型GarageType、建造年GarageYrBlt、车库质量GarageQual、车库完工状态GarageFinish)各缺失约5.4%,显然是同一批房屋没有车库导致的齐缺。地下室相关(BsmtExposure等)缺失约2.7%,也是无地下室的缺失模式。外墙砖饰类型(MasVnrType)及面积缺失0.8%,说明有少量房屋无外墙砖饰。Zoning、Utilities 等缺失极少,可以视为偶然缺漏。

根据上述,制定缺失值填补策略:

  • 高缺失率特征:对于缺失率极高的特征,如 PoolQC (泳池质量99.7%缺失), MiscFeature (96.5%), Alley (93.3%),几乎绝大多数房屋没有这些属性。这些特征带来的信息量很低,许多模型也无法有效利用如此稀疏的信息。因此,我们倾向于直接移除这些特征,以减少噪音和维度 。Kaggle 上的方案也普遍这样做 。Utilities字段虽然缺失不多,但经查其取值几乎统一(几乎所有房屋都是 AllPub,只有极少数NoSeWa),基本没有区分度,因此也可以删除 。

  • 表示不存在的空值:一些缺失实际上表示不存在该设施,例如:

    • FireplaceQu(壁炉质量):缺失表示没有壁炉。
    • Fence(栅栏质量):缺失表示没有围栏。
    • GarageType/GarageFinish/GarageQual/GarageCond/GarageYrBlt:一组车库相关特征,缺失表示没有车库。
    • BsmtQual/BsmtCond/BsmtExposure/BsmtFinType1/BsmtFinType2:地下室相关,缺失表示没有地下室。
    • MasVnrType/MasVnrArea(外墙石材类型和面积):缺失大概率表示没有外墙砖石饰面。
    • MiscFeature:缺失表示没有“杂项特征”(如没有凉亭等特别设施)。
    • Alley:缺失表示无后巷通路。

    对于这些本身是分类属性的特征,我们可以用一个新的类别比如 'None''No' 来填充缺失值,以明确表示“无此项” 。例如,Fence 用 ‘None’ 表示无栅栏 ,FireplaceQu 用 ‘None’ 表示无壁炉,等等。这种填补不仅保留了“无此设施”的信息,还能避免把缺失当成真的缺测数据处理。

  • 数值型的缺失:主要是 LotFrontage(临街长度)和 GarageYrBlt(车库建造年) 以及 GarageCars/GarageArea 这些:

    • LotFrontage: 通常与房屋周边环境有关,一个常见做法是用所在街区(neighborhood)的中位数临街长度填充缺失 。因为同一Neighborhood内房屋地块相似,lot frontage可能相近。如果不按街区分组,中位数填充也可,但我们采用更精细的方法:按 Neighborhood 分组计算 LotFrontage 中位值,填补各自缺失。
    • GarageYrBlt: 缺失是无车库,此时也可以填一个特殊值,比如0或忽略年份不适用。或者直接与GarageType一样标None,但Year是数值字段,填0或1970之类无意义值都可,稍后我们可能把没有车库的GarageYrBlt也作为缺失来不使用。可以考虑把 GarageYrBlt 转成类别“有车库/无车库”,但这里简单处理。
    • GarageCars/GarageArea: 若GarageType缺失(无车库),这些数值字段应该为0(车库车位数0,面积0) 。在原数据,缺 GarageCars 恰好对应 GarageType缺,所以填0合理 。
    • BsmtFinSF1/2、BsmtUnfSF、TotalBsmtSF、BsmtFullBath、BsmtHalfBath:如果无地下室(BsmtQual缺失),则这些地下室相关数值也应为0。
    • MasVnrArea: 如果没有外墙砖(MasVnrType None),面积填0。
    • 其他极少量缺失:如 Electrical(电气系统)有1个缺失,可用众数填充 。KitchenQual(厨房质量)有缺失时也可用众数(大部分厨房质量为TA)。SaleType 有缺失时也可用众数填充 。这些特征缺失很少,用最常见值填补是安全的选择 。

总的来说,我们按照数据含义制定填充方案而非盲目使用均值/中位数等统计值,这样更贴合实际 。

下面按照上述策略进行填充:

# 1. 删除不必要的特征
all_data.drop(['PoolQC','MiscFeature','Alley','Utilities'], axis=1, inplace=True)

# 2. 类别型缺失填充为 'None'
for col in ['Fence','FireplaceQu','GarageType','GarageFinish','GarageQual','GarageCond',
            'BsmtQual','BsmtCond','BsmtExposure','BsmtFinType1','BsmtFinType2',
            'MasVnrType']:
    all_data[col] = all_data.fillna('None')

# 3. 数值型缺失
# GarageYrBlt缺失,用0填表示无车库
all_data['GarageYrBlt'] = all_data.fillna(0)
# 车库容量和面积缺失(无车库)填0
for col in ['GarageCars','GarageArea']:
    all_data = all_data.fillna(0)
# 地下室相关缺失填0
for col in ['BsmtFinSF1','BsmtFinSF2','BsmtUnfSF','TotalBsmtSF','BsmtFullBath','BsmtHalfBath']:
    all_data = all_data.fillna(0)
# 外墙砖饰面积缺失填0
all_data['MasVnrArea'] = all_data.fillna(0)

# 4. 基于Neighborhood填充LotFrontage
all_data['LotFrontage'] = all_data.groupby('Neighborhood').transform(
    lambda x: x.fillna(x.median()))

# 5. 少量类别缺失用众数填充
for col in ['Electrical','KitchenQual','Exterior1st','Exterior2nd','SaleType','MSZoning']:
    all_data = all_data.fillna(all_data.mode())

# 再次检查是否还有缺失
print("是否还有缺失:", all_data.isnull().values.any())

解释:

  • 我们删除了 PoolQC、MiscFeature、Alley、Utilities 四列。【注:删除后 all_data 列数从79变75列】。
  • 'None' 字符串填充了Fence等所有前述的类别缺失。
  • 数值型缺失按逻辑填0。
  • LotFrontage按 Neighborhood 分组中位数填充。这里我们使用了 groupby().transform(lambda x: x.fillna(median)) 的技巧,一行代码实现对每个社区缺失值的替换。
  • Electrical、KitchenQual 等用 .mode()(众数)填入。

最后一行输出:

是否还有缺失: False

表示所有缺失值已经处理完毕。

经过这一步,我们解决了数据集中的NA问题,这对于很多机器学习算法(尤其是基于树的模型)非常重要,因为它们无法直接处理空值。

4. 特征类型转换与编码

清洗完数据,现在考虑将特征转换为合适的类型,并对分类变量进行编码。处理策略包括:

  • 将数字表示的分类特征转为字符串:某些特征虽然是数字类型,但实际上是分类编码。例如 MSSubClass(建筑分类)取值如20,30,…表示房屋类型代码,没有大小顺序意义,因此应视为类别。我们将 MSSubClass 转换为字符串形式,以便后续 One-Hot 编码或Label编码。同理可能的还有 MoSold(卖出月份,可视为类别而非数值时间序列)等。

  • 顺序(Ordinal)类别映射:有些分类特征具有明确的顺序等级,我们可以手动定义映射,将等级类别转换为有序数值,以保留次序信息。例如:

    • OverallQual(整体材料和饰面质量)和 OverallCond(整体房屋状况),原本就是1-10的评分,数值已体现顺序,无需变换。
    • ExterQual/ExterCond(外部材料质量/条件)、BsmtQual/BsmtCond(地下室高度质量/条件)、HeatingQC(供暖质量)、KitchenQual(厨房质量)、FireplaceQu(壁炉质量)、GarageQual/GarageCond(车库质量/条件)等,这些特征通常以[‘Ex’,‘Gd’,‘TA’,‘Fa’,‘Po’]等类别表示,代表从优秀(Excellent)到差(Poor)的顺序。我们可以建立映射字典,例如 {‘Ex’:5, ‘Gd’:4, ‘TA’:3, ‘Fa’:2, ‘Po’:1, ‘None’:0},将它们转为数值。这样模型可以直接利用大小关系。
    • BsmtExposure(地下室曝光度)类别 {‘Gd’:良好, ‘Av’:平均, ‘Mn’:最小, ‘No’:无} 也可映射成4,3,2,1顺序,None为0。
    • Functional(房屋功能性评级): [‘Typ’,‘Min1’,‘Min2’,‘Mod’,‘Maj1’,‘Maj2’,‘Sev’,‘Sal’]表示从典型无缺陷到严重功能问题的程度,也有序,可映射为数值。
    • GarageFinish(车库装修程度): {‘Fin’:3, ‘RFn’:2, ‘Unf’:1, ‘None’:0} 表示完成度。
    • PavedDrive(车道铺设): {‘Y’:2, ‘P’:1, ‘N’:0} 表示已铺/部分铺/未铺。
  • 其余无序类别:对没有内在顺序的类别型特征(如Neighborhood, Exterior1st, SaleType等),我们稍后将采用One-Hot编码。

完成这些转换可以减少后续One-Hot编码产生的维度,并让有序信息不丢失。

下面进行类型转换和映射:

# 将MSSubClass等原本是分类的数字特征转换为字符串表示
all_data['MSSubClass'] = all_data.astype(str)
all_data['MoSold'] = all_data.astype(str)
all_data['YrSold'] = all_data.astype(str)
# 注:YrSold年份也可视作类别(年份间无连续意义,只表示售出年份类别)
# 这样处理后,这些特征会被看作分类而非连续数值

# 定义映射字典
qual_map = {'Ex':5, 'Gd':4, 'TA':3, 'Fa':2, 'Po':1, 'None':0}
all_data['ExterQual'] = all_data.map(qual_map).astype(int)
all_data['ExterCond'] = all_data.map(qual_map).astype(int)
all_data['BsmtQual'] = all_data.map(qual_map).astype(int)
all_data['BsmtCond'] = all_data.map(qual_map).astype(int)
all_data['HeatingQC'] = all_data.map(qual_map).astype(int)
all_data['KitchenQual'] = all_data.map(qual_map).astype(int)
all_data['FireplaceQu'] = all_data.map(qual_map).astype(int)
all_data['GarageQual'] = all_data.map(qual_map).astype(int)
all_data['GarageCond'] = all_data.map(qual_map).astype(int)

bsmt_exp_map = {'Gd':4, 'Av':3, 'Mn':2, 'No':1, 'None':0}
all_data['BsmtExposure'] = all_data.map(bsmt_exp_map).astype(int)

func_map = {'Typ':7, 'Min1':6, 'Min2':5, 'Mod':4, 'Maj1':3, 'Maj2':2, 'Sev':1, 'Sal':0}
all_data['Functional'] = all_data.map(func_map).astype(int)

garage_fin_map = {'Fin':3, 'RFn':2, 'Unf':1, 'None':0}
all_data['GarageFinish'] = all_data.map(garage_fin_map).astype(int)

paved_map = {'Y':2, 'P':1, 'N':0}
all_data['PavedDrive'] = all_data.map(paved_map).astype(int)

完成这些映射后,相关特征已变为有序整数。

**说明:**上述映射值的具体数字选择(如Ex=5, … None=0)有一定人为成分,不会明显影响树模型,但对线性模型可能有些许影响(因为线性模型会认为间隔等差)。这里假设等级差异相等。也可以尝试不同评分方案,不过 Kaggle 社区普遍采用类似这种简单映射。

5. 特征分布变换(继续特征工程)

前面我们对目标变量做了对数转换。同样地,某些特征本身分布偏态严重,可能有必要进行对数或 Box-Cox 变换以减小偏度,尤其是对于会被线性模型使用的特征。典型的如 LotArea(土地面积)、GrLivArea(居住面积)、1stFlrSF(一层面积)、TotRmsAbvGrd(房间数)等往往呈长尾分布。对于树模型而言,偏态分布影响不大,但对于线性模型/神经网络而言,正态一点的分布更佳。

我们可以计算所有数值型特征的偏度,找出偏度大于某个阈值的,进行 log1p 变换:

# 找出偏态显著的数值特征并进行对数变换
numeric_feats = all_data.dtypes[all_data.dtypes != "object"].index
skewed_feats = all_data[numeric_feats].apply(lambda x: x.dropna().skew()).sort_values(ascending=False)
high_skew = skewed_feats[skewed_feats > 0.75]  # 偏度绝对值大于0.75认为高偏
print("高偏度的特征数:", high_skew.shape)
print(high_skew.head(10))  # 列举10个最偏的特征

输出:

高偏度的特征数: 10
MiscVal        21.94
PoolArea       17.69
LotArea        13.11
LowQualFinSF   12.10
BsmtFinSF2      4.25
KitchenAbvGr    4.09
BsmtFinSF1      2.64
ScreenPorch     2.13
EnclosedPorch   1.79
1stFlrSF        1.38
dtype: float64

(此处仅列出前10,实际总共10个特征偏度>0.75,根据结果,LotArea, BsmtFinSF1/2, 1stFlrSF, EnclosedPorch等都在其中。)

可以看到一些特征偏度非常高,比如 MiscVal(杂项额外值)21.94, PoolArea泳池面积17.69(绝大部分0),LotArea 13.11 等。对于这些偏态特征,我们进行 log(1+x) 变换使其分布更平滑。需要注意的是,如果原值有0,log1p(0)=0也不会有问题。

skewed_cols = high_skew.index
all_data[skewed_cols] = np.log1p(all_data)

经过这一步,偏态严重的特征已对数化。这对线性模型很重要,对树模型也可能有细微帮助。

6. 构造新特征

在理解数据的基础上,常常可以将现有特征组合,创造出更加直接、有预测力的特征。这在 Kaggle 比赛中属于特征工程的重要部分。针对房价问题,一些有用的新特征包括 :

  • TotalSF(总面积):将地下室面积(TotalBsmtSF)与一二层面积(1stFlrSF + 2ndFlrSF)相加,得到房屋总的可用面积。
  • TotalBathrooms(总浴室):浴室总数可由 FullBath(地上全浴) + 0.5HalfBath(地上半浴) + BsmtFullBath(地下全浴) + 0.5BsmtHalfBath 计算。这样把地上地下的浴室加总,半浴算一半。
  • TotalPorchSF(总门廊面积):将各类门廊面积相加,例如 OpenPorchSF + EnclosedPorch + 3SsnPorch + ScreenPorch。
  • TotalRooms(总房间数):可以将 TotRmsAbvGrd(地上房间数)与地下室房间数相加(如果有的话)。不过地下室房间数据未直接给,可以用地下室各面积/功能推断,这里略。
  • HouseAge(房龄):YrSold - YearBuilt,出售时距离建造多少年。房龄可能影响价格(新的房子通常更贵)。
  • RemodAge(改造年龄):YrSold - YearRemodAdd,出售时距离上次翻新多少年。这个可以反映房屋现代程度。
  • IsRemodeled(是否翻新过):YearRemodAdd != YearBuilt,可作为0/1特征,指示房屋曾经翻新。翻新过的房屋可能品质提升。
  • HasPool/2ndFloor/Garage:是否有游泳池、是否有二层、是否有车库等布尔特征。其实这些信息已有隐含,但提取出来也许对某些模型更直接。比如 PoolArea > 0 则 HasPool=1,否则0。同理 If 2ndFlrSF > 0 then Has2ndFloor=1。

我们来创建一些上述特征:

# Total square footage of house (Total Basement + 1st + 2nd floors)
all_data['TotalSF'] = all_data['TotalBsmtSF'] + all_data['1stFlrSF'] + all_data['2ndFlrSF']

# Total Bathrooms
all_data['TotalBathrooms'] = (all_data['FullBath'] + 0.5*all_data['HalfBath'] + 
                              all_data['BsmtFullBath'] + 0.5*all_data['BsmtHalfBath'])

# Total Porch SF
all_data['TotalPorchSF'] = (all_data['OpenPorchSF'] + all_data['EnclosedPorch'] + 
                             all_data['3SsnPorch'] + all_data['ScreenPorch'])

# House Age
all_data['HouseAge'] = all_data.astype(int) - all_data['YearBuilt'].astype(int)
# Remod Age
all_data['RemodAge'] = all_data.astype(int) - all_data['YearRemodAdd'].astype(int)
# Is Remodeled
all_data['Remodeled'] = (all_data != all_data).astype(int)

# Booleans: Has Pool, 2nd Floor, Garage, Basement
all_data['HasPool'] = (all_data['PoolArea'] > 0).astype(int)
all_data['Has2ndFloor'] = (all_data > 0).astype(int)
all_data['HasGarage'] = (all_data['GarageArea'] > 0).astype(int)
all_data['HasBsmt'] = (all_data > 0).astype(int)

添加这些衍生变量后,我们进一步丰富了特征集。这些新特征有些是连续值,有些是0/1指示器。实践中,这些特征往往能提高模型预测能力,因为它们直接捕获了一些我们推测重要的因素,比如房屋总体规模、浴室总数、房龄等,而原始特征可能分散在多个字段。

7. 特征编码(One-Hot Encoding)

经过以上处理,我们现在拥有两类特征:

  • 数值特征(包含原始连续值、有序编码后的、衍生的连续值)。
  • 类别特征(无序分类,如 Neighborhood, HouseStyle, SaleType 等)。

对于多数机器学习模型,我们需要将类别变量转换成数值形式才能训练。树模型(如XGBoost, LightGBM)其实能够直接接受类别型特征(通过编码为 int 等),但它们会把不同整数当作有序关系,这对无序分类是不恰当的。因此,我们采用独热编码(One-Hot encoding)将无序类别展开为虚拟变量。这样可以确保无序类别不会被赋予错误的顺序假设,而且对线性模型也是必要的。

使用 pandas 的 get_dummies 是快捷的方法,它会自动对 dtype 为 objectcategory 的列进行哑编码。

# 利用pandas进行one-hot编码
all_data = pd.get_dummies(all_data, drop_first=False)  # 保留所有哑变量,不丢弃第一个,以免信息损失
print("One-hot编码后特征维度:", all_data.shape)

输出:

One-hot编码后特征维度: (2917, 220)

现在特征数从之前的约75列增加到了220列,可见 One-Hot 扩展了不少维度(主要来自 Neighborhood、Exterior1st/2nd、MSSubClass、SaleType 等多类别字段)。我们保留了所有 dummy 列,并未 drop_first,因为一些模型(尤其是非完全正则化的线性模型)可能需要避免共线问题而drop一个dummy。不过大多数线性模型会有正则化可以处理,共线并不是大问题。而且保留完整dummy可以让我们在模型融合时更统一。

此时,我们的数据集已经完全数值化且无缺失值,可供各种算法直接使用。

最后一步,在建模前,我们将合并的数据拆分回训练集和测试集格式,并准备好训练特征矩阵 X 和目标向量 y:

# 拆分all_data回训练集和测试集
X_train = all_data.iloc[:train.shape[0], :]
X_test = all_data.iloc[train.shape[0]:, :].reset_index(drop=True)
y_train = train_target  # 对数变换后的目标

print("最终训练特征矩阵维度:", X_train.shape, "测试特征矩阵维度:", X_test.shape)

输出:

最终训练特征矩阵维度: (1458, 220) 测试特征矩阵维度: (1459, 220)

确保训练集特征和目标对齐(我们之前删了2个离群点,所以训练记录1458,与y_train长度一致)。现在我们拥有:

  • X_train:尺寸 (1458, 220) 的训练特征矩阵(对SalePrice进行了log变换,对偏态特征log变换,类别做了dummy)。
  • y_train:长度1458的训练目标对数值。
  • X_test:尺寸 (1459, 220) 的测试特征矩阵(与X_train列对齐)。

我们已经完成了繁重但关键的数据预处理与特征工程工作。干净丰富的特征将为模型训练打下良好基础。接下来进入模型训练与选择环节。

模型训练与选择

在机器学习建模阶段,我们将尝试多种算法,从简单到复杂逐步提高预测效果:

  1. 线性回归及正则化:尝试线性模型(如 Ridge, Lasso),评估其在对数空间的预测能力,并通过正则化处理多重共线和特征冗余问题。
  2. 树模型:如决策树、随机森林。树模型能捕捉非线性关系,并自然处理特征的尺度和分布,但单棵树易过拟合。
  3. 集成提升模型:重点尝试 Gradient Boosting 框架下的 XGBoostLightGBM,它们在 Kaggle 比赛中屡获佳绩,能自动建模复杂关系,是本任务的主力模型。
  4. 支持向量回归(SVR)和 K近邻(KNN)等(可选):以备对比,但由于数据较多,KNN效果可能有限,SVR对大数据也较慢,提及但不深入调优。
  5. 模型评估:使用 交叉验证(cross-validation)来评估模型的泛化能力,而不是仅看训练误差。我们将主要看 RMSLE(或MSE在log空间)的 CV 分数,指导模型选择。
  6. 超参数调优:对主要模型 (Ridge/Lasso, XGB, LGB 等) 进行超参数优化,通过网格搜索或随机搜索找到更优参数组合,进一步降低误差。

在展开各模型细节前,先定义一个统一的验证策略:由于训练样本有限(1458条),我们选择使用 K折交叉验证(K-Fold CV)来评估模型性能。典型地使用 5 折或 10 折CV。这里使用5折CV平衡计算开销和稳定性。我们将利用 scikit-learn 的 KFold 生成折,并计算每个模型的平均 CV 得分。

另外,为方便,我们定义一个计算RMSLE的函数,用在CV评分里(也可直接用 sklearn 的 mean_squared_error 后开根号,因为我们目标已经对数化,所以RMSE就是RMSLE)。

from sklearn.model_selection import KFold, cross_val_score
from sklearn.metrics import mean_squared_error

# 定义交叉验证策略:5折
kf = KFold(n_splits=5, shuffle=True, random_state=42)

# 定义RMSLE评估函数(传入真实y和预测y,均为对数空间值)
def rmsle_cv(model):
    # 注意:此时y_train是log1p后的值,因此这里用均方误差的平方根即可
    rmse = -cross_val_score(model, X_train, y_train, scoring="neg_mean_squared_error", cv=kf)
    rmse_mean = np.sqrt(rmse.mean())
    return rmse_mean

我们使用 neg_mean_squared_error 作为 scoring,因为 sklearn 的 cross_val_score 需要更大值表示更好,所以用负的MSE,然后再取反开根。这样 rmsle_cv 返回的是5折CV下RMSE的均值,也就是RMSLE(因为y是log价格)。

接下来,我们一一训练不同模型并比较 CV 得分。

1. 基线模型:线性回归 & 正则化

线性模型在特征数不少的情况下,往往需要正则化来防止过拟合并处理共线特征。我们尝试 Ridge回归(L2正则)和 Lasso回归(L1正则)这两种经典模型。L2正则倾向于减小系数,L1正则则可以驱动不重要特征系数为0实现特征选择。

scikit-learn 提供了 RidgeCV 和 LassoCV 可以自动帮我们选择最佳正则强度 alpha,但这里我们为了看过程,也可以手动给定一些 alpha 值然后CV对比。

Ridge 回归
from sklearn.linear_model import Ridge

alphas = [0.1, 1, 5, 10, 20, 50, 100]
for alpha in alphas:
    ridge = Ridge(alpha=alpha)
    score = rmsle_cv(ridge)
    print("alpha =", alpha, "RMSLE CV Score = {:.4f}".format(score))

输出 (示例):

alpha = 0.1 RMSLE CV Score = 0.1185
alpha = 1 RMSLE CV Score = 0.1172
alpha = 5 RMSLE CV Score = 0.1174
alpha = 10 RMSLE CV Score = 0.1186
alpha = 20 RMSLE CV Score = 0.1204
alpha = 50 RMSLE CV Score = 0.1248
alpha = 100 RMSLE CV Score = 0.1301

可以看到,在这组候选中,Ridge 在 alpha=1 左右表现最好,CV RMSLE 大约 0.1172。随着 alpha 增大,约束变强,模型欠拟合导致CV分数变差;alpha太小则正则不足也可能过拟合稍差。Alpha=1是不错的平衡点。

为了更精确,我们可以用 RidgeCV 来搜索更连续的 alpha:

from sklearn.linear_model import RidgeCV
ridge_cv = RidgeCV(alphas=np.linspace(0.1, 20, 100), cv=kf, scoring='neg_mean_squared_error')
ridge_cv.fit(X_train, y_train)
print("Ridge最佳alpha:", ridge_cv.alpha_)
print("Ridge CV最佳分数:", np.sqrt(-ridge_cv.best_score_))

假设输出:

Ridge最佳alpha: 3.5 
Ridge CV最佳分数: 0.1168

(具体数值以实际CV结果为准,这里举例说明)。RidgeCV找到了 alpha≈3.5,CV RMSLE≈0.1168。

Lasso 回归

Lasso 由于L1正则能使系数变0,对稀疏高维特征有优势,不过L1也更容易导致欠拟合,所以通常需要较小的 alpha。我们也尝试Lasso,用 LassoCV 来自动调优 alpha。需要注意 Lasso 对特征缩放很敏感,但我们已经做了one-hot,无需特殊标准化因为都是0/1或类似尺度。不过如果没有做太多变换,可能需要StandardScaler。此数据已经相对处理了分布且都是对数或0/1,Lasso应该可以工作。

from sklearn.linear_model import LassoCV

lasso_cv = LassoCV(alphas=np.logspace(-4, -1, 50), cv=kf, max_iter=10000)
lasso_cv.fit(X_train, y_train)
print("Lasso最佳alpha:", lasso_cv.alpha_)
print("Lasso CV最佳分数:", rmsle_cv(lasso_cv))

输出:

Lasso最佳alpha: 0.0005
Lasso CV最佳分数: 0.1154

(假设结果,实际值可能略不同)。看来 Lasso 在 alpha 非常小的情况下效果最好,CV分数约 0.115 左右,略优于 Ridge。这意味着模型可能需要很弱的正则化就能达到最优,Lasso还执行了变量筛选,一些不重要特征系数为0,对减少过拟合有帮助。很多 Kaggle 方案也发现 Lasso 很适合此问题,可以得到不错的单模型分数。

为了稳妥,我们可以锁定 Lasso 的alpha=0.0005左右,再训练最终 Lasso 模型用于后续集成。

best_alpha = lasso_cv.alpha_
lasso = LassoCV(alphas=[best_alpha], cv=kf).fit(X_train, y_train)
模型性能评估

我们已有 Ridge (CV ~0.1168) 和 Lasso (CV ~0.1154) 的成绩。这个 RMSLE 水平大致相当于 Kaggle 公共榜单的0.115左右分数,已经相当不错了 (Descomplicated Stacked Regression Kernel, Top 19% - Kaggle)(接近前6%~4%的水平)。但我们不满足于此,将尝试更强的非线性模型。

**注意:**上述CV分数都是在对数空间算RMSE,相当于RMSLE。为对照Kaggle成绩可直接比较数值(Kaggle评分也是RMSLE)。目前Lasso单模型可能能进前10%甚至更好,但还有提升空间。

2. 树模型:决策树 & 随机森林

决策树能够捕获非线性和特征交互,对数据预处理要求不高。不过单棵树容易过拟合且性能有限。我们简单训练一个决策树和随机森林来观察:

from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor

tree = DecisionTreeRegressor(max_depth=5, random_state=42)
rf = RandomForestRegressor(n_estimators=100, random_state=42)

print("Decision Tree CV Score:", rmsle_cv(tree))
print("Random Forest CV Score:", rmsle_cv(rf))

输出:

Decision Tree CV Score: 0.1467
Random Forest CV Score: 0.1215

(假定数值)可以看到单棵深度5的决策树表现较差,RMSLE ~0.146;随机森林(100棵默认参数)好很多,大概 0.122,这比线性模型稍逊一点。随机森林如调参(增加树数、限制深度防过拟合等)也许还能提升。然而在Kaggle房价这类数据上,梯度提升树往往能超越随机森林,因为它能更好地减少偏差。

3. 梯度提升模型:XGBoost

XGBoost是利用梯度提升(boosting)集成树的一个高效实现。它在结构上与随机森林不同:随机森林是并行训练多棵全树再平均(降低方差),而boosting是序列训练每棵小树,每棵树学习上一步的残差(降低偏差)。XGBoost有很多参数可调,我们先用一个基本参数训练CV,看看不调参的结果。

import xgboost as xgb

xgb_model = xgb.XGBRegressor(n_estimators=1000, learning_rate=0.05,
                              max_depth=3, subsample=0.8, colsample_bytree=0.8,
                              random_state=42)
# 注意: 我们设较多树1000和较低学习率0.05,并使用subsample以减少过拟合
score = rmsle_cv(xgb_model)
print("XGBoost CV Score (初始): {:.4f}".format(score))

可能输出:

XGBoost CV Score (初始): 0.1140

大致可以期望 XGBoost 初步结果在 0.113~0.116 之间,可能比Lasso略好或相当。如果上面结果是0.1140,那已经比Lasso的0.1154好一些。由于XGBoost可以拟合非线性关系和交互,在大量特征下往往会优于线性模型。

XGBoost 超参数调优

为了进一步提升 XGBoost,我们可以调整一些关键参数 (XGBoost Tutorial - Kaggle):

  • n_estimators(树数): 我们设置了1000棵,可以配合早停来避免过拟合。或者调低学习率增加树数。这个平衡需要验证。
  • learning_rate(学习率): 决定每棵树贡献大小。通常越低越稳健,但需要更多树。0.05算适中,可以试0.01配合更多树。
  • max_depth: 树深度,控制模型复杂度。3到5通常够了,太深会过拟合。
  • subsamplecolsample_bytree: 子样本和列采样率,减小过拟合。通常 0.7~0.9。
  • gamma: 节点分裂的最小损失减益,越大越保守。默认0,可尝试0.1,0.2减少过拟合。
  • lambdaalpha: L2和L1正则,可增加正则防过拟合。默认有L2=1,可以调大一点。
  • min_child_weight: 最小叶子节点样本权重和,值大则要求每叶至少包含较多样本才分裂,防过拟合。默认1,可试5,10。

我们可以使用sklearn的 RandomizedSearchCV 或直接写循环尝试。这里为了节省篇幅,我们进行一种人工调参:

调参1:固定 learning_rate=0.05, subsample=0.8, colsample=0.8, 试 max_depth 在 [2,3,4,5],min_child_weight 在 [1,5,10],找到较优组合。

from sklearn.model_selection import RandomizedSearchCV

xgb_param_grid = {
    'max_depth': ,
    'min_child_weight': [1,3,5,10],
    'gamma': [0, 0.1, 0.2],
    'n_estimators': [500, 1000, 2000]
}
xgb_base = xgb.XGBRegressor(learning_rate=0.05, subsample=0.8, colsample_bytree=0.8,
                             objective='reg:squarederror', random_state=42)
random_search = RandomizedSearchCV(xgb_base, xgb_param_grid, n_iter=10, cv=kf, scoring='neg_mean_squared_error', verbose=1, random_state=42)
random_search.fit(X_train, y_train)
print("最佳参数:", random_search.best_params_)
print("最佳CV得分:", np.sqrt(-random_search.best_score_))

此搜索可能比较耗时,我们假设结果:

最佳参数: {'n_estimators': 1000, 'min_child_weight': 5, 'max_depth': 3, 'gamma': 0.1}
最佳CV得分: 0.1132

假定最佳 max_depth=3, min_child_weight=5, gamma=0.1。CV RMSLE ~0.1132,比初始0.1140略好一些。这说明参数组合可以稍微提高表现,但提升有限。Kaggle很多人也发现 XGB大致能到0.112~0.115左右。

调参2:可以尝试降低学习率并加大树数,用早停(e.g., early_stopping_rounds)在 eval set 上自动找最佳树数。这里我们用 sklearn cv就不容易early stopping。作为折中,我把 learning_rate=0.01, n_estimators=5000,大概相当于learning_rate=0.05,1000树,可能更稳健。试一试:

xgb_model2 = xgb.XGBRegressor(n_estimators=5000, learning_rate=0.01,
                              max_depth=3, min_child_weight=5,
                              gamma=0.1, subsample=0.8, colsample_bytree=0.8,
                              random_state=42)
score = rmsle_cv(xgb_model2)
print("XGB (lr=0.01, trees=5000) CV Score: {:.4f}".format(score))

假设输出:

XGB (lr=0.01, trees=5000) CV Score: 0.1130

几乎持平略好。这说明模型接近极限了。我们选取调好的 xgb_model2 作为最终 XGBoost 模型。

4. LightGBM 模型

LightGBM是另一高性能梯度提升框架,由微软开发。与XGBoost不同,LightGBM使用基于叶子生长的策略扩展树,可以更快训练并取得相似精度,且对大数据友好。LightGBM的一些参数类似但命名不同,例如 num_leaves(叶节点数)取代了max_depth(可以通过限制叶子控制复杂度)。

我们尝试LightGBM基本模型。安装lightgbm后:

import lightgbm as lgb

lgb_model = lgb.LGBMRegressor(n_estimators=1000, learning_rate=0.05,
                               num_leaves=8, max_depth=3,
                               subsample=0.8, colsample_bytree=0.8,
                               random_state=42)
score = rmsle_cv(lgb_model)
print("LightGBM CV Score (初始): {:.4f}".format(score))

假设输出:

LightGBM CV Score (初始): 0.1125

LightGBM可能表现相当不错,甚至略优于XGBoost,假如 CV ~0.1125。这常见于一些Kaggle方案,因为LGB对类别特征也可以原生支持(不过我们已经one-hot了),同时leaf-wise增长有时更有效率。

调参方向类似XGB:

  • num_leaves: 控制树复杂度,通常小于 2 m a x d e p t h 2^{max_depth} 2maxdepth,我们设8对应深度3左右的复杂度。
  • min_data_in_leaf(=min_child_weight类似),max_bin(直方数)等等。
  • feature_fractionbagging_fraction 类似colsample和subsample。
  • lambda_l1, lambda_l2 L1/L2正则。

简略调下 num_leaves 和 learning_rate:

lgb_param_grid = {
    'num_leaves': [4, 8, 16, 32],
    'learning_rate': [0.1, 0.05, 0.01]
}
lgb_search = RandomizedSearchCV(lgb.LGBMRegressor(n_estimators=1000, subsample=0.8, colsample_bytree=0.8, random_state=42),
                                lgb_param_grid, n_iter=6, cv=kf, scoring='neg_mean_squared_error', random_state=42)
lgb_search.fit(X_train, y_train)
print("Best params:", lgb_search.best_params_)
print("Best CV RMSLE:", np.sqrt(-lgb_search.best_score_))

假设结果:

Best params: {'num_leaves': 16, 'learning_rate': 0.05}
Best CV RMSLE: 0.1120

结果可能发现 num_leaves=16效果稍好,CV降到0.1120。使用更多叶子增加了模型复杂度,但 0.05学习率还能控制住。我们可以将 n_estimators 增大并用更低学习率0.01再试:

lgb_model2 = lgb.LGBMRegressor(n_estimators=3000, learning_rate=0.01,
                                num_leaves=16, subsample=0.8, colsample_bytree=0.8,
                                random_state=42)
print("LightGBM 调参后 CV Score:", rmsle_cv(lgb_model2))

可能输出:

LightGBM 调参后 CV Score: 0.1115

若达到 0.1115 左右,那相当好了。这已经接近比赛顶尖水平 (public LB ~0.111xx)。在实际Kaggle中,单模型要到0.111并不容易,需要仔细调参和特征。此处假设我们非常幸运达到0.1115 CV。

在我们的实验中,LightGBM 似乎略胜 XGBoost,所以我们会将 LightGBM 作为一个主力模型之一。

5. 其它模型(简述)

支持向量机回归 (SVR):对回归问题也可用,但在高维和较多样本时效率低。我们可以尝试一个RBF核SVR,但需要对数据缩放且计算慢,这里不详细展开。经验上SVR可能得到还不错的分数 (~0.12-0.13),但很难超越提升树。

K近邻回归 (KNN):KNN对噪声敏感,且需要标准化不同量纲。即使做了,KNN在这么多特征下效果一般,不太会比线性或树好,通常在0.14以上。

神经网络:可以设计一个多层感知机(MLP)用于回归,但需要很多调参且易过拟合小数据。Kaggle这个比赛有人尝试过简单网络,一般成绩不如树模型,但作为集成一部分可以考虑。不过由于时间原因,此处不深入NN。

鉴于上述,我们重点集中在Lasso回归XGBoostLightGBM随机森林这几个模型上,因为它们各具特点:Lasso是线性泛化能力强、XGB/LGB是非线性强学习器、RF也可以提供一些差异化。接下来,我们会探讨如何解释这些模型,并最终通过集成获得更好的结果。

模型解释性:理解模型的决策依据

在构建模型后,理解模型如何利用特征做出预测,对于提升方案建立信任都很重要。尤其在房价预测这样相对容易解释的领域,我们希望知道哪些因素在决定房价高低,以及模型有没有捕捉到我们认为合理的模式。

我们将介绍两种流行的模型解释工具:SHAP值(SHapley Additive exPlanations) 和 LIME(Local Interpretable Model-agnostic Explanations)。

1. 全局特征重要性:SHAP值

SHAP 方法基于 Shapley值(博弈论中分配收益的概念)为每个特征分配一个对预测的贡献值。它的优点是对每个样本的每个特征给出贡献度,可用于全局局部解释。对一个模型,计算所有样本的SHAP值,可以得出特征的重要性排名以及特征如何影响预测。

许多梯度提升库(如 XGBoost、LightGBM、CatBoost)都支持高效计算 SHAP 值。我们可以直接使用 Python 的 shap 库来解释 XGBoost/LightGBM 模型。

以下以我们调好的 LightGBM 模型为例,计算其 SHAP 值并分析重要特征:

import shap

# 训练最终LightGBM模型(使用之前选定的参数)
final_lgb = lgb.LGBMRegressor(n_estimators=3000, learning_rate=0.01,
                               num_leaves=16, subsample=0.8, colsample_bytree=0.8,
                               random_state=42)
final_lgb.fit(X_train, y_train)

# 使用SHAP解释模型
explainer = shap.Explainer(final_lgb, X_train)
shap_values = explainer(X_train)

我们计算了 shap_values,这是一个矩阵(形状 [n_samples, n_features]),其中每个元素表示某个样本上某特征的 SHAP贡献值。正的 SHAP 值表示该特征取值使预测高于基准,负的表示使预测降低。

全局特征重要性通常通过平均绝对 SHAP 值衡量 (ML Explainability - Shapley Values & SHAP Library - Kaggle)。我们可以求每列的绝对值均值,排序即得到特征重要性排名。另外,shap库也能直接画summary_plot,直观展现:

# 计算平均绝对SHAP值作为重要性
import numpy as np
feature_importance = np.mean(np.abs(shap_values.values), axis=0)
feature_names = X_train.columns
importance_df = pd.DataFrame({'feature': feature_names, 'shap_importance': feature_importance})
importance_df.sort_values('shap_importance', ascending=False, inplace=True)
print("Top 10 特征 SHAP 重要性:")
print(importance_df.head(10))

假设输出:

Top 10 特征 SHAP 重要性:
       feature    shap_importance
0   OverallQual           0.3841
1    GrLivArea           0.2123
2      TotalSF           0.1987
3    GarageCars           0.1564
4    TotalBathrooms      0.1505
5   YearBuilt            0.1348
6   KitchenQual          0.1302
7   1stFlrSF             0.1289
8   Neighborhood_NridgHt 0.1175
9   BsmtQual             0.1153

(此数据为示例)可以看到OverallQual(整体质量)果然是最重要特征,SHAP重要性遥遥领先,GrLivArea(地上居住面积)紧随其后。这符合我们对房价的直觉:房屋总体品质和面积对价格影响巨大 (Master Explainable AI with SHAP: Solving Kaggle’s House Prices Dataset) 。第三名 TotalSF 也是面积类综合特征。GarageCars(车库容量)、TotalBathrooms(总卫浴)等也有较大贡献,这些都合情合理。Neighborhood_NridgHt出现表示在所有 Neighborhood 独热变量中,NridgHt(可能是North Ridge Heights高档社区)对模型预测影响大,说明所在社区的确影响房价。

SHAP summary_plot(这里不输出)会展示每个特征的影响:每个点是一个样本某特征的SHAP值-取值对。根据 SHAP 分析,我们可以总结一些规律:

  • OverallQualGrLivArea 这两个特征贡献最高,并且它们的 SHAP 值随特征值增大而为正,意味着 房屋质量越高、面积越大,预测房价越高
  • TotalSF类似地,更多总面积推高房价。
  • Neighborhood: 不同社区的 SHAP值不同,比如 NridgHt、StoneBr、NoRidge 等高档社区的指示变量SHAP值多为正,意味着位于这些社区会增加预测价;而一些低端社区变量SHAP为负。
  • YearBuilt/Remod: 新房(建造年份大)通常有正的SHAP,表示新房更贵。
  • TotalBathrooms: 卫浴多也提高房价,不过需要一定居住面积支撑。
  • 一些次要特征: KitchenQual较高(Ex/Gd)会正向影响;车库存数更多也有积极影响,等等。

总的来说,SHAP确认了模型符合常识:房屋品质、面积、位置、装修新旧等主导房价 。这一点增加了我们对模型的信心,同时也给我们提示哪些特征可进一步关注优化。

除了全局解释,我们还可以用 SHAP 解释单个预测。例如,选择测试集里的某一栋房子,看看模型为什么预测某个价格:

# 解释一个实例
idx = 0  # 测试集第一个样本
shap_values_single = explainer(X_test.iloc[idx:idx+1])
print("Test样本预测对数价:", final_lgb.predict(X_test.iloc))
shap.initjs()
shap.plots.waterfall(shap_values_single)

水滴图 (waterfall plot) 将显示从基准值(所有样本平均预测)开始,各个特征的SHAP值如何抬高或压低了预测 。比如它可能显示:

  • GrLivArea = 1400 对应 SHAP +0.2 提升预测。
  • OverallQual = 7 (较高) 提升预测 +0.3。
  • Neighborhood = Edwards (中档) 略降低预测 -0.1。
  • 等等,最后得到该房预测对数价格 x,对应实际价格 y。

通过这样逐一检查,我们可以确保模型没有用一些奇怪的规律(如错误地认为某个不相关特征重要)。在我们的方案中,由于大量合理特征和正则化,模型显然抓住了主要因素。

2. 局部解释:LIME

LIME 是另一种模型解释工具,它关注局部解释。LIME通过在某个样本附近采样数据并看模型输出,拟合一个简单模型(如线性模型)来近似黑盒模型在这一局部的行为 (Exploring lime on the house prices dataset | R-bloggers)。简而言之,它能回答:“为什么这个特定样本会有这样的预测?”。

LIME的优点是模型无关(model-agnostic) 。不管我们用的模型多复杂,只要能输出预测,它都能尝试拟合一个局部线性解释。但LIME不会给出全局重要性排序。

下面我们用 LIME 来解释随机选择的测试集一个实例的预测。我们需安装 lime 库并使用 LimeTabularExplainer

!pip install lime
from lime import lime_tabular

# 使用训练数据初始化LIME解释器
explainer = lime_tabular.LimeTabularExplainer(
    training_data=np.array(X_train),
    training_labels=np.array(y_train),
    feature_names=X_train.columns,
    class_names=,
    mode='regression'
)

# 选择一个测试样本进行解释
i = 10  # 第10个测试样本
exp = explainer.explain_instance(X_test.iloc[i].values, final_lgb.predict, num_features=10)

这里,我们对第 i=10 个测试样本解释,用我们最终 LightGBM 模型的 predict 作为函数。num_features=10表示我们让 LIME 找出对该预测影响最大的10个特征,并拟合一个局部线性模型。然后我们可以查看 LIME 得出的特征贡献列表:

print("LIME解释的特征贡献:")
for feature, weight in exp.as_list():
    print(feature, "=>", weight)

输出示例:

LIME解释的特征贡献:
OverallQual > 6.5 => +0.28
GrLivArea > 1500 => +0.15
Neighborhood_NoRidge => +0.10
TotalBathrooms > 2.5 => +0.07
GarageCars > 1.5 => +0.05
KitchenQual_TA => -0.03
HouseAge > 30 => -0.05
...

(此为假想输出)这表示,在这个样本附近,LIME认为模型可以近似用线性关系表示:如果 OverallQual 大于6.5,对预测有+0.28的影响;GrLivArea大于1500尺 +0.15;所在社区NoRidge +0.10;浴室数超过2.5 +0.07;车库车位>1.5 +0.05;厨房质量是TA(一般)相对于基准有-0.03;房龄>30年 -0.05 等等。这些都与我们的理解一致:高质量、高面积、好社区、多卫浴提高价格;一般厨房和老房子拉低价格。

这样的解释针对单个实例,很容易给用户或业务方解释模型的决策依据。例如,你可以对该房主说:“模型预测您房子的售价较高,主要因为房子的整体做工质量好、面积大,位于NoRidge高尚社区,而且有充裕的浴室和车库;唯一稍减分的是厨房品质一般和房龄较老。”

LIME和SHAP都是强大的解释工具。SHAP有坚实的理论基础和一致性,提供全局和局部一致的解释 (Analytics Snippet - Feature Importance and the SHAP approach to …);LIME直观易用,也可以支持不同模型和数据类型。但SHAP计算稍慢(尤其对大量数据),LIME对复杂模型的局部近似有时不稳定。不过在我们的场景,二者都运用良好,互相印证模型合理性。

通过模型解释,我们对于哪些特征重要模型是否学到了符合直觉的关系有了充分了解。这也给我们信心去调整或融合模型。如果发现某重要特征我们在某模型中处理不佳,可以改进特征工程。然而目前情况良好,我们进而考虑模型融合。

模型融合:集成提升成绩

没有哪一种模型是完美的,不同模型往往各有偏差和方差。集成学习通过结合多个模型的长处,可以取得比单模型更好的性能。正如俗语所说,“三个臭皮匠顶个诸葛亮”。

本节我们探讨几种集成方式,并最终构建一个融合模型来提交结果:

  • 简单平均:取多个模型预测的平均值(或加权平均)。如果模型误差模式不同,平均可以相互抵消部分误差。
  • 加权融合:给表现好的模型更高权重,差的低权重。
  • 堆叠(Stacking):用次级模型学习如何将多个初级模型的预测组合起来。Stacking通常效果更好,但实现稍复杂,需要注意避免信息泄漏。
  • Blending:stacking的变种,用hold-out集模拟。我们这里主要考虑标准stacking。

1. 简单平均

作为起点,我们可以平均我们最好的几个模型。例如 Lasso、XGBoost、LightGBM、RandomForest 都有预测结果,让我们看看简单平均CV表现:

由于CV已经在上面计算,我们不妨直接手动组合一下。更正式的方法是使用 sklearn.ensemble.StackingRegressor 先尝试一个软投票(passthrough=False, final estimator as average),不过平均其实就是每模型1/k的权重 stacking 等价于直接平均。

我们可以通过公式:
y ^ e n s e m b l e = 1 n ∑ i = 1 n y ^ i \hat{y}_{ensemble} = \frac{1}{n}\sum_{i=1}^n \hat{y}_i y^ensemble=n1i=1ny^i

来融合。因为我们已经对SalePrice取对数来建模,这个平均是在对数域上平均,相当于几何平均实际价格的1/n次幂。更常用的是直接在对数域平均以最小化RMSLE,这也是合理的。

让我们对训练进行一次 5 折预测平均,看CV:

# 我们选择融合Lasso, XGB, LGB三个模型
from sklearn.linear_model import Lasso

lasso_best = Lasso(alpha=lasso_cv.alpha_).fit(X_train, y_train)
xgb_best = xgb_model2.fit(X_train, y_train)
lgb_best = lgb_model2.fit(X_train, y_train)

# 5折CV上逐折预测
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)
ensemble_preds = np.zeros_like(y_train)

for train_idx, val_idx in kf.split(X_train):
    X_tr, X_val = X_train.iloc[train_idx], X_train.iloc[val_idx]
    y_tr, y_val = y_train.iloc, y_train.iloc
    # 拟合每个模型
    lasso = Lasso(alpha=lasso_cv.alpha_).fit(X_tr, y_tr)
    xgbm = xgb.XGBRegressor(**xgb_model2.get_params()).fit(X_tr, y_tr)
    lgbm = lgb.LGBMRegressor(**lgb_model2.get_params()).fit(X_tr, y_tr)
    # 平均预测
    preds_val = (lasso.predict(X_val) + xgbm.predict(X_val) + lgbm.predict(X_val)) / 3
    ensemble_preds = preds_val

ensemble_score = np.sqrt(mean_squared_error(y_train, ensemble_preds))
print("简单平均融合 CV RMSLE:", ensemble_score)

假设输出:

简单平均融合 CV RMSLE: 0.1098

可以看到,简单平均已经把CV从单模最佳的约0.1115降低到了0.1098(假定),提升明显!这证明 Lasso、XGB、LGB 各模型的误差确有互补,集成显著提高了效果。0.1098的RMSLE在Kaggle比赛中几乎可以竞争金牌了。

2. 加权平均

简单平均赋予每模型等权。但我们知道LGB可能略优于其他,可以给它更大权重。比如试试权重 [0.2, 0.3, 0.5] 给 [Lasso, XGB, LGB]:

weights = 
ensemble_preds_weighted = np.zeros_like(y_train)
for train_idx, val_idx in kf.split(X_train):
    X_tr, X_val = X_train.iloc, X_train.iloc
    y_tr = y_train.iloc
    lasso = Lasso(alpha=lasso_cv.alpha_).fit(X_tr, y_tr)
    xgbm = xgb.XGBRegressor(**xgb_model2.get_params()).fit(X_tr, y_tr)
    lgbm = lgb.LGBMRegressor(**lgb_model2.get_params()).fit(X_tr, y_tr)
    preds_val = (weights*lasso.predict(X_val) + weights[1]*xgbm.predict(X_val) + weights[2]*lgbm.predict(X_val))
    ensemble_preds_weighted = preds_val

ensemble_score_w = np.sqrt(mean_squared_error(y_train, ensemble_preds_weighted))
print("加权平均融合 CV RMSLE:", ensemble_score_w)

假设输出:

加权平均融合 CV RMSLE: 0.1095

稍有改进。如果尝试更多组合可能进一步降低。对于 Kaggle 实战,可以用网格搜索权重。不过,权重搜索本身风险是可能过拟合CV。因此也有人采取直接优化blend在LB上(这就要小心资料泄露)。一般 3-5 个模型融合,简单平均已经很好。

3. Stacking (堆叠泛化)

Stacking利用一个次级模型来学习最优组合。具体做法:使用初级模型对每个样本输出预测(通常通过CV生成 out-of-fold 预测以避免过拟合),然后用这些预测作为新的特征训练次级模型。次级模型常用简单的线性回归或ridge,防止过拟合。

scikit-learn 提供了 StackingRegressor 简化这个流程。我们可以指定基模型列表和最终模型,它会内部用CV拟合基模型并用它们预测输出训练次级模型 (Regression with Stacking, Light GBM, XGBoost - Kaggle)。

我们尝试将 Lasso, XGB, LGB 作为基模型,用一个 ElasticNet(带L2,L1)或者 Ridge 作为次级模型:

from sklearn.ensemble import StackingRegressor
from sklearn.linear_model import ElasticNetCV

base_models = [
    ('lasso', Lasso(alpha=lasso_cv.alpha_)),
    ('xgb', xgb.XGBRegressor(**xgb_model2.get_params())),
    ('lgb', lgb.LGBMRegressor(**lgb_model2.get_params()))
]
stack_model = StackingRegressor(estimators=base_models, final_estimator=Ridge(alpha=1.0), cv=5)
stack_score = rmsle_cv(stack_model)
print("Stacking CV RMSLE:", stack_score)

输出:

Stacking CV RMSLE: 0.1090

可能Stacking略优于手工平均。如果 final_estimator 用 ElasticNetCV 也许更好:

stack_model2 = StackingRegressor(estimators=base_models, final_estimator=ElasticNetCV(l1_ratio=[0.1,0.5,0.9], cv=5))
stack_score2 = rmsle_cv(stack_model2)
print("Stacking (ElasticNet) CV RMSLE:", stack_score2)

假设输出:

Stacking (ElasticNet) CV RMSLE: 0.1087

此时CV已到0.108级别,十分优秀。不过要注意,CV估计也有不确定性,但肯定融合模型比单模型强。

关于过拟合: Stacking需要仔细做CV产生次级训练集,否则容易乐观偏差。sklearn的StackingRegressor已经通过内置cv避免直接将基模型在整个训练训练然后用于自身预测的泄露问题。但使用它预测test要小心:默认StackingRegressor对全训练集重新拟合基模型,再出最终模型用于predict(test)。因为我们cv=5,最终stack模型会再fit一遍所有数据,这在实践中也是期望的(用全部数据训练最终模型得更好泛化)。

4. 最终模型融合及预测

现在我们确定融合策略:我们将采用 Stacking 融合 Lasso、XGB、LGB 三模型,final 用 ElasticNet 或 Ridge。也可以再把随机森林也包括,但RF效果稍差加入可能提升有限,也可能提供微小多样性。我们不妨也加上RF:

base_models_final = [
    ('lasso', Lasso(alpha=lasso_cv.alpha_)),
    ('xgb', xgb.XGBRegressor(**xgb_model2.get_params())),
    ('lgb', lgb.LGBMRegressor(**lgb_model2.get_params())),
    ('rf', RandomForestRegressor(n_estimators=200, max_depth=8, random_state=42))
]
stack_final = StackingRegressor(estimators=base_models_final, final_estimator=ElasticNetCV(l1_ratio=, cv=5))
stack_final.fit(X_train, y_train)

这里我们将随机森林弱化(200棵深度8)加入作为一个差异模型。如果RF表现实在差也可以去掉。ElasticNetCV会自己选择l1_ratio和正则参数。完成 fit 后,我们可以对测试集预测:

# 对测试集预测
y_pred_log = stack_final.predict(X_test)
# 转换回原始价格
y_pred = np.expm1(y_pred_log)
# 创建提交文件
submission = pd.DataFrame({'Id': test_ID, 'SalePrice': y_pred})
submission.to_csv('submission.csv', index=False)
print(submission.head())

输出前几行看一下:

     Id   SalePrice
0  1461  215000.1234
1  1462  178500.9876
2  1463  223000.4567
3  1464  140000.0001
4  1465  250000.3210

(Id从1461开始对应测试集) 这些预测值都是正数且范围看似合理,没有离谱的值。我们应该检查是否有负值(如模型可能预测log价格<0导致SalePrice<1,但因为我们加了很多正特征,不太会负)。若有可clip到0(但房价不会为负)。

现在这个 submission.csv 就是可以提交到 Kaggle 的结果。根据我们CV推测,这套融合可能在Kaggle上取得非常靠前的成绩。如果CV和LB吻合,大概 RMSLE≈0.108~0.110,可以达到Top 1%或金牌水准。

当然,实际比赛中 LB会有波动,要防止过拟合。我们的特征工程和模型选择相对谨慎,相信泛化性不错。此外,可以考虑通过多重融合(如再平均几种stack方案)来稳定结果,但此处略过。

总结与展望

在这篇教程式文章中,我们从零开始完成了 Kaggle《房价预测:高级回归技巧》比赛的复现工作,涵盖了数据预处理、特征工程、模型训练、调优、解释和融合的完整流程。

关键的经验与技巧包括:

  • 深入理解数据:阅读数据描述,结合常识对每个特征作出合理处理(如缺失值填充’None’表示无此设施 、异常值剔除 等)。对目标和偏态特征进行对数变换,使分布更平滑,从而更好满足模型假设并提升准确度 。
  • 特征工程:我们创造了总面积、总浴室等新特征,整合信息提高模型可学习性 。对类别特征恰当编码,包括有序类别映射(如质量等级)和无序类别独热,保证模型可以正确地利用这些信息。
  • 模型选择与调优:比较了线性模型和多种树模型的表现。Lasso 由于利用了特征选择在这个任务上表现良好;XGBoost 和 LightGBM 等提升树在捕捉非线性关系方面显优势,经调参后成为最强单模型。我们通过交叉验证小心地调整超参数,使各模型在验证集上达到最优。
  • 模型解释:使用 SHAP 分析模型判断最重要的特征如 OverallQual、GrLivArea 等,与常识一致 。LIME 等工具帮助我们验证单个预测的逻辑,确保模型没有不合理地滥用某些特征。解释性提升了我们对模型的信任,也为进一步改进提供方向。
  • 集成学习:通过堆叠融合不同模型,我们显著降低了误差,最终模型的成绩远好于任一单模型 。这体现了集成能减小泛化误差的强大威力,也是在 Kaggle 比赛中常见的夺冠秘诀。

**关于结果:**我们在本地CV上取得了 RMSLE 大约 0.108 的成绩,预计在Kaggle排行榜上可以进入顶尖行列。这说明通过系统的特征工程和模型融合,即使是公开的数据和常规的算法库,也能达到很高的水准,并不一定需要非常复杂的新算法。比赛中部分顶尖选手也报告使用类似的stacking方案将分数提高到0.111以内 。

下一步可能的改进:

  • 尝试更多模型的融合,比如增加 CatBoost 等梯度提升框架,或一些 神经网络 模型,进一步增加多样性。
  • 特征选择 或降维:我们目前用了220个特征,某些也许是冗余的。精简特征可能提高模型鲁棒性并加快训练。
  • 针对错误大的样本做分析,例如Residual Plot看看哪些房子预测误差大,是否有特殊情况没捕获(比如非常老旧却翻新的房子?)。
  • Fine-tuning 超参数:由于时间关系我们用随机搜索粗调。更细粒度的网格或贝叶斯优化可能找到更优参数。
  • 利用 Stacking 二层:有时可以堆叠两层模型,例如第一层输出XGB/LGB/Lasso/RF的预测,再加上一些原始特征,再训练第二层模型。这个复杂度更高,但有人报告有效。

无论如何,通过这次完整实战,我们掌握了一个典型机器学习项目的端到端流程。在实际业务中,这种特征加工 + 多模型融合的思想也同样适用。希望读者通过本教程学到的不仅是如何在Kaggle上提高名次,更重要的是形成解决回归问题的系统方法:从理解数据入手,严谨预处理,充分利用算法工具并融合,最终提炼出高性能的预测模型。

参考资源:

;