本文用于对kaggel项目:House Prices - Advanced Regression Techniques 的学习。

一.参考资料

Stacked Regressions : Top 4% on LeaderBoard

【kaggel学习笔记】Comprehensive data exploration with Python 使用python进行全面数据探索-CSDN博客

【kaggel学习笔记】Regularized Linear Models-正则化线性模型-CSDN博客

【kaggel学习笔记】A study on Regression applied to the Ames dataset-基于Ames房价数据集的回归模型研究-CSDN博客

二.数据处理部分

原文:

由于这个数据集使用起来十分方便,几天前我决定重新回到这场比赛,把目前学到的知识付诸实践,尤其是模型堆叠(stacking)相关的内容。为此,我实现了两个堆叠类(一种最简方案,以及一种稍复杂的方案)。

这些类是为通用场景编写的,你可以轻松将它们适配或扩展到自己的回归问题中。整体思路我尽量写得简洁易懂。

这份笔记中的特征工程相对精简(至少和其他一些优秀代码相比),主要包括:

  • 按数据顺序依次填充缺失值
  • 将一些看似是数值、实际为分类的变量做类别转换
  • 对一些包含有序信息的分类变量做标签编码
  • 对有偏分布的特征做 Box-Cox 变换(而非对数变换):这一处理让我的公榜分数和交叉验证结果都有了小幅提升
  • 对分类特征做独热编码(哑变量)

之后我选用了多种基模型(大部分是 scikit-learn 原生模型,加上 DMLC 的 XGBoost 与微软的 LightGBM 的 sklearn 接口),在数据上做交叉验证,再进行堆叠与集成。关键在于让(线性)模型对异常值更稳健,这一点同时提升了公榜成绩和交叉验证效果。

出乎我意料的是,这套方案在榜单上表现相当不错(最后一次测试是 2017 年 7 月 2 日,得分 0.11420,前 4%)。

希望看完这份笔记后,那些和我当初一样觉得堆叠概念难以理解的人,能够真正弄明白 stacking 到底是什么。

1. 库导入 + 读数据 2. 合并 train + test(安全做法,不泄露) 3. 数据清洗 + 缺失值处理(业务逻辑) 4. 特征工程(轻量但有效) 5. 偏斜特征 Box-Cox 变换 6. 独热编码 7. 分离回 train / test 8. 定义一堆基础模型(base models) 9. 定义 Stacking 类(核心!) 10. 交叉验证训练所有模型 11. Stacking 融合 12. 预测测试集 13. 生成提交文件

1.库导入 + 读数据

# 导入一些必要的库

import numpy as np      # 线性代数运算
import pandas as pd     # 数据处理、CSV文件读写(例如 pd.read_csv)
%matplotlib inline
import matplotlib.pyplot as plt  # Matlab风格的绘图
import seaborn as sns            # 更美观的绘图库
color = sns.color_palette()       # 获取配色方案
sns.set_style('darkgrid')         # 设置绘图背景为暗网格

# 忽略警告设置
import warnings
def ignore_warn(*args, **kwargs):
    pass
warnings.warn = ignore_warn  # 忽略来自sklearn和seaborn的烦人的警告


from scipy import stats       # 科学计算库
from scipy.stats import norm, skew  # 用于一些统计计算(正态分布、偏度)


# 限制浮点数输出为3位小数
pd.set_option('display.float_format', lambda x: '{:.3f}'.format(x))


from subprocess import check_output
# 查看 ../input 文件夹下有哪些文件(Kaggle环境专用)
print(check_output(["ls", "../input"]).decode("utf8"))
sample_submission.csv
test.csv
train.csv
#Now let's import and put the train and test datasets in  pandas dataframe

train = pd.read_csv('../input/train.csv')
test = pd.read_csv('../input/test.csv')
##display the first five rows of the train dataset.
train.head(5)

5 rows × 81 columns

##display the first five rows of the test dataset.
test.head(5)

5 rows × 80 columns

# 查看样本和特征的数量
print("删除 Id 列之前,训练集大小为:{} ".format(train.shape))
print("删除 Id 列之前,测试集大小为:{} ".format(test.shape))

# 保存 Id 列(后面提交结果时需要用到)
train_ID = train['Id']
test_ID = test['Id']

# 删除 Id 列,因为它对预测没有帮助
train.drop("Id", axis = 1, inplace = True)
test.drop("Id", axis = 1, inplace = True)

# 再次查看删除 Id 后的数据大小
print("\n删除 Id 列之后,训练集大小为:{} ".format(train.shape)) 
print("删除 Id 列之后,测试集大小为:{} ".format(test.shape))
The train data size before dropping Id feature is : (1460, 81) 
The test data size before dropping Id feature is : (1459, 80) 

The train data size after dropping Id feature is : (1460, 80) 
The test data size after dropping Id feature is : (1459, 79) 

2.EDA+数据预处理

2.1异常值

# 创建绘图对象
fig, ax = plt.subplots()
# 绘制 GrLivArea 与 SalePrice 的散点图
ax.scatter(x = train['GrLivArea'], y = train['SalePrice'])
plt.ylabel('SalePrice', fontsize=13)
plt.xlabel('GrLivArea', fontsize=13)
plt.show()

#Deleting outliers
train = train.drop(train[(train['GrLivArea']>4000) & (train['SalePrice']<300000)].index)

#Check the graphic again
fig, ax = plt.subplots()
ax.scatter(train['GrLivArea'], train['SalePrice'])
plt.ylabel('SalePrice', fontsize=13)
plt.xlabel('GrLivArea', fontsize=13)
plt.show()

原文:

删除异常值并非总是安全的。我们之所以决定删除这两个样本,是因为它们过于极端,影响非常糟糕(面积极大,售价却极低)。

训练数据中很可能还存在其他异常值。但如果测试数据中也存在异常值,那么把所有异常值都删掉可能会对模型产生严重负面影响。因此,我们不会删除全部异常值,而是会设法让部分模型对异常值具备鲁棒性。你可以参考本笔记的建模部分了解具体实现。

2.2因变量分析

sns.distplot(train['SalePrice'] , fit=norm);

# Get the fitted parameters used by the function
(mu, sigma) = norm.fit(train['SalePrice'])
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))

#Now plot the distribution
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],
            loc='best')
plt.ylabel('Frequency')
plt.title('SalePrice distribution')

#Get also the QQ-plot
fig = plt.figure()
res = stats.probplot(train['SalePrice'], plot=plt)
plt.show()

2.3因变量log1p变换

#我们使用numpy函数log1p,对该列的所有元素执行log(1+x)变换
train["SalePrice"] = np.log1p(train["SalePrice"])

#检查变换后的新分布
sns.distplot(train['SalePrice'] , fit=norm);

#获取函数使用的拟合参数
(mu, sigma) = norm.fit(train['SalePrice'])
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))

#绘制分布图像
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],
            loc='best')
plt.ylabel('Frequency')
plt.title('SalePrice distribution')

#同时绘制QQ图
fig = plt.figure()
res = stats.probplot(train['SalePrice'], plot=plt)
plt.show()
 mu = 12.02 and sigma = 0.40

2.4合并数据集

# 计算训练集的样本数量(行数)
ntrain = train.shape[0]

# 计算测试集的样本数量(行数)
ntest = test.shape[0]

# 把训练集的目标值(房价)提取出来,保存到y_train中
y_train = train.SalePrice.values

# 把训练集和测试集上下拼接在一起,生成一个总数据集,并重置索引
all_data = pd.concat((train, test)).reset_index(drop=True)

# 把拼接后数据里的 SalePrice 列删掉(因为测试集没有房价,需要统一格式)
all_data.drop(['SalePrice'], axis=1, inplace=True)

# 输出拼接后所有数据的大小(行数和列数)
print("all_data size is : {}".format(all_data.shape))
all_data size is : (2917, 79)

2.5缺失率分析

# 计算每列缺失值占的百分比
all_data_na = (all_data.isnull().sum() / len(all_data)) * 100

# 去掉缺失率为0的列,按缺失率从高到低排序,取前30个
all_data_na = all_data_na.drop(all_data_na[all_data_na == 0].index).sort_values(ascending=False)[:30]

# 把缺失比例存成DataFrame,方便查看
missing_data = pd.DataFrame({'Missing Ratio' :all_data_na})

# 展示缺失最多的前20个特征
missing_data.head(20)

# 创建画布,设置图片大小
f, ax = plt.subplots(figsize=(15, 12))
# X轴文字旋转90度,防止重叠
plt.xticks(rotation='90')
# 绘制柱状图:X轴是特征名称,Y轴是缺失值百分比
sns.barplot(x=all_data_na.index, y=all_data_na)
# X轴标签:特征
plt.xlabel('Features', fontsize=15)
# Y轴标签:缺失值百分比
plt.ylabel('Percent of missing values', fontsize=15)
# 图表标题
plt.title('Percent missing data by feature', fontsize=15)
Text(0.5,1,'Percent missing data by feature')

2.6数据相关性分析

# 绘制相关性热力图,观察各特征与 SalePrice 的相关程度
corrmat = train.corr()
plt.subplots(figsize=(12,9))
sns.heatmap(corrmat, vmax=0.9, square=True)
<matplotlib.axes._subplots.AxesSubplot at 0x7efd7b454898>

2.7填充缺失值

# 填充缺失值
# 我们按照顺序逐个处理有缺失值的特征

# PoolQC:数据说明中写 NA 代表“没有游泳池”
# 考虑到超过99%的缺失率,且大多数房子确实没有泳池,这样填充合理
all_data["PoolQC"] = all_data["PoolQC"].fillna("None")

# MiscFeature:NA 代表“没有额外设施”
all_data["MiscFeature"] = all_data["MiscFeature"].fillna("None")

# Alley:NA 代表“没有小巷通道”
all_data["Alley"] = all_data["Alley"].fillna("None")

# Fence:NA 代表“没有围栏”
all_data["Fence"] = all_data["Fence"].fillna("None")

# FireplaceQu:NA 代表“没有壁炉”
all_data["FireplaceQu"] = all_data["FireplaceQu"].fillna("None")

# LotFrontage:
# 同一个街区的房屋,临街面积通常很相似
# 因此用同一 Neighborhood 的中位数来填充
all_data["LotFrontage"] = all_data.groupby("Neighborhood")["LotFrontage"].transform(
    lambda x: x.fillna(x.median()))

# GarageType, GarageFinish, GarageQual, GarageCond:
# 缺失代表没有车库,填充为 None
for col in ('GarageType', 'GarageFinish', 'GarageQual', 'GarageCond'):
    all_data[col] = all_data[col].fillna('None')

# GarageYrBlt, GarageArea, GarageCars:
# 没有车库 → 这些数值都是 0
for col in ('GarageYrBlt', 'GarageArea', 'GarageCars'):
    all_data[col] = all_data[col].fillna(0)

# BsmtFinSF1, BsmtFinSF2, BsmtUnfSF, TotalBsmtSF, BsmtFullBath, BsmtHalfBath:
# 没有地下室 → 面积、数量都填 0
for col in ('BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF','TotalBsmtSF', 'BsmtFullBath', 'BsmtHalfBath'):
    all_data[col] = all_data[col].fillna(0)

# BsmtQual, BsmtCond, BsmtExposure, BsmtFinType1, BsmtFinType2:
# 没有地下室 → 填 None
for col in ('BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2'):
    all_data[col] = all_data[col].fillna('None')

# MasVnrArea 和 MasVnrType:
# 缺失代表没有砖石饰面 → 类型填 None,面积填 0
all_data["MasVnrType"] = all_data["MasVnrType"].fillna("None")
all_data["MasVnrArea"] = all_data["MasVnrArea"].fillna(0)

# MSZoning:
# 最多的类别是 RL,用众数填充
all_data['MSZoning'] = all_data['MSZoning'].fillna(all_data['MSZoning'].mode()[0])

# Utilities:
# 几乎所有值都是 AllPub,只有一个不同,且缺失2个
# 对建模没用,直接删除
all_data = all_data.drop(['Utilities'], axis=1)

# Functional:
# 数据说明写 NA 代表 typical(正常)
all_data["Functional"] = all_data["Functional"].fillna("Typ")

# Electrical:
# 只有1个缺失,用众数 SBrkr 填充
all_data['Electrical'] = all_data['Electrical'].fillna(all_data['Electrical'].mode()[0])

# KitchenQual:
# 只有1个缺失,用众数 TA 填充
all_data['KitchenQual'] = all_data['KitchenQual'].fillna(all_data['KitchenQual'].mode()[0])

# Exterior1st 和 Exterior2nd:
# 各缺1个,用众数填充
all_data['Exterior1st'] = all_data['Exterior1st'].fillna(all_data['Exterior1st'].mode()[0])
all_data['Exterior2nd'] = all_data['Exterior2nd'].fillna(all_data['Exterior2nd'].mode()[0])

# SaleType:
# 用众数 WD 填充
all_data['SaleType'] = all_data['SaleType'].fillna(all_data['SaleType'].mode()[0])

# MSSubClass:
# 缺失代表没有建筑类别 → 填 None
all_data['MSSubClass'] = all_data['MSSubClass'].fillna("None")
#Check remaining missing values if any 
# 检查是否还有剩余的缺失值
all_data_na = (all_data.isnull().sum() / len(all_data)) * 100
all_data_na = all_data_na.drop(all_data_na[all_data_na == 0].index).sort_values(ascending=False)
missing_data = pd.DataFrame({'Missing Ratio' :all_data_na})
missing_data.head()
Missing Ratio

3.特征工程

3.1将一些看似是数值、实际是分类的变量进行类型转换

# 将一些看似是数值、实际是分类的变量进行类型转换
# Transforming some numerical variables that are really categorical

# MSSubClass:房屋建筑类型,实际是分类特征,转成字符串类型
all_data['MSSubClass'] = all_data['MSSubClass'].apply(str)

# 将 OverallCond(整体状况)转换为分类变量
# Changing OverallCond into a categorical variable
all_data['OverallCond'] = all_data['OverallCond'].astype(str)

# 将销售年份、销售月份转换成分类特征
# Year and month sold are transformed into categorical features.
all_data['YrSold'] = all_data['YrSold'].astype(str)
all_data['MoSold'] = all_data['MoSold'].astype(str)

3.2对一些具有有序类别信息的分类变量进行标签编码

# 对一些具有有序类别信息的分类变量进行标签编码
# Label Encoding some categorical variables that may contain information in their ordering set

from sklearn.preprocessing import LabelEncoder

# 需要进行标签编码的特征列表(这些特征有高低/好坏之分)
cols = ('FireplaceQu', 'BsmtQual', 'BsmtCond', 'GarageQual', 'GarageCond', 
        'ExterQual', 'ExterCond','HeatingQC', 'PoolQC', 'KitchenQual', 'BsmtFinType1', 
        'BsmtFinType2', 'Functional', 'Fence', 'BsmtExposure', 'GarageFinish', 'LandSlope',
        'LotShape', 'PavedDrive', 'Street', 'Alley', 'CentralAir', 'MSSubClass', 'OverallCond', 
        'YrSold', 'MoSold')

# 遍历这些列,对分类特征进行标签编码
for c in cols:
    # 初始化标签编码器
    lbl = LabelEncoder() 
    # 编码器学习当前列的所有类别
    lbl.fit(list(all_data[c].values)) 
    # 将编码后的值替换回原数据
    all_data[c] = lbl.transform(list(all_data[c].values))

# 输出处理后数据的形状
print('Shape all_data: {}'.format(all_data.shape))
Shape all_data: (2917, 78)

Q1. 什么是 “有序分类特征”?

比如:

  • ExterQual(外墙质量):Po < Fa < TA < Gd < Ex
  • KitchenQual(厨房质量):差 < 中 < 好 < 极好
  • BsmtQual(地下室质量):无 < 差 < 中 < 好
  • FireplaceQu(壁炉质量)
  • GarageQual(车库质量)
  • Fence(围栏质量)
  • PoolQC(泳池质量)
  • HeatingQC(暖气质量)

它们不是平等的类别,而是有等级、有顺序、有好坏。

Q2. 为什么不能用独热编码?

独热编码会破坏顺序关系!

比如:

  • Ex(极好)
  • Gd(好)
  • TA(一般)

独热会变成:

Ex [1,0,0]
Gd [0,1,0]
TA [0,0,1]

模型完全不知道 Ex > Gd > TA,它会认为它们是平等、无差别的。

Q3. 为什么 LabelEncoder 是对的?

LabelEncoder 会把它们变成:

Ex → 4
Gd → 3
TA → 2
Fa → 1
Po → 0

模型能直接看懂大小关系:

数字越大 → 质量越好 → 房价越高

3.3新增重要特征:房屋总面积

# Adding total sqfootage feature 
# 增加【房屋总面积】特征(总地下室面积 + 1楼面积 + 2楼面积)
all_data['TotalSF'] = all_data['TotalBsmtSF'] + all_data['1stFlrSF'] + all_data['2ndFlrSF']

4.其他处理+收尾

4.1偏度特征分析

# =====================
# 偏度特征分析
# Skewed features
# =====================

# 筛选出所有数值型特征(排除object类型,即分类特征)
numeric_feats = all_data.dtypes[all_data.dtypes != "object"].index

# 计算所有数值特征的偏度(skew),并按从大到小排序
# 先dropna去掉缺失值,再计算偏度
skewed_feats = all_data[numeric_feats].apply(lambda x: skew(x.dropna())).sort_values(ascending=False)

# 输出提示文字
print("\nSkew in numerical features: \n")

# 把偏度结果存到DataFrame里
skewness = pd.DataFrame({'Skew' :skewed_feats})

# 查看偏度最大的前10个特征
skewness.head(10)
Skew in numerical features:

4.2Box-Cox 变换

对(高度)偏斜特征进行 Box-Cox 变换

我们使用 scipy 中的 boxcox1p 函数,它计算的是 1+x 的 Box-Cox 变换。

注意:当 λ=0 时,该变换等价于我们之前对目标变量使用的 log1p 变换。

更多关于 Box-Cox 变换以及该 scipy 函数的详细说明,可以参见相关页面。

# 筛选出偏度绝对值大于 0.75 的特征
skewness = skewness[abs(skewness) > 0.75]

# 打印提示:有多少个偏斜特征需要进行 Box-Cox 变换
print("There are {} skewed numerical features to Box Cox transform".format(skewness.shape[0]))

# 导入 Box-Cox 变换函数
from scipy.special import boxcox1p

# 获取需要变换的特征名称
skewed_features = skewness.index

# 设置 Box-Cox 的 lambda 参数
lam = 0.15

# 对每个偏斜特征循环执行 Box-Cox 变换
for feat in skewed_features:
    #all_data[feat] += 1
    all_data[feat] = boxcox1p(all_data[feat], lam)

# 注释掉的代码:也可以使用 log1p 进行对数变换
#all_data[skewed_features] = np.log1p(all_data[skewed_features])
There are 59 skewed numerical features to Box Cox transform

4.3独热编码

all_data = pd.get_dummies(all_data)
print(all_data.shape)
(2917, 220)

4.4划分数据集

train = all_data[:ntrain]
test = all_data[ntrain:]

三.建模部分

1.定义RMSE和k折交叉验证

# 导入库
# Import libraries

# 线性回归模型
from sklearn.linear_model import ElasticNet, Lasso, BayesianRidge, LassoLarsIC
# 集成学习模型
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
# 核岭回归
from sklearn.kernel_ridge import KernelRidge
# 构建流水线
from sklearn.pipeline import make_pipeline
# 鲁棒标准化(对异常值不敏感)
from sklearn.preprocessing import RobustScaler
# 自定义模型基类
from sklearn.base import BaseEstimator, TransformerMixin, RegressorMixin, clone
# 交叉验证工具
from sklearn.model_selection import KFold, cross_val_score, train_test_split
# 模型评估指标
from sklearn.metrics import mean_squared_error
# 梯度提升库
import xgboost as xgb
import lightgbm as lgb


# 定义交叉验证策略
# Define a cross validation strategy


# 我们使用 Sklearn 的 cross_val_score 函数。
# 但是这个函数没有 shuffle 参数,
# 所以我们添加一行代码,在交叉验证之前打乱数据集。


# 交叉验证折数
n_folds = 5

# 定义交叉验证函数(计算 RMSLE)
#Validation function
def rmsle_cv(model):
    # 设置5折交叉验证,打乱数据,随机种子固定
    kf = KFold(n_folds, shuffle=True, random_state=42).get_n_splits(train.values)
    # 计算负均方误差,取反并开根号,得到 RMSE
    rmse = np.sqrt(-cross_val_score(model, train.values, y_train, scoring="neg_mean_squared_error", cv=kf))
    # 返回每一折的 RMSE 误差
    return rmse

2.基础模型

2.1LASSO Regression

这个模型可能对异常值非常敏感。 因此我们需要让模型对异常值更加鲁棒(稳健)。为此,我们在流水线中使用 sklearn 的 Robustscaler() 方法。

# 构建 Lasso 模型流水线:先做鲁棒标准化,再训练 Lasso
lasso = make_pipeline(RobustScaler(), Lasso(alpha=0.0005, random_state=42))

2.2Elastic Net Regression

同样对异常值进行鲁棒处理

# 构建弹性网络回归模型:
# 先做鲁棒标准化,再传入 ElasticNet 进行训练
ENet = make_pipeline(RobustScaler(), ElasticNet(alpha=0.0005, l1_ratio=.9, random_state=3))
  • make_pipeline(...):把预处理和模型串成一条流水线
  • RobustScaler():对异常值不敏感的标准化
  • ElasticNet(...):弹性网络回归(结合 Lasso + Ridge)
    • alpha=0.0005:正则化强度
    • l1_ratio=0.9:L1 正则占比,接近 Lasso
    • random_state=3:固定随机种子,结果可复现

2.3Kernel Ridge Regression

# 核岭回归模型 (Kernel Ridge Regression)
KRR = KernelRidge(alpha=0.6, kernel='polynomial', degree=2, coef0=2.5)
  • KernelRidge:核岭回归,结合核方法与岭回归,能拟合非线性关系
  • alpha=0.6:正则化参数,控制模型复杂度,防止过拟合
  • kernel='polynomial':使用多项式核函数
  • degree=2:多项式次数为 2(二次多项式)
  • coef0=2.5:核函数的偏移项,提升多项式拟合效果

2.4Gradient Boosting Regression

使用 huber 损失函数,使得模型对异常值具有鲁棒性

# 梯度提升回归模型定义
GBoost = GradientBoostingRegressor(n_estimators=3000, learning_rate=0.05,
                                   max_depth=4, max_features='sqrt',
                                   min_samples_leaf=15, min_samples_split=10, 
                                   loss='huber', random_state=5)
  • n_estimators=3000树的数量,用 3000 棵树一起预测
  • learning_rate=0.05学习率,越小学得越精细、越稳定
  • max_depth=4每棵树最大深度 4,防止过深、过拟合
  • max_features='sqrt'每次分裂只用平方根数量的特征,增加随机性
  • min_samples_leaf=15叶子节点至少 15 个样本,让模型更平滑
  • min_samples_split=10节点至少 10 个样本才分裂,控制树复杂度
  • loss='huber':用 Huber 损失,对异常值不敏感
  • random_state=5固定随机种子,保证结果可复现

2.5XGBoost 

# 定义 XGBoost 回归模型
model_xgb = xgb.XGBRegressor(colsample_bytree=0.4603, gamma=0.0468, 
                             learning_rate=0.05, max_depth=3, 
                             min_child_weight=1.7817, n_estimators=2200,
                             reg_alpha=0.4640, reg_lambda=0.8571,
                             subsample=0.5213, silent=1,
                             random_state=7, nthread=-1)

1. 基础参数

  • learning_rate=0.05学习率,越小学得越细,越不容易过拟合
  • max_depth=3树的最大深度 3,很浅,防止过拟合
  • n_estimators=2200一共用 2200 棵树 集成预测

2. 控制随机性(防过拟合)

  • subsample=0.5213每次训练只用 52% 左右的样本
  • colsample_bytree=0.4603每次训练只用 46% 左右的特征

3. 正则化(核心)

  • reg_alpha=0.4640L1 正则(类似 Lasso)
  • reg_lambda=0.8571L2 正则(类似 Ridge)
  • gamma=0.0468分裂门槛,越大越保守

4. 其他

  • min_child_weight=1.7817叶子节点最小权重,控制树复杂度
  • silent=1不打印训练日志
  • random_state=7固定随机种子
  • nthread=-1使用所有 CPU 核心加速

2.6LightGBM 

# 定义 LightGBM 回归模型
model_lgb = lgb.LGBMRegressor(objective='regression',num_leaves=5,
                              learning_rate=0.05, n_estimators=720,
                              max_bin = 55, bagging_fraction = 0.8,
                              bagging_freq = 5, feature_fraction = 0.2319,
                              feature_fraction_seed=9, bagging_seed=9,
                              min_data_in_leaf =6, min_sum_hessian_in_leaf = 11)

1. 任务与基础

  • objective='regression'任务 = 回归(预测房价,连续值)
  • num_leaves=5每棵树最多 5 个叶子(树非常小,防过拟合)
  • learning_rate=0.05学习率小,训练更稳定
  • n_estimators=720使用 720 棵树

2. 速度与内存优化

  • max_bin=55把特征值分箱成 55 段,训练更快、更稳

3. 随机抽样(防过拟合)

  • bagging_fraction=0.8每次用 80% 样本训练
  • bagging_freq=5每 5 轮做一次样本抽样
  • feature_fraction=0.2319每次只用 23% 特征,增加随机性

4. 随机种子(保证结果可复现)

  • feature_fraction_seed=9
  • bagging_seed=9

5. 正则化(让模型更平稳)

  • min_data_in_leaf=6叶子至少 6 个样本
  • min_sum_hessian_in_leaf=11叶子最小二阶导数和,控制复杂度,防过拟合

3.基础模型分数

score = rmsle_cv(lasso)
print("\nLasso score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))
Lasso score: 0.1115 (0.0074)
score = rmsle_cv(ENet)
print("ElasticNet score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))
ElasticNet score: 0.1116 (0.0074)
score = rmsle_cv(KRR)
print("Kernel Ridge score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))
Kernel Ridge score: 0.1153 (0.0075)
score = rmsle_cv(GBoost)
print("Gradient Boosting score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))
Gradient Boosting score: 0.1177 (0.0080)
score = rmsle_cv(model_xgb)
print("Xgboost score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))
Xgboost score: 0.1161 (0.0079)
score = rmsle_cv(model_lgb)
print("LGBM score: {:.4f} ({:.4f})\n" .format(score.mean(), score.std()))
LGBM score: 0.1157 (0.0067)

4.模型堆叠

最简单的堆叠方法:平均基础模型我们从这种平均基础模型的简单方法入手。我们构建一个新类来对我们的模型扩展 scikit-learn 功能,同时利用封装和代码复用(继承)

4.1平均基础模型类

# 定义一个平均模型的类,继承 sklearn 基类,使其可以使用 fit/predict
class AveragingModels(BaseEstimator, RegressorMixin, TransformerMixin):

    # 初始化方法,传入要融合的多个模型
    def __init__(self, models):
        self.models = models  # 保存传入的模型列表
        
    # 训练函数:克隆每个模型,然后分别训练
    def fit(self, X, y):
        # 为每个模型创建一个独立副本,避免修改原始模型
        self.models_ = [clone(x) for x in self.models]
        
        # 遍历所有复制后的模型,分别训练
        for model in self.models_:
            model.fit(X, y)

        return self  # 返回自身,符合 sklearn 规范
    
    # 预测函数:对所有模型的预测结果取平均
    def predict(self, X):
        # 把每个模型的预测结果按列拼在一起
        predictions = np.column_stack([
            model.predict(X) for model in self.models_
        ])
        # 对每一行(每个样本)求平均值,作为最终预测结果
        return np.mean(predictions, axis=1)

4.2平均基础模型分数

我们在这里只对四个模型进行平均:弹性网络、梯度提升、核岭回归和套索回归。当然我们也可以轻松地加入更多模型来进行组合。

averaged_models = AveragingModels(models = (ENet, GBoost, KRR, lasso))

score = rmsle_cv(averaged_models)
print(" Averaged base models score: {:.4f} ({:.4f})\n".format(score.mean(), score.std()))
 Averaged base models score: 0.1091 (0.0075)

4.3更简单的堆叠:Meta-model

在这种方法中,我们在平均后的基础模型上添加一个元模型,并使用这些基础模型的折外预测结果来训练我们的元模型。

在训练部分,该过程可以描述如下:

将整个训练集划分为两个不相交的集合(这里是训练集和留出集)

在第一部分(训练集)上训练多个基础模型

在第二部分(留出集)上测试这些基础模型

将步骤 3 中的预测结果(称为折外预测)作为输入,将正确的响应(目标变量)作为输出,来训练一个更高级的学习器,称为元模型。

前三个步骤是迭代执行的。例如,如果我们采用 5 折堆叠,我们首先将训练数据划分为 5 折。然后我们将进行 5 次迭代。在每次迭代中,我们在 4 折数据上训练每个基础模型,并在剩余的一折(留出折)上进行预测。

因此,我们可以确保,在 5 次迭代之后,使用全部数据得到折外预测结果,然后我们将这些结果作为新特征,在步骤 4 中训练我们的元模型。

在预测部分,我们对所有基础模型在测试数据上的预测结果取平均,并将它们用作元特征,最终通过元模型完成最终预测。

4.4堆叠平均模型类

# 堆叠平均模型类
class StackingAveragedModels(BaseEstimator, RegressorMixin, TransformerMixin):
    # 初始化:传入基础模型、元模型、折数
    def __init__(self, base_models, meta_model, n_folds=5):
        self.base_models = base_models
        self.meta_model = meta_model
        self.n_folds = n_folds
   
    # 我们再次在原始模型的副本上拟合数据
    def fit(self, X, y):
        self.base_models_ = [list() for x in self.base_models]
        self.meta_model_ = clone(self.meta_model)
        kfold = KFold(n_splits=self.n_folds, shuffle=True, random_state=156)
        
        # 训练复制的基础模型,然后创建训练复制元模型所需的折外预测值
        out_of_fold_predictions = np.zeros((X.shape[0], len(self.base_models)))
        for i, model in enumerate(self.base_models):
            for train_index, holdout_index in kfold.split(X, y):
                instance = clone(model)
                self.base_models_[i].append(instance)
                instance.fit(X[train_index], y[train_index])
                y_pred = instance.predict(X[holdout_index])
                out_of_fold_predictions[holdout_index, i] = y_pred
                
        # 现在使用折外预测值作为新特征来训练复制的元模型
        self.meta_model_.fit(out_of_fold_predictions, y)
        return self
   
    # 在测试数据上对所有基础模型进行预测,并将平均预测值作为元特征
    # 最终预测由元模型完成
    def predict(self, X):
        meta_features = np.column_stack([
            np.column_stack([model.predict(X) for model in base_models]).mean(axis=1)
            for base_models in self.base_models_ ])
        return self.meta_model_.predict(meta_features)

4.5堆叠平均模型分数

# 定义堆叠平均模型:基础模型为 ENet、GBoost、KRR,元模型为 Lasso
stacked_averaged_models = StackingAveragedModels(base_models = (ENet, GBoost, KRR),
                                                 meta_model = lasso)

# 交叉验证评估堆叠模型
score = rmsle_cv(stacked_averaged_models)
# 输出分数:均值(标准差)
print("Stacking Averaged models score: {:.4f} ({:.4f})".format(score.mean(), score.std()))
Stacking Averaged models score: 0.1085 (0.0074)

5.最终训练与预测

5.1集成堆叠回归器、XGBoost 和 LightGBM

我们将 XGBoost 和 LightGBM 添加到之前定义的堆叠回归器中。

我们首先定义一个均方根对数误差评估函数

def rmsle(y, y_pred):
    return np.sqrt(mean_squared_error(y, y_pred))
# 最终训练与预测
# 堆叠回归器:

stacked_averaged_models.fit(train.values, y_train)
stacked_train_pred = stacked_averaged_models.predict(train.values)
stacked_pred = np.expm1(stacked_averaged_models.predict(test.values))
print(rmsle(y_train, stacked_train_pred))

# 1. test.values:把测试集特征数据转成numpy数组(适配模型输入格式)
# 2. stacked_averaged_models.predict(...):用训练好的堆叠模型对测试集做预测
# 3. np.expm1(...):对预测结果**指数还原**
#    → 因为训练前对房价标签做了 log(1+x) 处理,必须用 expm1 恢复真实房价
# 4. 最终结果 stacked_pred:测试集的**真实房价预测值**
0.0781571937916
# XGBoost:

model_xgb.fit(train, y_train)
xgb_train_pred = model_xgb.predict(train)
xgb_pred = np.expm1(model_xgb.predict(test))
print(rmsle(y_train, xgb_train_pred))
0.0785165142425
#LightGBM:

model_lgb.fit(train, y_train)
lgb_train_pred = model_lgb.predict(train)
lgb_pred = np.expm1(model_lgb.predict(test.values))
print(rmsle(y_train, lgb_train_pred))
0.0716757468834
# 注释:对全部训练数据进行【加权平均融合】时的 RMSLE 误差计算
'''RMSE on the entire Train data when averaging'''

# 打印输出提示文字
print('RMSLE score on train data:')

# 注释:
# 对三个模型的训练集预测结果进行【加权融合】
# stacked_train_pred  权重 0.70 → 占比70%(堆叠模型,权重最高)
# xgb_train_pred     权重 0.15 → 占比15%
# lgb_train_pred     权重 0.15 → 占比15%
# 融合后与真实标签 y_train 计算 RMSLE 误差
print(rmsle(y_train,stacked_train_pred*0.70 +
               xgb_train_pred*0.15 + lgb_train_pred*0.15 ))
RMSLE score on train data:
0.0752190464543

5.2预测及提交

# 集成预测:
# 将三个模型的测试集预测结果 按照权重加权求和,得到最终的融合预测值
ensemble = stacked_pred*0.70 + xgb_pred*0.15 + lgb_pred*0.15

# 生成提交文件
# 创建一个空的DataFrame用于保存提交结果
sub = pd.DataFrame()
# 把测试集的ID列存入提交文件
sub['Id'] = test_ID
# 把集成学习的最终预测结果(房价)存入SalePrice列
sub['SalePrice'] = ensemble
# 将结果保存为csv文件,不保存行索引,用于比赛提交
sub.to_csv('submission.csv',index=False)

四.一些思考总结

1.数据处理流程总结

【整个机器学习 Pipeline】
├─ 1. 库导入 + 读取数据(train + test)
│
├─ 2. EDA 探索性数据分析(只分析、不修改数据、不碰测试集y)
│   ├─ 异常值分析(画图、观察、不删除)
│   ├─ 目标变量分布分析(SalePrice)
│   ├─ 缺失值统计分析(计算缺失率)
│   ├─ 特征相关性分析(热力图)
│   └─ 数值特征偏度分析(计算skew)
│
├─ 3. 数据预处理(第一步:合并 train + test)
│   │
│   ├─ 【第一步:合并训练集 + 测试集】
│   │
│   ├─ 【子模块1:数据清洗】
│   │   ├─ 删除异常值
│   │   └─ 填充所有缺失值(None / 0 / 众数 / 中位数)
│   │
│   ├─ 【子模块2:特征工程】
│   │   ├─ 类型转换(数值转分类特征)
│   │   ├─ 有序特征标签编码(LabelEncoder)
│   │   ├─ 构造新特征(TotalSF 总面积)
│   │   ├─ 偏度特征 Box-Cox 变换
│   │   ├─ 无序特征独热编码
│   │   └─ 目标变量 log1p 变换(SalePrice)
│   │
│   └─ 【最后一步:拆分回 train / test】
│
└─ 4. 模型训练 & 预测(只用训练集!)
    ├─ 模型构建(Lasso / XGBoost / 堆叠)
    ├─ 模型训练
    ├─ 测试集预测
    └─ 提交结果

大类

包含的具体步骤(对应项目实操)

核心定义

EDA(探索性数据分析)

1. 异常值分析(仅观察、统计,不执行删除/修正动作);2. 目标变量(因变量)分布分析(如SalePrice分布观察);3. 缺失率分析(统计各特征缺失比例);4. 数据相关性分析(如特征间热力图分析);5. 数值特征偏度分析(计算各数值特征skew,判断分布偏斜程度)

独立前置阶段,仅对原始数据进行观察、统计、诊断,不修改任何数据,只找问题、定处理方案,不做“治疗”操作

数据清洗(Data Cleaning)

1. 异常值处理(执行删除/修正异常值的动作);2. 填充缺失值(按规则填充所有缺失值,如None、0、众数、中位数、按街区分组填充等)

数据预处理的基础子步骤,核心任务是“把脏数据弄干净”,解决数据中的缺失、异常等基础问题,为后续处理铺路

数据预处理(Data Preprocessing)

1. 合并训练集与测试集;2. 数据清洗(填充缺失值、处理异常值);3. 目标变量log1p变换;4. 类型转换(如数值型分类特征转字符串);5. 标签编码(有序分类特征);6. Box-Cox变换(偏斜特征分布修正);7. 独热编码(无序分类特征);8. 划分训练集与测试集

核心执行阶段,动手修改原始数据,将未处理的原始数据转换为模型可接受、可学习的格式,包含数据清洗、特征工程所有操作,是连接原始数据与建模的核心环节

特征工程(Feature Engineering)

1. 类型转换(将数值形式的分类特征转为字符串,修正特征类型);2. 标签编码(对有序分类特征进行编码,保留顺序信息);3. 新增特征(如构造TotalSF房屋总面积特征);4. Box-Cox变换(修正数值特征的偏斜分布,优化特征质量);5. 独热编码(对无序分类特征进行编码,避免顺序误导);6. 目标变量log1p变换(修正目标变量分布,提升模型拟合效果)

数据预处理的高级子步骤,核心任务是优化现有特征、构造新特征,提升特征与目标变量的关联性,让模型能更好地学习数据规律,是提升模型性能的关键

2.堆叠模型stacking的原理

2.1.核心原理

让一堆模型先预测,把它们的预测结果当成新特征,再训练一个最终模型来做决定。

2.2.标准 Stacking 流程(5 折为例)

把训练集分成 5 份:A B C D E

第一轮用 A B C D 训练基础模型 → 在 E 上预测得到 E 的预测值

第二轮用 A B C E 训练 → 在 D 上预测得到 D 的预测值

第三、四、五轮同理

最后会得到:全部训练集的 “模型预测值”这些预测值 = 新特征

最后一步

用这些新特征去训练一个元模型(比如 Lasso)元模型学会怎么组合这些预测才最准。

2.3代码里的 Stacking 到底在干嘛?

# 基础模型:3个
base_models = (ENet, GBoost, KRR)

# 元模型:1个(总指挥)
meta_model = lasso

训练时:

  1. 用 5 折交叉训练 3 个基础模型
  2. 得到每条训练样本的 3 个预测值
  3. 把这 3 个预测当成新特征
  4. 用 Lasso 学习怎么加权最准

预测时:

  1. 3 个模型分别预测测试集
  2. 把它们的预测送给 Lasso
  3. Lasso 输出最终房价

 2.4为什么要这么做?

  1. 每个模型擅长的地方不一样
  2. 有的擅长线性
  3. 有的擅长非线性
  4. Stacking 让它们互补→ 结果比任何单个模型都准

3.最后集成的权重怎么来的?

3.1经验值,凭感觉

3.2GridSearchCV 可以自动找权重

from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LinearRegression

# 把三个模型的预测结果拼在一起
train_meta = np.column_stack([stacked_train_pred, xgb_train_pred, lgb_train_pred])

# 用线性回归找最优权重
grid = LinearRegression(fit_intercept=False)
grid.fit(train_meta, y_train)

# 输出自动找到的权重!
print(grid.coef_)

运行后可能的输出:[0.68, 0.16, 0.16]

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐