[Kaggle] House Prices: Advanced Regression Techniques
房价预测是我入门Kaggle的第二个比赛,参考学习了他人的一篇优秀教程:https://www.kaggle.com/serigne/stacked-regressions-top-4-on-leaderboard
通过Serigne的这篇notebook,我学习到了关于数据分析、特征工程、集成学习等等很多有用的知识,在这里感谢一下这位大佬。
本篇文章立足于Serigne的教程,将他的大部分代码实现了一遍,修正了个别小错误,也加入了自己的一些视角和思考,做了一些自认为reasonable的“改进”。最终在Leaderboard上的得分为0.11676,排名前13%。虽然最后结果反而变差了一点(没有道理啊!QAQ),但我觉得整个实践的过程仍然值得记录一下。
废话不多说,下面进入正文。
数据集概览
导入相关Python包:
1 | #import some necessary librairies |
sample_submission.csv test.csv train.csv
读取csv文件:
1 | train = pd.read_csv('datasets/train.csv') |
查看训练、测试集的大小:
1 | #check the numbers of samples and features |
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)
特征工程
离群值处理
通过绘制散点图可以直观地看出特征是否有离群值,这里以GrLivArea为例。
1 | fig, ax = plt.subplots() |
我们可以看到图像右下角的两个点有着很大的GrLivArea,但相应的SalePrice却异常地低,我们有理由相信它们是离群值,要将其剔除。
1 | #Deleting outliers |
值得一提的是,删除离群值并不总是安全的。我们不能也不必将所有的离群值全部剔除,因为测试集中依然会有一些离群值。用带有一定噪声的数据训练出的模型会具有更高的鲁棒性,从而在测试集中表现得更好。
目标值分析
SalePrice是我们将要预测的目标,有必要对其进行分析和处理。
我们画出SalePrice的分布图和QQ图(Quantile Quantile Plot)。这里简单说一下QQ图,它是由标准正态分布的分位数为横坐标,样本值为纵坐标的散点图。如果QQ图上的点在一条直线附近,则说明数据近似于正态分布,且该直线的斜率为标准差,截距为均值。对于QQ图的详细介绍可以参考这篇文章:https://blog.csdn.net/hzwwpgmwy/article/details/79178485
1 | sns.distplot(train['SalePrice'] , fit=norm); |
mu = 180932.92 and sigma = 79467.79
SalePrice的分布呈正偏态,而线性回归模型要求因变量服从正态分布。我们对其做对数变换,让数据接近正态分布。
1 | #We use the numpy fuction log1p which applies log(1+x) to all elements of the column |
mu = 12.02 and sigma = 0.40
正态分布的数据有很多好的性质,使得后续的模型训练有更好的效果。另一方面,由于这次比赛最终是对预测值的对数的误差进行评估,所以我们在本地测试的时候也应该用同样的标准。
特征相关性
用相关性矩阵热图表现特征与目标值之间以及两两特征之间的相关程度,对特征的处理有指导意义。
1 | #Correlation map to see how features are correlated with SalePrice |
缺失值处理
首先将训练集和测试集合并在一起:
1 | ntrain = train.shape[0] |
all_data size is : (2917, 79)
统计各个特征的缺失情况:
1 | all_data_na = (all_data.isnull().sum() / len(all_data)) * 100 |
Missing Ratio | |
---|---|
PoolQC | 99.691 |
MiscFeature | 96.400 |
Alley | 93.212 |
Fence | 80.425 |
FireplaceQu | 48.680 |
LotFrontage | 16.661 |
GarageQual | 5.451 |
GarageCond | 5.451 |
GarageFinish | 5.451 |
GarageYrBlt | 5.451 |
GarageType | 5.382 |
BsmtExposure | 2.811 |
BsmtCond | 2.811 |
BsmtQual | 2.777 |
BsmtFinType2 | 2.743 |
BsmtFinType1 | 2.708 |
MasVnrType | 0.823 |
MasVnrArea | 0.788 |
MSZoning | 0.137 |
BsmtFullBath | 0.069 |
1 | f, ax = plt.subplots(figsize=(15, 12)) |
在data_description.txt中已有说明,一部分特征值的缺失是因为这些房子根本没有该项特征,对于这种情况我们统一用“None”或者“0”来填充。
1 | all_data["PoolQC"] = all_data["PoolQC"].fillna("None") |
对于缺失较少的离散型特征,可以用众数填补缺失值。
1 | all_data['MSZoning'] = all_data['MSZoning'].fillna(all_data['MSZoning'].mode()[0]) |
对于LotFrontage项,由于每个Neighborhood的房子的LotFrontage很可能是比较相近的,所以我们可以用各个房子所在Neighborhood的LotFrontage的中位数作为填充值。
1 | #Group by neighborhood and fill in missing value by the median LotFrontage of all the neighborhood |
data_description.txt中还提到过,Functional默认是“Typ”。
1 | all_data["Functional"] = all_data["Functional"].fillna("Typ") |
Utilities特征有两个缺失值,且只有一个样本是“NoSeWa”,除此之外全部都是“AllPub”,因此该项特征的方差非常小,我们可以直接将其删去。
1 | all_data = all_data.drop(['Utilities'], axis=1) |
最后确认缺失值是否已全部处理完毕:
1 | all_data.isnull().sum().max() |
0
进一步挖掘特征
我们注意到有些特征虽然是数值型的,但其实表征的只是不同类别,其数值的大小并没有实际意义,因此我们将其转化为类别特征。
1 | all_data['MSSubClass'] = all_data['MSSubClass'].astype(str) |
反过来,有些类别特征实际上有高低好坏之分,这些特征的质量越高,就可能在一定程度导致房价越高。我们将这些特征的类别映射成有大小的数字,以此来表征这种潜在的偏序关系。
1 | all_data['FireplaceQu'] = all_data['FireplaceQu'].map({'Ex': 5, 'Gd': 4, 'TA': 3, 'Fa': 2, 'Po': 1, 'None': 0}) |
利用一些重要的特征构造更多的特征:
1 | all_data['TotalSF'] = all_data['TotalBsmtSF'] + all_data['1stFlrSF'] + all_data['2ndFlrSF'] |
Box-Cox变换
对于数值型特征,我们希望它们尽量服从正态分布,也就是不希望这些特征出现正负偏态。
那么我们先来计算一下各个特征的偏度:
1 | numeric_feats = all_data.dtypes[all_data.dtypes != "object"].index |
Skew | |
---|---|
MiscVal | 21.940 |
PoolQC | 19.549 |
PoolArea | 17.689 |
LotArea | 13.109 |
LowQualFinSF | 12.085 |
3SsnPorch | 11.372 |
KitchenAbvGr | 4.301 |
BsmtFinSF2 | 4.145 |
Alley | 4.137 |
EnclosedPorch | 4.002 |
可以看到这些特征的偏度较高,需要做适当的处理。这里我们对数值型特征做Box-Cox变换,以改善数据的正态性、对称性和方差相等性。更多关于Box-Cox变换的知识可以参考这篇博客:https://blog.csdn.net/sinat_26917383/article/details/77864582
1 | skewness = skewness[abs(skewness['Skew']) > 0.75] |
There are 41 skewed numerical features to Box Cox transform
独热编码
对于类别特征,我们将其转化为独热编码,这样既解决了模型不好处理属性数据的问题,在一定程度上也起到了扩充特征的作用。
1 | all_data = pd.get_dummies(all_data) |
(2917, 254)
现在我们有了经过处理后的训练集和测试集:
1 | train = all_data[:ntrain] |
至此,特征工程就算完成了。
建立模型
导入算法包:
1 | from sklearn.linear_model import ElasticNet, Lasso |
标准化
由于数据集中依然存在一定的离群点,我们首先用RobustScaler对数据进行标准化处理。
1 | scaler = RobustScaler() |
评价函数
先定义一个评价函数。我们采用5折交叉验证。与比赛的评价标准一致,我们用Root-Mean-Squared-Error (RMSE)来为每个模型打分。
1 | #Validation function |
基本模型
- 套索回归
1 | lasso = Lasso(alpha=0.0005, random_state=1) |
- 弹性网络
1 | ENet = ElasticNet(alpha=0.0005, l1_ratio=.9, random_state=3) |
- 核岭回归
1 | KRR = KernelRidge(alpha=0.6, kernel='polynomial', degree=2, coef0=2.5) |
- 梯度提升回归
1 | GBoost = GradientBoostingRegressor(n_estimators=1000, learning_rate=0.05, |
- XGBoost
1 | model_xgb = xgb.XGBRegressor(colsample_bytree=0.5, gamma=0.05, |
- LightGBM
1 | model_lgb = lgb.LGBMRegressor(objective='regression',num_leaves=5, |
看看它们的表现如何:
1 | score = rmsle_cv(lasso) |
Lasso score: 0.1115 (0.0073)
ElasticNet score: 0.1115 (0.0073)
Kernel Ridge score: 0.1189 (0.0045)
Gradient Boosting score: 0.1140 (0.0085)
Xgboost score: 0.1185 (0.0081)
LGBM score: 0.1175 (0.0079)
Stacking方法
集成学习往往能进一步提高模型的准确性,Stacking是其中一种效果颇好的方法,简单来说就是学习各个基本模型的预测值来预测最终的结果。详细步骤可参考:https://www.jianshu.com/p/59313f43916f
这里我们用ENet、KRR和GBoost作为第一层学习器,用Lasso作为第二层学习器:
1 | class StackingAveragedModels(BaseEstimator, RegressorMixin, TransformerMixin): |
Stacking的交叉验证评分:
1 | stacked_averaged_models = StackingAveragedModels(base_models = (ENet, GBoost, KRR), |
Stacking Averaged models score: 0.1081 (0.0085)
我们得到了比单个基学习器更好的分数。
建立最终模型
我们将XGBoost、LightGBM和StackedRegressor以加权平均的方式融合在一起,建立最终的预测模型。
先定义一个评价函数:
1 | def rmsle(y, y_pred): |
用整个训练集训练模型,预测测试集的房价,并给出模型在训练集上的评分。
- StackedRegressor
1 | stacked_averaged_models.fit(train, y_train) |
0.08464515778854238
- XGBoost
1 | model_xgb.fit(train, y_train) |
0.08362948457258125
- LightGBM
1 | model_lgb.fit(train, y_train) |
0.06344397467222622
融合模型的评分:
1 | print('RMSLE score on train data:') |
RMSLE score on train data: 0.07939492590501797
预测
1 | ensemble = stacked_pred*0.70 + xgb_pred*0.15 + lgb_pred*0.15 |
生成提交文件
1 | sub = pd.DataFrame() |