对于刚刚入门机器学习的童孩来说,如何快速地通过不同实战演练以提高代码能力和流程理解是一个需要关注的问题。Kaggle平台正好提供了数据科学家的所需要的交流环境,并且为痴迷于人工智能的狂热的爱好者举办了各种类型的竞赛(如,数据科学/图像分类/图像识别/自然语言处理/漏洞检测)。
Kaggle社区是一种全球性的交流社区,集中大量优秀的AI科学家和数据分析家,能够相互分享实战经验和代码,并且有基础入门教程,对新手非常友好~
房价是一个生活中耳熟能详的概念,在大城市买房尤其成为了上班族几乎最大的苦恼(以后即将面临····),而在美国的爱荷华州埃姆斯市有许多因素影响着房屋的最终价格,例如房屋面积、地下室、浴室和车库等等;
kaggle平台收集了约80个可能影响房价的特征变量,要求数据科学家利用机器学习等工具对房价进行预测,即该案例是一种简单的回归问题。
官方提供的房屋特征描述文件我已翻译成中文,供大家参考。英文原版的可以点击Kaggle竞赛栏目下的下载按钮,数据集也是一样。如下所示:
接下来的工作就是基于这些特征进行数据挖掘和构建模型来预测了。整体流程的思路如下:
import numpy as np #基本矩阵计算工具
import pandas as pd #基本数据可视化工具
import matplotlib.pyplot as plt #绘图工具
import seaborn as sns
from datetime import datetime #记录时间
from scipy.stats import skew #偏度计算
from scipy.special import boxcox1p #box-cox变换工具
from scipy.stats import boxcox_normmax
from sklearn.linear_model import LinearRegression, ElasticNetCV, LassoCV, RidgeCV #线性模型
from sklearn.ensemble import GradientBoostingRegressor #GBDT模型
from sklearn.svm import SVR #SVR模型
from sklearn.pipeline import make_pipeline #构建Pipeline
from sklearn.preprocessing import RobustScaler #稳健标准化,用于缩放包含许多异常值的数据
from sklearn.model_selection import KFold, RepeatedKFold, cross_val_score, GridSearchCV #K折取样以及交叉验证
from sklearn.metrics import mean_squared_error #均方根指标
from mlxtend.regressor import StackingCVRegressor #带交叉验证的Stacking回归器
from xgboost import XGBRegressor #XGBoost模型
from lightgbm import LGBMRegressor #LGB模型
import warnings #系统警告提示
import os #系统读取工具
warnings.filterwarnings('ignore') #忽略警告
#文件根目录,输入本地下载好的文件目录地址
DATA_ROOT = 'D:/Kaggle比赛/房价回归预测/'
print(os.listdir(DATA_ROOT))
['data_description.txt', 'House_price_submission.csv', 'sample_submission.csv', 'test.csv', 'test_results.csv', 'train.csv', '数据描述中文介绍.txt']
#导入训练集、测试集和提交样本
train = pd.read_csv(f'{DATA_ROOT}/train.csv')
test = pd.read_csv(f'{DATA_ROOT}/test.csv')
sub = pd.read_csv(f'{DATA_ROOT}/sample_submission.csv')
#打印数据维度
print("Train set size:", train.shape)
print("Test set size:", test.shape)
输出结果:
Train set size: (1460, 81) , Test set size: (1459, 80)
#查看训练集数据摘要
print(train.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1460 entries, 0 to 1459
Data columns (total 81 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Id 1460 non-null int64
1 MSSubClass 1460 non-null int64
2 MSZoning 1460 non-null object
3 LotFrontage 1201 non-null float64
4 LotArea 1460 non-null int64
5 Street 1460 non-null object
6 Alley 91 non-null object
7 LotShape 1460 non-null object
8 LandContour 1460 non-null object
9 Utilities 1460 non-null object
10 LotConfig 1460 non-null object
......
#查看测试集数据摘要
print(test.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1460 entries, 0 to 1459
Data columns (total 81 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Id 1460 non-null int64
1 MSSubClass 1460 non-null int64
2 MSZoning 1460 non-null object
3 LotFrontage 1201 non-null float64
4 LotArea 1460 non-null int64
5 Street 1460 non-null object
6 Alley 91 non-null object
7 LotShape 1460 non-null object
8 LandContour 1460 non-null object
9 Utilities 1460 non-null object
10 LotConfig 1460 non-null object
.....
通过简单粗略看数据,我们知道这里有着数值型变量和非数值变量(类别型变量),除开ID和SalePrice以外共有79个特征。
#先将样本ID赋值并删除
train_ID = train['Id']
test_ID = test['Id']
train.drop(['Id'], axis=1, inplace=True)
test.drop(['Id'], axis=1, inplace=True)
#整理出数值型特征和类别型特征
all_cols = test.columns.tolist()
numerical_cols = []
categorical_cols = []
for col in all_cols:
if (test[col].dtype != 'object') :
numerical_cols.append(col)
else:
categorical_cols.append(col)
print('数值型变量数目为:',len(numerical_cols))
print('类别型变量数目为:',len(categorical_cols))
数值型变量数目为: 36
类别型变量数目为: 43
#对训练集的连续性数值变量绘制箱型图筛选异常值
fig = plt.figure(figsize=(80,60),dpi=120)
for i in range(len(numerical_cols)):
plt.subplot(6, 6, i+1)
sns.boxplot(train[numerical_cols[i]], orient='v', width=0.5)
plt.ylabel(numerical_cols[i], fontsize=36)
plt.show()
#地面上居住面积与房屋售价关系
fig = plt.figure(figsize=(6,5))
plt.axvline(x=4600, color='r', linestyle='--')
sns.scatterplot(x='GrLivArea',y='SalePrice',data=train, alpha=0.6)
#显然对于可居住面积越大,其售价肯定也越高,但图中显示有两个离散点不遵循此规则,查看其具体的数值
train.GrLivArea.sort_values(ascending=False)[:4]
1298 5642
523 4676
1182 4476
691 4316
Name: GrLivArea, dtype: int64
#地皮建筑面积与房屋售价关系
fig = plt.figure(figsize=(6,5))
plt.axvline(x=200000, color='r', linestyle='--')
sns.scatterplot(x='LotArea',y='SalePrice',data=train, alpha=0.6)
*强#地皮建筑面积与房屋售价关系
fig = plt.figure(figsize=(6,5))
plt.axvline(x=200000, color='r', linestyle='--')
sns.scatterplot(x='LotArea',y='SalePrice',data=train, alpha=0.6)
(通过数据集中能看出,对于地皮建筑面积越大,其售价却不一定更高,二者不成正比,因此异常值不用删除)
#地下室总面积与房屋售价关系
fig = plt.figure(figsize=(6,5))
plt.axvline(x=5900, color='r', linestyle='--')
sns.scatterplot(x='TotalBsmtSF',y='SalePrice',data=train, alpha=0.6)
#同上,查看其具体的数值
train.TotalBsmtSF.sort_values(ascending=False)[:3]
1298 6110
332 3206
496 3200
Name: TotalBsmtSF, dtype: int64
#第一层面积与房屋售价关系
fig = plt.figure(figsize=(6,5))
plt.axvline(x=4000, color='r', linestyle='--')
sns.scatterplot(x='1stFlrSF',y='SalePrice',data=train, alpha=0.6)
#同上,查看其具体的数值
train['1stFlrSF'].sort_values(ascending=False)[:3]
1298 4692
496 3228
523 3138
Name: 1stFlrSF, dtype: int64
你会发现原来这几个特征的离群点都是Index=1298的这个样本.
#装饰石材面积与房屋售价关系
fig = plt.figure(figsize=(6,5))
plt.axvline(x=1500, color='r', linestyle='--')
sns.scatterplot(x='MasVnrArea',y='SalePrice',data=train, alpha=0.6)
通过数据集中能看出,对于装饰石材面积越大,其售价却不一定更高,还需要看石材的类型,因此异常值不用删除。还有其余特征变量可以用来探索,具体方式是先看箱型图,再细看可能会存在离群值的一些特征做散点图,最最重要的就是不要过分地删除异常值,一定要基于人为经验或者可观事实判断。比如,住房面积大房价却很低,人的年龄超过200岁,月份数为-1等等。
综上,需要将部分异常值删除。
#剔除异常值并将数据集重新排序
train = train[train.GrLivArea < 4600]
train = train[train.TotalBsmtSF < 5000]
train = train[train['1stFlrSF'] < 4000]
train.reset_index(drop=True, inplace=True)
train.shape
(1458, 80)
先对咱们的标签(房价)做一下偏度图,一般用直方图和Q-Q图来看。
不懂Q-Q图的小伙伴可以移步这里~
#对'SalePrice'绘制直方图和Q-Q图
from scipy import stats
plt.figure(figsize=(10,5))
ax_121 = plt.subplot(1,2,1)
sns.distplot(train["SalePrice"],fit=stats.norm)
ax_122 = plt.subplot(1,2,2)
res = stats.probplot(train["SalePrice"],plot=plt)
可见,咱们的房价分布并不完全符合正态,而是一种向左的偏态分布。
由于该竞赛最终的评估指标是取房价对数的RMSE值,因此有必要先将房价转化为对数形式,方便后续用于模型的评估。(这里可以用numpy.log()或者numpy.log1p()将数值转化为对数。注意,log()是指e为底数,而log1p代表了ln(1+x))
#使用log1p也就是log(1+x),用来对房价数据进行数据预处理,它的好处是转化后的数据更加服从正态分布,有利于后续的评估结果。
#但需要注意最后需要将预测出的平滑数据还原,而还原过程就是log1p的逆运算expm1
train["SalePrice"] = np.log1p(train["SalePrice"])
plt.figure(figsize=(10,5))
ax_121 = plt.subplot(1,2,1)
sns.distplot(train["SalePrice"],fit=stats.norm)
ax_122 = plt.subplot(1,2,2)
res = stats.probplot(train["SalePrice"],plot=plt)
接下来需要合并训练和测试数据,做一些统一的预处理变化,如果分开做会显得比较麻烦。
#分离标签和特征,合并训练集和测试集便于统一预处理
y = train['SalePrice'].reset_index(drop=True)
train_features = train.drop(['SalePrice'], axis=1)
test_features = testfeatures = pd.concat([train_features, test_features],axis=0).reset_index(drop=True)
print("剔除训练数据中的极端值后,将其特征矩阵和测试数据中的特征矩阵合并,维度为:",features.shape)
剔除训练数据中的极端值后,将其特征矩阵和测试数据中的特征矩阵合并,维度为: (2917, 79)
通过阅读官方提供的说明文件(这一点很重要)能够加深对数据特征的理解,以便更好的进行特征处理。在这里,我们会发现有一些特征本身是数值型的数据,但是却没有连续值,而是一些单一分布的值,因此需要检验它们是不是原本就是类别型的数据,只不过用数值来表达了。
#寻找数值变量中实际应该为类别变量的特征(即并不连续分布)
transform_cols = []
for col in numerical_cols:
if len(features[col].unique()) < 20:
transform_cols.append(col)
transform_cols
['MSSubClass',
'OverallQual',
'OverallCond',
'BsmtFullBath',
'BsmtHalfBath',
'FullBath',
'HalfBath',
'BedroomAbvGr',
'KitchenAbvGr',
'TotRmsAbvGrd',
'Fireplaces',
'GarageCars',
'PoolArea',
'MoSold',
'YrSold']
通过对比文件描述 (data_distribution) 中的特征含义:
故此,数值型变量中存在列名为’MSSubClass’、‘YrSold’、'MoSold’的特征列,实际为one-hot类别型变量需要更正为string形式。 (不懂one-hot和label_encoder区别的伙伴点这里)
#对于列名为'MSSubClass'、'YrSold'、'MoSold'的特征列,将列中的数据类型转化为string格式。
features['MSSubClass'] = features['MSSubClass'].apply(str)
features['YrSold'] = features['YrSold'].astype(str)
features['MoSold'] = features['MoSold'].astype(str)
#将其加入对应的组别
numerical_cols.remove('MSSubClass')
numerical_cols.remove('YrSold')
numerical_cols.remove('MoSold')
categorical_cols.append('MSSubClass')
categorical_cols.append('YrSold')
categorical_cols.append('MoSold')
由dataframe.info()能看出对于训练和测试数据都有不同程度的缺失情况,而缺失值的存在会导致模型无法工作,因此需要题前将这部分数据处理好。
#数据总缺失情况查阅
(features.isna().sum()/features.shape[0]).sort_values(ascending=False)[:35]
PoolQC 0.996915
MiscFeature 0.964004
Alley 0.932122
Fence 0.804251
FireplaceQu 0.486802
LotFrontage 0.166610
GarageCond 0.054508
GarageQual 0.054508
GarageYrBlt 0.054508
GarageFinish 0.054508
GarageType 0.053822
BsmtCond 0.028111
......
GarageArea 0.000343
GarageCars 0.000343
OverallQual 0.000000
dtype: float64
注意,由特征文件说明中信息可知许多NA项并非缺失,而是表示“没有”此功能的含义, 如PoolQC游泳池质量的缺失NA,实际含义表示没有游泳池,故需要仔细对照说明信息进行处理。
以下根据缺失值实际情况进行填充:
#PoolQC, NA表示没有游泳池,为一个类型
print(features["PoolQC"].unique())
print(features["PoolQC"].fillna("None").unique()) #空值填充为str型数据"None",表示没有泳池。
[nan 'Ex' 'Fa' 'Gd']
['None' 'Ex' 'Fa' 'Gd']
#MiscFeature, NA表示-其他类别中“没有”未涵盖的其他特性,故填充为"None"
print(features["MiscFeature"].unique())
print(features["MiscFeature"].fillna("None").unique())
[nan 'Shed' 'Gar2' 'Othr' 'TenC']
['None' 'Shed' 'Gar2' 'Othr' 'TenC']
#由于类别型变量的许多NA均表示没有此功能,先从data_distribution中找出这样的列然后统一填充为"None"
(features[categorical_cols].isna().sum()/features.shape[0]).sort_values(ascending=False)[:25]
PoolQC 0.996915
MiscFeature 0.964004
Alley 0.932122
Fence 0.804251
FireplaceQu 0.486802
GarageCond 0.054508
.....
SaleType 0.000343
KitchenQual 0.000343
LotShape 0.000000
LandContour 0.000000
dtype: float64
for col in ('PoolQC', 'MiscFeature','Alley', 'Fence', 'FireplaceQu', 'MasVnrType', 'Utilities',
'GarageCond', 'GarageQual', 'GarageFinish', 'GarageType',
'BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2'):
features[col] = features[col].fillna('None')
(features[categorical_cols].isna().sum()/features.shape[0]).sort_values(ascending=False)[:10]
MSZoning 0.001371
Functional 0.000686
SaleType 0.000343
Exterior2nd 0.000343
Exterior1st 0.000343
Electrical 0.000343
KitchenQual 0.000343
BldgType 0.000000
ExterQual 0.000000
MasVnrType 0.000000
dtype: float64
#其余类别型变量由所在列的众数填充
for col in ('Functional', 'SaleType', 'Electrical', 'Exterior2nd', 'Exterior1st', 'KitchenQual'):
features[col] = features[col].fillna(features[col].mode()[0])
(features[categorical_cols].isna().sum()/features.shape[0]).sort_values(ascending=False)[:3]
MSZoning 0.001371
BldgType 0.000000
Foundation 0.000000
dtype: float64
#由于MSSubClass(确定销售涉及的住宅类型)和 MSZoning(销售分区的一般分类确定)之间有一定联系。
#具体来说是指在MSSubClass基础上确定MSZoning,故可以按照'MSSubClass'列中的元素分布进行分组,然后将'MSZoning'列分组后取众数填充。
features['MSZoning'] = features.groupby('MSSubClass')['MSZoning'].transform(lambda x: x.fillna(x.mode()[0]))
print('类别型数据缺失值数量为:', features[categorical_cols].isna().sum().sum())
类别型数据缺失值数量为: 0
最后的df.groupby()工具用法详见: Groupby的用法及原理详解
到这里,类别型数据缺失填充已经完成啦~
接下来就是数值型的特征:
#数值型变量缺失情况
(features[numerical_cols].isna().sum()/features.shape[0]).sort_values(ascending=False)[:12]
LotFrontage 0.166610
GarageYrBlt 0.054508
MasVnrArea 0.007885
BsmtFullBath 0.000686
BsmtHalfBath 0.000686
GarageArea 0.000343
GarageCars 0.000343
BsmtFinSF1 0.000343
BsmtFinSF2 0.000343
BsmtUnfSF 0.000343
TotalBsmtSF 0.000343
OpenPorchSF 0.000000
dtype: float64
#因为某些类别型变量为"None",表示不包含此项,所以造成数值型变量也会缺失,故将这样的数值变量缺失值填充为"0"
for col in ('GarageYrBlt', 'GarageArea', 'GarageCars', 'MasVnrArea',
'BsmtHalfBath', 'BsmtFullBath', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF'):
features[col] = features[col].fillna(0)
(features[numerical_cols].isna().sum()/features.shape[0]).sort_values(ascending=False)[:3]
LotFrontage 0.16661
BsmtFullBath 0.00000
LotArea 0.00000
dtype: float64
#对于 LotFrontage (连接到地产的街道的直线英尺距离)而言,其受Neighborhood(城市限制内的物理位置)的影响
#故对于这两个特征进行分组后取列的中位数填充
features['LotFrontage'] = features.groupby('Neighborhood')['LotFrontage'].transform(lambda x: x.fillna(x.median()))
print('数值型数据缺失值数量为:',features[numerical_cols].isna().sum().sum())
数值型数据缺失值数量为: 0
至此,数据缺失值填充全部完成!!(先放一个小烟花,嘣~ 嘣 ~ 嘣~)
这一步是整个Baseline中最核心的部分,特征工程的好坏将影响最终的模型效果。因此,业界都流传着一句话:“数据和特征决定了机器学习的上线,而模型和算法只是逼近这个上线而已”, 由此可见特征工程在机器学习中的重要性。具体来说,特征越好、灵活性越强,则构建的模型越简单且性能出色。(更多关于特征工程的知识请参考:机器学习实战之特征工程)
#GrLivArea: 地上居住总面积
#TotalBsmtSF: 地下室总面积
#将二者加和形成新的“总居住面积”特征
features['TotalSF'] = features['GrLivArea'] + features['TotalBsmtSF']
#LotArea: 建筑面积
#LotFrontage: 房子同街道之间的距离
#将二者乘积形成新的“区域面积”特征
features['Area'] = features['LotArea'] * features['LotFrontage']
#OpenPorchSF :开放式门廊面积
#EnclosedPorch :封闭式门廊面积
#3SsnPorch :时令门廊面积
#ScreenPorch :屏风门廊面积
#将四者加和形成新的"门廊总面积"特征
features['Total_porch_sf'] = (features['OpenPorchSF'] + features['EnclosedPorch'] +
features['3SsnPorch'] + features['ScreenPorch'])
#FullBath :地面上的全浴室数目
#HalfBath :地面以上半浴室数目
#BsmtFullBath :地下室全浴室数量
#BsmtHalfBath :地下室半浴室数量
#将半浴室权重设为0.5,全浴室为1,将四者加和形成新的"总浴室数目"特征
features['Total_Bathrooms'] = (features['FullBath'] + (0.5 * features['HalfBath']) +
features['BsmtFullBath'] + (0.5 * features['BsmtHalfBath']))
#将新特征加入到数值变量中
numerical_cols.append('TotalSF')
numerical_cols.append('Area')
numerical_cols.append('Total_porch_sf')
numerical_cols.append('Total_Bathrooms')
print('特征创建后的数据维度 :', features.shape)
特征创建后的数据维度 : (2917, 83)
小伙伴们可以根据自己对特征的理解来自定义构建新的特征,这里就因人而异了,充分发挥你们的创造力吧,奥里给~~
许多与房价属性高度相关的特征可能需要分箱 binning 来表达更明确的含义,或者有效地去减少对于数值的拟合来增加其泛化性(在测试集上的准确度)。
分箱也是一门学问,我还是把知识链接给放上吧…
#查看与标签10个最相关的特征属性
train_ = features.iloc[:len(y),:]
train_ = pd.concat([train_,y],axis=1)
cols = train_ .corr().nlargest(10, 'SalePrice').index
plt.subplots(figsize=(8,8))
sns.set(font_scale=1.1)
sns.heatmap(train_ [cols].corr(),square=True, annot=True)
由热图可知,‘完工质量和材料’,‘总居住面积’,‘地面上居住面积’,'车库容量数’,‘总浴室数目’,‘车库面积’,‘总地下室面积’,'第一层面积’等都是与房价密切相关的特征。
#完工质量和材料
sns.distplot(features['OverallQual'],bins=10,kde=False)
#完工质量和材料分组
def OverallQual_category(cat):
if cat <= 4:
return 1
elif cat <= 6 and cat > 4:
return 2
else:
return 3
features['OverallQual_cat'] = features['OverallQual'].apply(OverallQual_category)
#总居住面积
sns.distplot(features['TotalSF'],bins=10,kde=False)
#总居住面积分组
def TotalSF_category(cat):
if cat <= 2000:
return 1
elif cat <= 3000 and cat > 2000:
return 2
elif cat <= 4000 and cat > 3000:
return 3
else:
return 4
features['TotalSF_cat'] = features['TotalSF'].apply(TotalSF_category)
博主后面还进行了车库面积、地面上居住面积、地下室总面积、建筑相关时间等特征的分箱操作,原理都一样,这里不再贴代码。
#然后将创建的分组加入类别型变量中
categorical_cols.append('GarageArea_cat')
categorical_cols.append('GrLivArea_cat')
categorical_cols.append('TotalBsmtSF_cat')
categorical_cols.append('TotalSF_cat')
categorical_cols.append('OverallQual_cat')
categorical_cols.append('LotFrontage_cat')
categorical_cols.append('YearBuilt_cat')
categorical_cols.append('YearRemodAdd_cat')
categorical_cols.append('GarageYrBlt_cat')
#打印当前数据维度
features.shape
(2917, 92)
针对一些线性回归模型,它们本身对数据分布有一定要求,例如正态分布等。所以需要在使用这些模型之前将所使用的特征尽可能转化为正态分布状态,就需要对数据的偏度和峰度进行了解和转化。不了解数据偏度和峰度的小伙伴看这里。
#查看数值型特征变量的偏度情况并绘图
skew_features = features[numerical_cols].apply(lambda x: skew(x)).sort_values(ascending=False)
sns.set_style("white")
f, ax = plt.subplots(figsize=(8, 12))
ax.set_xscale("log")
ax = sns.boxplot(data=features[numerical_cols], orient="h", palette="Set1")
ax.xaxis.grid(False)
ax.set(ylabel="Feature names")
ax.set(xlabel="Numeric values")
ax.set(title="Numeric Distribution of Features")
sns.despine(trim=True, left=True)
#对特征变量'GrLivArea',绘制直方图和Q-Q图,以清楚数据分布结构
plt.figure(figsize=(8,4))
ax_121 = plt.subplot(1,2,1)
sns.distplot(features['GrLivArea'],fit=stats.norm)
ax_122 = plt.subplot(1,2,2)
res = stats.probplot(features['GrLivArea'],plot=plt)
#以0.5作为阈值,统计偏度超过此数值的高偏度分布数据列,获取这些数据列的index
high_skew = skew_features[skew_features > 0.5]
skew_index = high_skew.index
print("There are {} numerical features with Skew > 0.5 :".format(high_skew.shape[0]))
high_skew.sort_values(ascending=False)
There are 28 numerical features with Skew > 0.5 :
MiscVal 21.939672
Area 18.642721
PoolArea 17.688664
LotArea 13.109495
LowQualFinSF 12.084539
3SsnPorch 11.372080
...
HalfBath 0.696666
TotalBsmtSF 0.671751
BsmtFullBath 0.622415
OverallCond 0.569314
dtype: float64
对高偏度数据进行处理,将其转化为正态分布时,一般使用Box-Cox变换。它可以使数据满足线性性、独立性、方差齐次以及正态性的同时,又不丢失信息。
#使用boxcox_normmax用于找出最佳的λ值
for i in skew_index:
features[i] = boxcox1p(features[i], boxcox_normmax(features[i] + 1))
features[numerical_cols].apply(lambda x: skew(x)).sort_values(ascending=False)
BsmtFinSF2 2.578329
EnclosedPorch 2.149132
Area 1.000000
MasVnrArea 0.977618
2ndFlrSF 0.895453
WoodDeckSF 0.785550
HalfBath 0.732625
OpenPorchSF 0.621231
BsmtFullBath 0.616643
Fireplaces 0.553135
.....
GarageArea 0.216857
OverallQual 0.189591
FullBath 0.165514
LotFrontage 0.059189
BsmtUnfSF 0.054195
TotRmsAbvGrd 0.047190
TotalSF 0.027351
GrLivArea 0.008823
dtype: float64
#box-cox变换后的对特征变量'GrLivArea'
plt.figure(figsize=(8,4))
ax_121 = plt.subplot(1,2,1)
sns.distplot(features['GrLivArea'],fit=stats.norm)
ax_122 = plt.subplot(1,2,2)
res = stats.probplot(features['GrLivArea'],plot=plt)
(呼~好累,活动一下手臂继续肝!!)
在某些类别型特征中,某个种类占据了99%以上的部分,也就是说特征之间的具有明显的单一值特点,这些特征对模型也没有什么贡献可言,需要删除。
查看类别型特征的唯一值分布情况
features[categorical_cols].describe(include='O').T
count unique top freq
MSZoning 2917 5 RL 2265
Street 2917 2 Pave 2905
Alley 2917 3 None 2719
LotShape 2917 4 Reg 1859
LandContour 2917 4 Lvl 2622
Utilities 2917 3 AllPub 2914
LotConfig 2917 5 Inside 2132
......
SaleType 2917 9 WD 2526
SaleCondition 2917 6 Normal 2402
MSSubClass 2917 16 20 1079
YrSold 2917 5 2007 691
MoSold 2917 12 6 503
#对于类别型特征变量中,单个类型占比超过99%以上的特征(即> 2888个)进行删除.
freq_ = features[categorical_cols].describe(include='O').T.freq
drop_cols = []
for index,num in enumerate(freq_):
if (freq_[index] > 2888) :
drop_cols.append(freq_.index[index])
features = features.drop(drop_cols, axis=1)
print('These drop_cols are:', drop_cols)
print('The new shape is :', features.shape)
categorical_cols.remove('Street')
categorical_cols.remove('PoolQC')
categorical_cols.remove('Utilities')
These drop_cols are: ['Street', 'Utilities', 'PoolQC']
The new shape is : (2917, 89)
对于某些分布单调的数字型数据列, 按照“有”和“没有”来进行二值化处理,以扩充更多地特征维度。
#通过对于特征含义理解,筛选出了以下几个变量进行二值化处理
features['HasPool'] = features['PoolArea'].apply(lambda x: 1 if x > 0 else 0)
features['HasWoodDeckSF'] = features['WoodDeckSF'].apply(lambda x: 1 if x > 0 else 0)
features['Hasfireplace'] = features['Fireplaces'].apply(lambda x: 1 if x > 0 else 0)
features['HasBsmt'] = features['TotalBsmtSF'].apply(lambda x: 1 if x > 0 else 0)
features['HasGarage'] = features['GarageArea'].apply(lambda x: 1 if x > 0 else 0)
#查看当前特征数
print("经过特征处理后的特征维度为 :",features.shape)
经过特征处理后的特征维度为 : (2917, 94)
至此,特征构造处理完成全部完成!
对于类别型数据,一般采用独热编码onehot形式,对于彼此有数量关联的特征一般采用labelencoder编码。
#使用pd.get_dummies()方法对特征矩阵进行类似“坐标投影”操作,获得在新空间下onehot的特征表达。
final_features = pd.get_dummies(features,columns=categorical_cols).reset_index(drop=True)
print("经过onehot编码后的特征维度为 :", final_features.shape)
经过onehot编码后的特征维度为 : (2917, 370)
#训练集&测试集数据还原
X_train = final_features.iloc[:len(y), :]
X_sub = final_features.iloc[len(y):, :]
print("训练集特征维度为:", X_train.shape)
print("测试集特征维度为:", X_sub.shape)
训练集特征维度为: (1458, 370)
测试集特征维度为: (1459, 370)
除了根据可视化的异常值筛查以外,使用模型对数据进行拟合,然后设定一个残差阈值(y_true - y_pred) 也能从另一个角度找出可能潜在的异常值。
#定义回归模型找出异常值并绘图的函数
def find_outliers(model, X, y, sigma=4):
try:
y_pred = pd.Series(model.predict(X), index=y.index)
except:
model.fit(X,y)
y_pred = pd.Series(model.predict(X), index=y.index)
#计算模型预测y值与真实y值之间的残差
resid = y - y_pred
mean_resid = resid.mean()
std_resid = resid.std()
#计算异常值定义的参数z参数,数据的|z|大于σ将会被视为异常
z = (resid - mean_resid) / std_resid
outliers = z[abs(z) > sigma].index
#打印结果并绘制图像
print('R2 = ',model.score(X,y))
print('MSE = ',mean_squared_error(y, y_pred))
print('RMSE = ',np.sqrt(mean_squared_error(y, y_pred)))
print('------------------------------------------')
print('mean of residuals',mean_resid)
print('std of residuals',std_resid)
print('------------------------------------------')
print(f'find {len(outliers)}','outliers:')
print(outliers.tolist())
plt.figure(figsize=(15,5))
ax_131 = plt.subplot(1,3,1)
plt.plot(y,y_pred,'.')
plt.plot(y.loc[outliers],y_pred.loc[outliers],'ro')
plt.legend(['Accepted','Outliers'])
plt.xlabel('y')
plt.ylabel('y_pred');
ax_132 = plt.subplot(1,3,2)
plt.plot(y, y-y_pred, '.')
plt.plot(y.loc[outliers],y.loc[outliers] - y_pred.loc[outliers],'ro')
plt.legend(['Accepted','Outliers'])
plt.xlabel('y')
plt.ylabel('y - y_pred');
ax_133 = plt.subplot(1,3,3)
z.plot.hist(bins=50, ax=ax_133)
z.loc[outliers].plot.hist(color='r', bins=30, ax=ax_133)
plt.legend(['Accepted','Outliers'])
plt.xlabel('z')
return outliers
#使用LR回归模型
outliers_lr = find_outliers(LinearRegression(), X_train, y, sigma=3.5)
R2 = 0.9533461995514986
MSE = 0.007448781362371816
RMSE = 0.08630632284121376
------------------------------------------
mean of residuals -2.8022090059126034e-17
std of residuals 0.08633593557937841
------------------------------------------
find 15 outliers:
[30, 88, 431, 462, 580, 587, 631, 687, 727, 873, 967, 969, 1322, 1430, 1451]
#使用Elasnet模型
outliers_ent = find_outliers(ElasticNetCV(), X_train, y, sigma=3.5)
R2 = 0.8237243364637833
MSE = 0.028144306885302683
RMSE = 0.16776265044789523
------------------------------------------
mean of residuals -1.6593950721969417e-15
std of residuals 0.1678202118324841
------------------------------------------
find 10 outliers:
[30, 185, 410, 462, 495, 631, 687, 915, 967, 1243]
#使用XGB模型
outliers_xgb = find_outliers(XGBRegressor(), X_train, y, sigma=4)
R2 = 0.9993821316841015
MSE = 9.864932656333151e-05
RMSE = 0.00993223673516351
------------------------------------------
mean of residuals 6.241242620683598e-06
std of residuals 0.009935642643516977
------------------------------------------
find 3 outliers:
[883, 1055, 1279]
后面还用了LGB模型、GBDT模型和SVR模型来确定outliers,这里省略绘图了。
然后比较每个模型下的异常值序号,进行人工投票选择,超过半数即为异常值,这样最终确定了outliers,并在特征集和标签集中删除。
outliers = [30, 462, 631, 967]
X_train = X_train.drop(X_train.index[outliers])
y = y.drop(y.index[outliers])
当使用one-hot编码后,一些列可能会带来过拟合的风险。判断某一列是否将产生过拟合的条件是:
特征矩阵某一列中的某个值出现的次数除以特征矩阵的列数超过99.95%,即其几乎在被投影的各个维度上都有着同样的取值,并不具有“主成分”的性质,则记为过拟合的列。
#记录产生过拟合的数据列的序号
overfit = []
for i in X_train.columns:
counts = X_train[i].value_counts(ascending=False)
zeros = counts.iloc[0]
if zeros / len(X_train) * 100 > 99.95:
overfit.append(i)
overfit
['Area', 'MSSubClass_150']
#对训练集和测试集同时删除这些列
X_train = X_train.drop(overfit, axis=1).copy()
X_sub = X_sub.drop(overfit, axis=1).copy()
print('经过异常值和过拟合删除后训练集的特征维度为:', X_train.shape)
print('经过异常值和过拟合删除后测试集的特征维度为:', X_sub.shape)
经过异常值和过拟合删除后训练集的特征维度为: (1454, 368)
经过异常值和过拟合删除后测试集的特征维度为: (1459, 368)
至此,数据预处理和特征工程部分全部完成!(喘一口粗气)
那么本期的Kaggle入门案例解析就到此啦,实在没办法一下全部写完,分成两期写吧。数据处理和特征工程已经可以结束了,下一期的话给大家带来后面的模型搭建、调优和融合部分的代码解析和讲解。感谢努力学习知识,并且沉稳帅气/美丽动人的你~,咱们后续再见!