House Prices Advanced Regression Techniques -- Data Analysis

Posted by Sometimes Naive on October 21, 2018

House Prices Advanced Regression Techniques 是一个Kaggle比赛,要求我们利用数据预测房价以期获得一个较好的预测值。

提出问题

  1. 数据中是否有缺失值、异常值?
  2. 数据中是否有相互关联的特征可以剔除?
  3. 哪些特征与房价之间具有较好的相关性?
  4. 是否有更好的预测方法?

数据加工

第一步是数据采集,由于平台已经为我们准备好了数据,这一步便可以免去。

第二步是数据清洗,在这一步我们需要进行检查数据一致性,处理无效值和缺失值等操作。实际上,数据清洗和数据探索是相互关联,我们在清洗的过程中,也在对数据进行探索。同样的,在数据探索过程中,我们也可能会发现数据清洗的不彻底。

首先,我们导入相关的包和所需的数据集。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
Id = test.Id

其次,我们来了解一下数据的基本情况。

print(train.shape)
train.info()
print(test.shape)
test.info()

由于结果太长,这里就不展示出来了。

通过结果,我们知道:

训练集有1460条数据,81个字段。测试集有1459条数据,80个字段。训练集比测试集多一个SalePrice字段。

然后,我们来了解一下数据的缺失情况。

nans = pd.concat([train.isnull().sum(), train.isnull().sum() / train.shape[0], test.isnull().sum(), test.isnull().sum() / test.shape[0]], axis=1, keys=['Train', 'Percentage', 'Test', 'Percentage'])
print(nans[nans.sum(axis=1) > 0])
              Train  Percentage    Test  Percentage
Alley          1369    0.937671  1352.0    0.926662
BsmtCond         37    0.025342    45.0    0.030843
BsmtExposure     38    0.026027    44.0    0.030158
BsmtFinSF1        0    0.000000     1.0    0.000685
BsmtFinSF2        0    0.000000     1.0    0.000685
BsmtFinType1     37    0.025342    42.0    0.028787
BsmtFinType2     38    0.026027    42.0    0.028787
BsmtFullBath      0    0.000000     2.0    0.001371
BsmtHalfBath      0    0.000000     2.0    0.001371
BsmtQual         37    0.025342    44.0    0.030158
BsmtUnfSF         0    0.000000     1.0    0.000685
Electrical        1    0.000685     0.0    0.000000
Exterior1st       0    0.000000     1.0    0.000685
Exterior2nd       0    0.000000     1.0    0.000685
Fence          1179    0.807534  1169.0    0.801234
FireplaceQu     690    0.472603   730.0    0.500343
Functional        0    0.000000     2.0    0.001371
GarageArea        0    0.000000     1.0    0.000685
GarageCars        0    0.000000     1.0    0.000685
GarageCond       81    0.055479    78.0    0.053461
GarageFinish     81    0.055479    78.0    0.053461
GarageQual       81    0.055479    78.0    0.053461
GarageType       81    0.055479    76.0    0.052090
GarageYrBlt      81    0.055479    78.0    0.053461
KitchenQual       0    0.000000     1.0    0.000685
LotFrontage     259    0.177397   227.0    0.155586
MSZoning          0    0.000000     4.0    0.002742
MasVnrArea        8    0.005479    15.0    0.010281
MasVnrType        8    0.005479    16.0    0.010966
MiscFeature    1406    0.963014  1408.0    0.965045
PoolQC         1453    0.995205  1456.0    0.997944
SaleType          0    0.000000     1.0    0.000685
TotalBsmtSF       0    0.000000     1.0    0.000685
Utilities         0    0.000000     2.0    0.001371

可以看到,数据的缺失情况还是很普遍的,部分特征的缺失情况特别严重。尤其是Alley,Fence,FireplaceQu,MiscFeature,PoolQC这几个特征。对于这样的特征,我们常常把它们删除。

cols = ['Alley','Fence','FireplaceQu','MiscFeature','PoolQC']
train = train.drop(cols,1, inplace=True)
test = test.drop(cols,1, inplace=True)

除了缺失比较严重的特征可以处理外,我们还可以处理一些相互关联的特征。

corrmat = train.corr()
f,ax = plt.subplots(figsize=(12,9))
sns.heatmap(corrmat, vmax=.8,square=True)
sns.plt.show()

通过热力图,我们会发现,GarageYrBlt和YearBuilt,1stFlrSF和TotalBsmtSF,GrLivArea和TotRmsAbvGrd,GarageArea和GarageCars具有较强的相关性。我们可以二选一删除一个。

由于GarageYrBlt,TotalBsmtSF,TotRmsAbvGrd存在缺失值,我们选择删除它(当然,不止这个原因),留下YearBuilt,1stFlrSF,GrLivArea。

cols = ['GarageYrBlt','TotalBsmtSF','TotRmsAbvGrd']
train = train.drop(cols,1, inplace=True)
test = test.drop(cols,1, inplace=True)

GarageArea和GarageCars都有缺失值,该如何是好?先别急,我们可以看一下它们和SalePrice的相关性。

KitchenAbvGr    -0.135907
EnclosedPorch   -0.128578
MSSubClass      -0.084284
OverallCond     -0.077856
YrSold          -0.028923
LowQualFinSF    -0.025606
Id              -0.021917
MiscVal         -0.021190
BsmtHalfBath    -0.016844
BsmtFinSF2      -0.011378
3SsnPorch        0.044584
MoSold           0.046432
PoolArea         0.092404
ScreenPorch      0.111447
BedroomAbvGr     0.168213
BsmtUnfSF        0.214479
BsmtFullBath     0.227122
LotArea          0.263843
HalfBath         0.284108
OpenPorchSF      0.315856
2ndFlrSF         0.319334
WoodDeckSF       0.324413
LotFrontage      0.351799
BsmtFinSF1       0.386420
Fireplaces       0.466929
MasVnrArea       0.477493
GarageYrBlt      0.486362
YearRemodAdd     0.507101
YearBuilt        0.522897
TotRmsAbvGrd     0.533723
FullBath         0.560664
1stFlrSF         0.605852
TotalBsmtSF      0.613581
GarageArea       0.623431
GarageCars       0.640409
GrLivArea        0.708624
OverallQual      0.790982
SalePrice        1.000000
Name: SalePrice, dtype: float64

通过数据,我们发现,GarageCars比GarageArea略胜一筹。而且,我们还发现了许多与SalePrice负相关和低相关(<0.1)的字段。因而,我们决定把它们一并删除。

cols =['GarageArea','KitchenAbvGr','EnclosedPorch','MSSubClass','OverallCond','YrSold','LowQualFinSF','Id','MiscVal','BsmtHalfBath','BsmtFinSF2','3SsnPorch','MoSold','PoolArea']
train = train.drop(cols,1)
test = test.drop(cols,1)

前面我们处理的都是数值型的数据,现在我们来处理一下标称型数据。

处理标称型数据有一个有意思的地方就在于特征的缺失值本身是有意义的,也就是说它代表着某种类别或状态。

对于缺失值(“NA”)本身有意义的这种情况,我们常常使用None进行替换。比如,在FireplaceQu字段中,NA代表着No Fireplace,是没有壁炉的意思。因而,我们使用None进行替换。类似的字段有很多,不一一列举,请看代码。

cols1 = ['BsmtQual','BsmtCond','BsmtExposure','BsmtFinType1',
         'BsmtFinType2','GarageType','GarageFinish',
         'GarageQual','GarageCond']
for col in cols1:
    train[col].fillna("None", inplace=True)
    test[col].fillna("None", inplace=True)

还有一些字段的缺失值也能进行填充,他们依赖于上一步的某些字段。比如,BsmtFullBath,他的意思是地下室有全套浴室(A full bathroom is generally understood to contain a bath or shower (or both), a toilet, and a sink.)。我查看了一下数据,发现这个字段有缺失值的,都是没有地下室的,所以我们也可以使用None替换。除了类别类型,还有数值型也可以进行填充。不过,数值型,我们采用0进行填充。

cols1 = ['BsmtFullBath']
cols2 = ['BsmtFinSF1','TotalBsmtSF',
         'GarageCars','BsmtUnfSF']
for col in cols1:
    train[col].fillna("None", inplace=True)
    test[col].fillna("None", inplace=True)
for col in cols2:
    train[col].fillna(0, inplace=True)
    test[col].fillna(0, inplace=True)

还有一些个人认为与SalePrice不相关的特征,我们也一并删除。

cols =['GarageYrBlt','MasVnrArea','Functional','MasVnrType',]
train.drop(cols,1, inplace=True)
test.drop(cols,1, inplace=True)

还有缺失值的特征我们将进行常规填充。对于数值型特征,我们将填充平均值或众数;对于标称型数据,我们将使用出现次数最多的类别进行填充。

y = train.pop('SalePrice')
data = pd.concat([train,test],keys=['train', 'test'])
data['LotFrontage'] = data['LotFrontage'].fillna(data['LotFrontage'].mean())
missing = data.isnull().sum()
cols = [i for i in missing[missing>0].index]
for col in cols:
    data[col] = data[col].fillna(data[col].mode()[0])

这样,我们就把缺失值都处理完了。

最后,我们把数据调整一下,以便可以直接放入模型。

numeric_data = data[data.dtypes[data.dtypes != 'object'].index]
numeric_data_standardized = (numeric_data - numeric_data.mean())/numeric_data.std()
y = np.log(y)
for col in data.dtypes[data.dtypes == 'object'].index:
    feature = data.pop(col)
    data = pd.concat([data, pd.get_dummies(feature, prefix=col)], axis=1)
data.update(numeric_data_standardized)
train = data.loc['train']
test = data.loc['test']

数据探索

要想对数据有所了解,必然离不开描述(data_description)文件和Excel数据分析(如果数据量不大)。

如果能够做到以上两点,我们很有可能会在其中发现一下秘密。在Excel中,你可能会发现,TotalBsmtSF竟然等于BsmtFinSF1,BsmtFinSF2(已经被删除),BsmtUnfSF之和。由于TotalBsmtSF比后三者与SalePrice的相关度更高,我们决定留下TotalBsmtSF,抛弃后三者。

train.drop(['BsmtFinSF1', 'BsmtUnfSF'], axis=1, inplace=True)
test.drop(['BsmtFinSF1', 'BsmtUnfSF'], axis=1, inplace=True)

那么,是不是还有类似这种情况的特征呢?答案是肯定的。TotalBsmtSF,1stFlrSF,2ndFlrSF也是类似,但是情况稍有不同。我们考虑把三者组成一个特征以期获得更高相关度。

train['TotalSF'] = train['TotalBsmtSF'] + train['1stFlrSF'] + train['2ndFlrSF']
f, (ax1, ax2, ax3, ax4) = sns.plt.subplots(1,4,figsize=(22, 6))
data_total = pd.concat([train['SalePrice'], train['TotalSF']], axis=1)
data_total.plot.scatter(x='TotalSF', y='SalePrice', ylim=(0, 800000), ax=ax1)
data1 = pd.concat([train['SalePrice'], train['TotalBsmtSF']], axis=1)
data1.plot.scatter(x='TotalBsmtSF', y='SalePrice', ylim=(0, 800000), ax=ax2)
data2 = pd.concat([train['SalePrice'], train['1stFlrSF']], axis=1)
data2.plot.scatter(x='1stFlrSF', y='SalePrice', ylim=(0, 800000), ax=ax3)
data3 = pd.concat([train['SalePrice'], train['2ndFlrSF']], axis=1)
data3.plot.scatter(x='2ndFlrSF', y='SalePrice', ylim=(0, 800000), ax=ax4)
sns.plt.show()

从图中我们可以看到,TotalSF与SalePrice的相关性表现得更好。那么,我们可以把它留下,其他删除。

train.drop(['TotalBsmtSF', '1stFlrSF', '2ndFlrSF'], axis=1, inplace=True)
test.drop(['TotalBsmtSF', '1stFlrSF', '2ndFlrSF'], axis=1, inplace=True)

总结

from sklearn import linear_model
from sklearn.model_selection import cross_val_score
model = linear_model.LinearRegression() 
score = np.sqrt(-cross_val_score(model, train, y, scoring='neg_mean_squared_error'))
print(score)
model = model.fit(train, y)
results = model.predict(test)
final = np.exp(results)

submission = pd.DataFrame()
submission['Id'] = Id
submission['SalePrice'] = final

submission.to_csv("submission.csv", index= False)

对于这次的数据分析,我个人认为还有提升的空间,先列出以下几点,有时间再分析一下。

  1. 数据的删除过于草率,特别是数据缺失情况严重那部分,没有查看是否有填补的可能就删除,下次一定要注意。
  2. 数据的填补方式没有考虑周详。实际上,数据的填补方式有很多种,都非常值得一试。
  3. 数据探索得不够彻底。这部分就只有一张图,应该加强这部分的学习。
  4. 模型的选择。现在只会线性回归,要学会使用更多的模型。

好了,就这样吧!