Kaggle机器学习实战(2)——房价预测(上)

kaggle房价预测是一个典型的回归问题。该问题提供了数千条房屋的房价和大量的特征变量,需要挑战者训练回归模型并预测测试集上房屋的房价,追求最小的均方误差。

这篇文章翻译自其中点赞数量最多的一篇kernel,它主要讲的是探索性数据分析这一方面。

导入数据

1
2
3
4
5
6
7
8
9
10
11
# 调包
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from scipy.stats import norm
from sklearn.preprocessing import StandardScaler
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline

注意:%matplotlib inline是一个魔法函数,可以让matplotlib在Jupter Notebook中直接绘图,否则要使用plt.show()才能显示图片。

1
2
# 读取csv格式的训练集
df_train = pd.read_csv('path/train.csv')

观察变量

训练集包含1460个样本,每个样本包含详细概括房子的各个信息的变量共计79个,以及它们的房价。

首先,搞清这79个变量是数值型numerical)还是标称型categorical)变量。然后我们根据生活中的实际经验,对可能影响房价的因素分成三大类:建筑本身,空间大小和区位。接着,对每个因子的重要性进行预估。

在依靠直觉对每个变量的重要性进行评估时,可以用三个问题作为标准:

  • 假如你在买房,你会考虑这个因素吗?
  • 如果你要考虑这个因素,你会把它摆在第几位?
  • 这个因素是否和其他因素有些重合?

据此就可以筛选出主观上较重要的因子。

1
2
# 显示变量名 
print(df_train.columns)

参考data_description.txt

主观上认定以下变量与房价存在显著的相关性:

  • OverallQual(房屋的总体质量):代表房屋质量的一个重要变量。
  • YearBuilt(房龄):代表房屋质量的另一个重要变量。
  • TotalBsmtSF(地下室总面积)、GrLivArea(地上居住面积):这个很容易理解。

除了“房屋本身的质量”和“面积”这两大因素之外,很显然“区位”也是影响房价必须考虑的一项,但原数据给出的区位相关变量都是诸如周围有哪些地标建筑、靠近哪些交通枢纽这些无法直接体现区位优劣的变量,并且它们难以可视化。我们暂时不考虑它们。

数据可视化

先观察预测变量Y:房价。它是一个连续型的变量。

1
2
3
4
5
# 描述性统计
print(df_train['SalePrice'].describe())
# 可视化Y
sns.distplot(df_train['SalePrice'])
plt.show()

从图可知,训练集的房价呈现偏态分布。分析偏度和峰度:

1
2
3
# 偏度和峰度
print("Skewness: %f" % df_train['SalePrice'].skew())
print("Kurtosis: %f" % df_train['SalePrice'].kurt())

我们希望因变量能服从正态分布,因为这将大大提升线性模型的性能。之后会使之变换为正态分布。

接下来可视化“主观上认为比较重要的变量”。

与数值型变量的关系

1
2
3
4
5
6
7
8
9
# 地上居住面积/房价散点图
var = 'GrLivArea'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0, 800000))

# 地下室面积/房价散点图
var = 'TotalBsmtSF'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0, 800000))

以上两个图可以在Jupter Notebook中自行绘制。从图上可以直观的看出,这两个变量都与房价正相关。值得注意的是这两个散点图出现了很多零散的不符合规律的点(离群值)。

与标称型变量的关系

1
2
3
4
5
6
# 房屋质量/房价箱型图
var = 'OverallQual'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
f, ax = plt.subplots(figsize=(8, 6))
fig = sns.boxplot(x=var, y="SalePrice", data=data)
fig.axis(ymin=0, ymax=800000)

很显然,房价与房屋质量(OverallQual)呈正相关。

1
2
3
4
5
6
7
# 房子修建的年份/房价箱型图
var = 'YearBuilt'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
f, ax = plt.subplots(figsize=(16, 8))
fig = sns.boxplot(x=var, y="SalePrice", data=data)
fig.axis(ymin=0, ymax=800000)
plt.xticks(rotation=90)

这里,房屋的修建年份(YearBuilt)和房价似乎只有微弱的正相关性,但仍可以认为绝大多数情况下,越新的房子越能卖个高价。

对以上四个变量的可视化做个总结:

  1. “地上居住面积”(GrLivArea)和“地下室面积”(TotalBsmtSF)与房价呈显著的正相关,且“地下室面积”与房价拟合的直线斜率较大。
  2. “房屋质量”(OverallQual)和“修建年份”(YearBuilt)与房价也具有相关性,其中“房屋质量”相关性更强。

接下来,观察其他变量。客观地讲,它们是同等重要的。

与其他变量的关系

1
2
3
4
5
6
7
8
9
10
11
# 所有变量的相关性矩阵热图
corrmat = df_train.corr()
f, ax = plt.subplots(figsize=(12, 9))
sns.heatmap(corrmat, vmax=.8, square=True)

# 与Y的相关性矩阵top10热图
k = 10 # 热力图变量数目
cols = corrmat.nlargest(k, 'SalePrice')['SalePrice'].index
cm = np.corrcoef(df_train[cols].values.T)
sns.set(font_scale=1.25)
hm = sns.heatmap(cm, cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size': 10}, yticklabels=cols.values, xticklabels=cols.values)

seaborn的热图是快速了解变量之间关系的极佳方法。我们主要关心浅色和深色区块,它表示两个变量之间具有强烈的正/负相关。在线性模型中,若自变量之间具有线性关系会对模型的性能产生影响,因为它们包含重复的信息,应当舍去。

从第一张图我们看出,“地下室面积”(TotalBsmtSF)和“一楼面积”(1stFlrSF)这两个变量高度正相关,以及车库系列变量(GarageCars, GarageArea)之间具有很强的关联(这些也符合我们的生活经验),我们将会只保留其中一个与房价相关性更高的变量。

第二张图主要是看与预测变量Y最相关的10个变量之间的相关性。我们可以更加清楚的看到这些变量之间的相关系数。这top10里面出现了地上面积、地下室面积、建筑质量和修建年份这些指标,证实了我们第一步的猜想;“地上居住面积”(GrLivArea)和“地上房间总数”(TotRmsAbvGrd)非常相关,因此我们只保留其中一个。

得到了与Y有较高关联性的特征(并剔除了一些相似的特征),我们接下来用散点图来进一步查看变量之间的联系。

1
2
3
4
# 房价与一些变量的关系的散点图
sns.set()
cols = ['SalePrice', 'OverallQual', 'GrLivArea', 'GarageCars', 'TotalBsmtSF', 'FullBath', 'YearBuilt']
sns.pairplot(df_train[cols], size=2.5)

我们发现“地下室面积”(TotalBsmtSF)和“地上面积”(GrLiveArea)的散点图上,有一串点可以连成一条直线。观察房价与修建年份的散点图,这些点云的上界和下界呈指数函数状分布,而近几年修建的新房的房价保持在指数函数上方,推测近几年房价涨的更快了。

至此我们初步完成了数据的可视化。

处理缺失值

在着手处理缺失值之前,我们要考虑清楚:

  1. 数据缺失的现象有多普遍?
  2. 丢失的数据是随机的,还是有规律可循的?

缺失值的处理要谨慎,我们显然不希望训练样本大量流失。此外,数据的缺失可能导致预测结果出现偏见以及歪曲事实。

首先,统计出缺失的变量及其频率。

1
2
3
4
5
# 缺失值查看
total = df_train.isnull().sum().sort_values(ascending=False)
percent = (df_train.isnull().sum() / df_train.isnull().count()).sort_values(ascending=False)
missing_data = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])
print(missing_data.head(20))
Total Percent
PoolQC 1453 0.995205
MiscFeature 1406 0.963014
Alley 1369 0.937671
Fence 1179 0.807534
FireplaceQu 690 0.472603
LotFrontage 259 0.177397
GarageCond 81 0.055479
GarageType 81 0.055479
GarageYrBlt 81 0.055479
GarageFinish 81 0.055479
GarageQual 81 0.055479
BsmtExposure 38 0.026027
BsmtFinType2 38 0.026027
BsmtFinType1 37 0.025342
BsmtCond 37 0.025342
BsmtQual 37 0.025342
MasVnrArea 8 0.005479
MasVnrType 8 0.005479
Electrical 1 0.000685
Utilities 0 0.000000

通常情况下,我们考虑当某一个变量的缺失值超过15%,就直接删除这个变量,而不是尝试去填充它。按这个思路,“泳池质量”(PoolQC)、“杂项(电梯等)”(MiscFeature)、“屋旁小巷类型”(Alley)、“栅栏质量”(Fence)等几个变量就应当剔除。关键在于我们抛弃这些数据会错失一些有用信息吗?我认为不会,因为我们在买房时不会重视这些信息。所以可以放心删除它们。

在剩下来的变量中,车库系列变量(GarageX)缺失的数目相同。考虑到有关车库的信息中最重要的是容量(GarageCars),因此我们可以删除这些不怎么重要的变量。同理,我们对地下室系列变量(BsmtX)做同样的处理。

对于“砌砖面积”(MasVnrArea)及其材质(MasVnrType)这两个变量,我们认为它不重要,并且它们与“修建年份”(YearBuilt)和“房屋总体质量”(OverallQual)有很强的相关性。因此,抛弃这两个变量不会丢失重要信息。

最后,在“电气系统”(Electrical)这个变量中有一个确失值,我们选择删除包含这个缺失值的样本,并保留这一变量。

1
2
3
4
5
6
# 缺失值处理
df_train = df_train.drop((missing_data[missing_data['Total'] > 1]).index, 1)
df_train = df_train.drop(df_train.loc[df_train['Electrical'].isnull()].index)

# 验证一下是否处理了全部的缺失值
df_train.isnull().sum().max()

处理离群值

离群值就是指数据中偏离其他数值较大的数值。离群值可以显著地影响模型,并且提供了一些特例以供我们解读。这里就以预测变量Y——房价的标准差来分析。

单变量分析

我们要设定一个阈值,把部分观测值设定为离群值。首先,需要标准化数据。这里的标准化包括把数据的平均值化为0,同时标准差化为1。

1
2
3
4
5
6
7
8
# 数据标准化
saleprice_scaled = StandardScaler().fit_transform(df_train['SalePrice'][:, np.newaxis]);
low_range = saleprice_scaled[saleprice_scaled[:, 0].argsort()][:10]
high_range = saleprice_scaled[saleprice_scaled[:, 0].argsort()][-10:]
print('outer range (low) of the distribution:')
print(low_range)
print('\nouter range (high) of the distribution:')
print(high_range)

从结果中可以看出,最低的10个值偏离0都不太远,而最高的10个值中,距离0都较远。尤其是需要注意两个大于7的值。我们再做进一步的分析。

双变量分析

重新绘制与房价相关变量的散点图,这次的目的是观察离群值。

1
2
3
4
# 地上居住面积/房价双变量分析
var = 'GrLivArea'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0, 800000))

现在重新审视这张图可以发现:地上面积最大的两个房子,它们的价格却反常的低,我们可以推测比如这两个房子在乡下。我们要做的是把这两个样本定义为离群值并剔除,因为它们不能代表典型的情况。

上文说要留意两个偏离较大的值,也就是图中房价最高的两个点。但从这张散点图上看,这两个特殊案例符合主流趋势,因此对这两个点予以保留。

1
2
3
4
# 删除离群值
df_train.sort_values(by='GrLivArea', ascending=False)[:2]
df_train = df_train.drop(df_train[df_train['Id'] == 1299].index)
df_train = df_train.drop(df_train[df_train['Id'] == 524].index)

同样,我们重新观察一下地下室面积与房价的散点图。

1
2
3
4
# 地下室面积/房价双变量分析
var = 'TotalBsmtSF'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0, 800000))

可以观察到部分点(例如面积大于3000的3个)有离群的迹象,但可以接受,因此予以保留。

正态化数据

根据多重线性回归的假设检验,我们要确保数据具有以下特性:

  1. 正态性:残差e服从正态分布。
  2. 方差齐性:残差e的大小不随变量取值水平的改变而改变。
  3. 线性:自变量与因变量之间存在线性关系。
  4. 独立性:各个自变量之间不存在多重共线性问题。

现在要解决正态性问题。单变量的正态化并不能保证整个多元回归的正态性,但有一定效果。这同时可以解决异方差性的问题。

数据服从标准正态分布是我们的追求。对数据正态化的方法有很多,比如Box-Cox变换。我们已经知道这组数据中有些变量,例如因变量房价,服从偏态分布,可以用更简单的方法使之正态化。

首先我们绘制所要考察变量的柱状图和正态概率分布图。

1
2
3
4
# 房价的柱状图和正太概率分布图
sns.distplot(df_train['SalePrice'], fit=norm)
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)

很明显这组房价满足偏态分布;用一种简单的转换方法可以使这组数据符合正态分布——取对数。

1
2
3
4
5
6
7
# 对房价取对数
df_train['SalePrice'] = np.log(df_train['SalePrice'])

# 转换后的房价的柱状图和正太概率分布图
sns.distplot(df_train['SalePrice'], fit=norm)
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)

然后处理一个重要的自变量“地上居住面积”(GrLiveArea):

1
2
3
4
# 地上面积的柱状图和正太概率分布图
sns.distplot(df_train['GrLivArea'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['GrLivArea'], plot=plt)
1
2
3
4
5
6
7
# 对地上面积取对数
df_train['GrLivArea'] = np.log(df_train['GrLivArea'])

# 转换后的地上面积的柱状图和正太概率分布图
sns.distplot(df_train['GrLivArea'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['GrLivArea'], plot=plt)

“地下室面积”(totalBesmtSF):

1
2
3
4
# 地下室面积的柱状图和正太概率分布图
sns.distplot(df_train['TotalBsmtSF'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['TotalBsmtSF'], plot=plt)

处理这个变量相对比较棘手,因为有相当一部分的观测值为0。而0值是无法取对数的。除去这些0,总体上还是满足偏态分布的。因此,我们会新建一个二值变量来反映这个样本是否包含地下室,对于有地下室的样本进行对数转换,忽略无地下室的样本。(因为无法确定这种做法对不对,原作者称之为‘high risk engineering’。)

1
2
3
4
5
# 建立一个新的列来存储新的变量
# 如果area>0则新变量为1,如果area==0则为0
df_train['HasBsmt'] = pd.Series(len(df_train['TotalBsmtSF']), index=df_train.index)
df_train['HasBsmt'] = 0
df_train.loc[df_train['TotalBsmtSF']>0,'HasBsmt'] = 1
1
2
3
4
5
6
7
# 对符合条件的地下室面积取对数
df_train.loc[df_train['HasBsmt']==1,'TotalBsmtSF'] = np.log(df_train['TotalBsmtSF'])

# 转换后的地下室面积的柱状图和正太概率分布图
sns.distplot(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], plot=plt)

同方差性

以地上居住面积与房价关系的散点图为例,早先版本的散点图左下角密集而右上角稀疏,呈现圆锥状,这就是具有同方差性的问题。

绘制对数转换后的散点图:

1
2
# 地上居住面积/房价散点图
plt.scatter(df_train['GrLivArea'], df_train['SalePrice'])

可以看见我们已经解决了同方差性的问题,数据变得正态了。再来看看房价和地下室面积的关系:

1
2
# 地下室面积/房价散点图
plt.scatter(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], df_train[df_train['TotalBsmtSF']>0]['SalePrice'])

至此,我们可以认为这几个重要变量的正态化已经完成了。

虚拟变量

这一步将把标称型的数据one-hot化:

1
2
# 把标称性变量虚拟化
df_train = pd.get_dummies(df_train)

这篇探索性数据分析的kernel就到此为止了。接下来要做的就是训练模型,将会参考另外一篇kernel

总结

  • 对于大多数Kaggle数据科学竞赛,处理数据是耗费的时间最多的、并且是对结果精度影响最大的环节。因此这篇kernel尽管没有到训练那一步,却获得了极高的点赞数。
  • 探索性数据分析,包含数据可视化、处理缺失值、离群值以及数据标准化、正态化、虚拟化等等,对于数据挖掘和统计机器学习非常重要。作为初学者,要熟练运用numpy、matplotlib、pandas这三大利器。