赛题原址:House Prices: Advanced Regression Techniques
赛题描述:
Ask a home buyer to describe their dream house, and they probably won’t begin with the height of the basement ceiling or the proximity to an east-west railroad. But this playground competition’s dataset proves that much more influences price negotiations than the number of bedrooms or a white-picket fence.
With 79 explanatory variables describing (almost) every aspect of residential homes in Ames, Iowa, this competition challenges you to predict the final price of each home.
先导入文件,做出各变量间混淆矩阵查看变量间相关程度:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
df_train = pd.read_csv('C:\\Users\\rinnko\\Desktop\\learning\\MLlearning\\caseH\\train.csv')
print(df_train.columns)
#查看特征之间关联程度:相关系数矩阵可视化
corrmat = df_train.corr()
f, ax = plt.subplots(figsize=(12, 9))
sns.heatmap(corrmat, vmax=.8, square=True,cmap='magma');
(数值形式变量)相关系数矩阵如下:
初步观察,可以发现在对角线上有两个醒目的大方块。由此发现特征“TotalBsmtSF“与”1stFlrSF”,特征“GarageCars”与“GarageArea”之间相关系数接近于1,即变量间有十分强的关联性,这便意味着这两组相量均会引起多重共线性(Multicollinearity)。
多重共线性是指线性回归模型中的解释变量之间由于存在精确相关关系或高度相关关系而使模型估计失真或难以估计准确。观察变量名可初步推测变量间关系十分紧密基本可以确信其存在引起多重共线性的能力。继续寻找颜色浅甚至接近白色的方块,可以发现变量“YearBuilt”与“GarageYrBuilt”,“TotRmsAbvGrd”与“GrLivArea”相关系数很大,疑似存在引起多重共线性的可能性。
依题可知,此时预测的结果,即因变量为“SalePrice”变量。观察相关系数矩阵可发现,其中“SalePrice”与“OverallQual”、“GrLivArea”以及其他诸多变量相关系数都偏大,根据颜色可认为大多corr大于0.5,接下来用“SalePrice”变量中相关系数较大的变量做出一个相关系数矩阵,观察这些变量之间的相关程度。
#查看Saleprice相关程度较强的几个变量的混淆矩阵
#取10个corr最大的变量
cols = corrmat.nlargest(10,'SalePrice')['SalePrice'].index
corrSP = np.corrcoef(df_train[cols].values.T) #np.xorrcoef计算相关系数的方法,默认以行计算
hm = sns.heatmap(corrSP,cmap='magma',annot=True,square=True,fmt='.2f',annot_kws={'size':10},yticklabels=cols.values,xticklabels=cols.values)
plt.show()
观察最左列以查看与因变量关系最紧密的变量排名,变量“OverallQual”,“GrLivArea”的关系自然不用说。“TotalBsmtSF“与”1stFlrSF”,特征“GarageCars”与“GarageArea”之间可以2选1,这里取与因变量关系紧密的变量,即“TotalBsmtSF“、“GarageCars”。相关系数在0.5左右的这几个变量是否重要仍待后续考证。“重要”的变量在处理时要多加留意。
接下来可以以散点图的形式观察部分重要自变量(数值形式)与因变量“SalePrice”之间的关系形式,顺便获取更多有用的信息!
sns.set()
cols = ['SalePrice', 'OverallQual', 'GrLivArea', 'GarageCars', 'TotalBsmtSF', 'FullBath', 'YearBuilt']
sns.pairplot(df_train[cols], size = 2.5)#sns多变量图
plt.show();
观察(3,5)或者(5,3)可知:变量“TotRmsAbvGrd”与“GrLivArea”的点只占据了半个平面,部分散点构成了一条直线作为分界线,其余散点则聚集在直线的单侧。查看官方给的data_description可知:
TotalBsmtSF: Total square feet of basement area
GrLivArea: Above grade (ground) living area square feet
该数据集内房屋的地上居住面积通常是大于地下居住面积的,这也符合生活常识,House的地下面积可以等于地上面积,但不会超过地上面积,毕竟没有人愿意住地堡 。
观察(1,7)或者(7,1)可以察觉到,变量“YearBuilt”与因变量“SalePrice”之间的关系近似于指数型,散点图显示出类似于售价随年份变化“上界”的存在。
“SalePrice”和“GrLivArea”变量之间疑似线性关系。
from scipy.stats import norm
from scipy import stats
sns.distplot(df_train['SalePrice'] , fit=norm);
#查看正弦分布拟合的参数
(mu, sigma) = norm.fit(df_train['SalePrice'])
print( '\n mu = {:.2f} and sigma = {:.2f}\n'.format(mu, sigma))
#绘制分布图:displot()集合了matplotlib的hist()与核函数估计kdeplot的功能
plt.legend(['Normal dist. ($\mu=$ {:.2f} and $\sigma=$ {:.2f} )'.format(mu, sigma)],loc='best')
plt.ylabel('Frequency')
plt.title('SalePrice distribution')
#也可以用QQ-plot来表示
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)
plt.show()
输出:
mu = 180932.92 and sigma = 79467.79
由此可见SalePrice稍稍偏离正态分布,是正偏态分布的,而线性模型适用于正太分布的数据集,我们可以想办法对该变量所有数据进行一个统一的处理,令因变量SalePrice在处理后近似服从正态分布。
数据预处理时首先可以对偏度比较大的数据用log1p函数进行转化,使其更加服从高斯分布,此步处理可能会使我们后续的分类结果得到一个好的结果。log1p = log(x+1)。这里我们采用np.log1p()方法对SalePrice进行转化。
SalePriceA = df_train["SalePrice"]#备份用
df_train["SalePrice"] = np.log1p(df_train["SalePrice"])
输出:
mu = 12.02 and sigma = 0.40
在对数据集有了一定的了解后,现在开始准备训练和测试用的dataframe,为此要进行缺失值处理、离群值处理、新特征的生产(如果可以提取出来的话)、偏态数据的处理、dummies和categorize或是scaling处理并进行一定的变量(特征)筛选,即特征工程(Feature Engineering)。
#重开个文件
csv_data1 = pd.read_csv("C:\\Users\\rinnko\\Desktop\\learning\\MLlearning\\caseH\\train.csv")
csv_data2 = pd.read_csv("C:\\Users\\rinnko\\Desktop\\learning\\MLlearning\\caseH\\test.csv")
df_train=pd.DataFrame(csv_data1)#转换成dataframe格式
df_test=pd.DataFrame(csv_data2)
df_combined = df_train.append(df_test)
train_ID = df_train['Id']
test_ID = df_test['Id']
df_train = df_train.drop("Id", axis = 1)
df_test = df_test.drop("Id", axis = 1)
#缺失值查看
#缺失值查看
count = df_combined.isnull().sum().sort_values(ascending=False)#倒序排一下
percent = (df_combined.isnull().sum()/df_combined.isnull().count()).sort_values(ascending=False)
miss_df = pd.concat([count,percent],axis=1,keys=['Total','Percent'])
print(miss_df.head(40)) #先看看缺得最多的20个特征叭
结果如下:
Total Percent
PoolQC 2909 0.996574
MiscFeature 2814 0.964029
Alley 2721 0.932169
Fence 2348 0.804385
SalePrice 1459 0.499829
FireplaceQu 1420 0.486468
LotFrontage 486 0.166495
GarageFinish 159 0.054471
GarageCond 159 0.054471
GarageQual 159 0.054471
GarageYrBlt 159 0.054471
GarageType 157 0.053786
BsmtCond 82 0.028092
BsmtExposure 82 0.028092
BsmtQual 81 0.027749
BsmtFinType2 80 0.027407
BsmtFinType1 79 0.027064
MasVnrType 24 0.008222
MasVnrArea 23 0.007879
MSZoning 4 0.001370
BsmtFullBath 2 0.000685
BsmtHalfBath 2 0.000685
Utilities 2 0.000685
Functional 2 0.000685
Electrical 1 0.000343
Exterior2nd 1 0.000343
KitchenQual 1 0.000343
Exterior1st 1 0.000343
GarageCars 1 0.000343
TotalBsmtSF 1 0.000343
GarageArea 1 0.000343
BsmtUnfSF 1 0.000343
BsmtFinSF2 1 0.000343
BsmtFinSF1 1 0.000343
SaleType 1 0.000343
Condition2 0 0.000000
FullBath 0 0.000000
2ndFlrSF 0 0.000000
3SsnPorch 0 0.000000
BedroomAbvGr 0 0.000000
发现Condition2开始缺失值个数和比例为0,这也就是说总共有35个特征存在缺失值。
df_combined["PoolQC"] = df_combined["PoolQC"].fillna("None")
df_combined = df_combined.drop(["MiscFeature"],axis=1)
df_combined["Alley"] = df_combined["Alley"].fillna("None")
df_combined["Fence"] = df_combined["Fence"].fillna("None")
df_combined["FireplaceQu"] = df_combined["FireplaceQu"].fillna("None")
df_combined["LotFrontage"] = df_combined.groupby("Neighborhood")["LotFrontage"].transform(lambda x: x.fillna(x.median()))
for col in ('GarageType', 'GarageFinish', 'GarageQual', 'GarageCond'):
df_combined[col] = df_combined[col].fillna('None')
for col in ('GarageYrBlt', 'GarageArea', 'GarageCars'):
df_combined[col] = df_combined[col].fillna(0)
for col in ('BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2'):
df_combined[col] = df_combined[col].fillna('None')
for col in ('BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF','TotalBsmtSF', 'BsmtFullBath', 'BsmtHalfBath'):
df_combined[col] = df_combined[col].fillna(0)
df_combined["MasVnrType"] = df_combined["MasVnrType"].fillna("None")
df_combined["MasVnrArea"] = df_combined["MasVnrArea"].fillna(0)
df_combined['MSZoning'] = df_combined['MSZoning'].fillna(df_combined['MSZoning'].mode()[0])
Utilities
AllPub 2915
NoSeWa 1
其中就一个样本为NoSeWa类,这特征毫无作用,删了罢。
df_combined = df_combined.drop(['Utilities'], axis=1)
df_combined["Functional"] = df_combined["Functional"].fillna("Typ")
df_combined["Electrical"] = df_combined["Electrical"].fillna("None")
df_combined['KitchenQual'] = df_combined['KitchenQual'].fillna(df_combined['KitchenQual'].mode()[0])
df_combined['SaleType'] = df_combined['SaleType'].fillna(df_combined['SaleType'].mode()[0])
df_combined['Exterior1st'] = df_combined['Exterior1st'].fillna(df_combined['Exterior1st'].mode()[0])
df_combined['Exterior2nd'] = df_combined['Exterior2nd'].fillna(df_combined['Exterior2nd'].mode()[0])
df_combined["MSSubClass"] = df_combined["MSSubClass"].fillna("None")
现在再来看看还有没有缺失值。
print(df_combined.isnull().sum().sort_values(ascending=False).head(5))
SalePrice 1459
YrSold 0
Foundation 0
ExterCond 0
ExterQual 0
dtype: int64
这就对了,测试集中还剩1459个SalePrice待预测,而其他变量的缺失值均得到了处理。到此为止缺失值处理结束。
对离群值的判断需要设定阈值threshold,在清洗前可以考虑将数据归一化处理,使数据分布与0和1之间,更有利于观察数据的分布。
#scaling
from sklearn.preprocessing import StandardScaler
saleprice_scaled = StandardScaler().fit_transform(df_train['SalePrice'][:,np.newaxis]); #输出仍为列
print(saleprice_scaled)
low_range = saleprice_scaled[saleprice_scaled[:,0].argsort()][:10]
high_range= saleprice_scaled[saleprice_scaled[:,0].argsort()][-10:]
#argsort()函数是将x中的元素从小到大排列,提取其对应的index(索引),然后输出到y
print('outer range (low) of the distribution:')
print(low_range)
print('\nouter range (high) of the distribution:')
print(high_range)
输出:
outer range (low) of the distribution:
[[-1.83870376]
[-1.83352844]
[-1.80092766]
[-1.78329881]
[-1.77448439]
[-1.62337999]
[-1.61708398]
[-1.58560389]
[-1.58560389]
[-1.5731 ]]
outer range (high) of the distribution:
[[3.82897043]
[4.04098249]
[4.49634819]
[4.71041276]
[4.73032076]
[5.06214602]
[5.42383959]
[5.59185509]
[7.10289909]
[7.22881942]]
可见,SalePrice数据最小也距离0并不远,而最大的数据就像这两个归一化后为7.几的数据完全可以称为离群值,这里暂且放过其余数据。但作为因变量,其离群值,即这两个7.几的数据真的应该剔除掉吗?
2.来看看“GrLivArea”变量?
图右下角的两个数据明显脱离变化趋势,可以作为离群值剔除,但从这张图看,极高的两个SalePrice数据大致符合GrLivArea对SalePrice的曲线变化趋势,这两个7.几的数据可以考虑保留。
fig, ax = plt.subplots()
ax.scatter(x = df_train['GrLivArea'], y = df_train['SalePrice'])
plt.ylabel('SalePrice', fontsize=13)
plt.xlabel('GrLivArea', fontsize=13)
plt.show()
df_train = df_train.drop(df_train[(df_train['GrLivArea']>4000) & (df_train['SalePrice']<300000)].index)#删除离群值点
通过可视化人工判定离群值并丢掉离群值对应数据,可以人工判断出过于不符合变化规律的离群值点加以剔除。但这并不意味着我们需要找出所有的“离群值”,删除一部分outliers固然可以提升模型的鲁棒性,但是删除过多的“离群值”则有可能使系统过于敏感,在测试数据集含有“离群值”时模型将无法很好地做出判断。
房屋面积对于房屋定价是相当重要的,但是题目给出特征中却没有总面积这一项。这里我们将“地下室面积”、“1层面积”和“2层面积”加起来得到一个新特征“TotalSF”即房屋总面积。
#提取新特征
df_combined['TotalSF'] = df_combined['TotalBsmtSF'] + df_combined['1stFlrSF'] + df_combined['2ndFlrSF']
前文已经探索过因变量“SalePrice”了,对其进行log1p处理。接下来看一看特征(变量)之中是否也存在这样偏态分布的,是否也有可能进行处理。
df_train["SalePrice"] = np.log1p(df_train["SalePrice"])#上文的处理
numeric_feats = df_combined.dtypes[df_combined.dtypes != "object"].index#找出类型为numeric的特征
from scipy import stats
from scipy.stats import norm, skew
#查看skewness
skewed_feats = df_combined[numeric_feats].apply(lambda x: skew(x.dropna())).sort_values(ascending=False)#降序排列
skewness = pd.DataFrame({'Skew' :skewed_feats})#字典转df
print(skewness.head(10))
输出:
Skew
MiscVal 21.943434
PoolArea 16.895403
LotArea 12.820198
LowQualFinSF 12.086650
3SsnPorch 11.374072
KitchenAbvGr 4.301402
BsmtFinSF2 4.145323
EnclosedPorch 4.003118
ScreenPorch 3.945898
BsmtHalfBath 3.930795
这里我们用Box-Cox变换对这些变量进行处理。Box-Cox变换是Box和Cox在1964年提出的一种广义幂变换方法,是统计建模中常用的一种数据变换,用于连续的响应变量不满足正态分布的情况。主要特点是引入一个参数,通过数据本身估计该参数进而确定应采取的数据变换形式,Box-Cox变换可以明显地改善数据的正态性、对称性和方差相等性,一定程度上减小不可观测的误差和预测变量的相关性。
详情参考Box-Cox Transformations这篇。
利用boxcox1p()方法, 计算的是Box-Cox transformation of 1 + x 1+x 1+x .
现把偏度大于0.75的特征均进行Box-Cox变换。
skewness = skewness[abs(skewness.Skew) > 0.75]
print("There are {} skewed numerical features to Box Cox transform".format(skewness.shape[0]))#显示要处理的特征个数
from scipy.special import boxcox1p
skewed_features = skewness.index
lam = 0.15#设定lamdba为0.15
#lambda根据正态分布反CDF函数phi与变换结果的相关系数来选取,好的lambda应该使其相关系数最大,即变换后分布越接近于正态分布。
for feat in skewed_features:
df_combined[feat] = boxcox1p(df_combined[feat], lam)
There are 25 skewed numerical features to Box Cox transform
*对不同的 λ \lambda λ所作的变换不同。在 λ = 0 \lambda=0 λ=0 时该变换为对数变换,和我们对因变量做的变换log1p是一样的。
可以考虑对每个偏分布的变量都寻找其最优变换的 λ \lambda λ值,本文统一选取 λ = 0.15 \lambda=0.15 λ=0.15,尽管部分变量变换后相关系数不尽人意,但大多数变量的分布得到改善。
用数字表示类别的特征:先Label化再dummies;
#数据的转化
#str转换三连
#MSSubClass=The building class
df_combined['MSSubClass'] = df_combined['MSSubClass'].apply(str)
#Changing OverallCond into a categorical variable
df_combined['OverallCond'] = df_combined['OverallCond'].astype(str)
#Year and month sold are transformed into categorical features.
df_combined['YrSold'] = df_combined['YrSold'].astype(str)
df_combined['MoSold'] = df_combined['MoSold'].astype(str)
利用LabelEncoder() 将转换成数值型变量表示类别,也就是对不连续的数字或者文本进行编号。
#LabelEncoder:字符表示类别变成用数字(即第一次出现的索引号)表示类别
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')
# process columns, apply LabelEncoder to categorical features
for cc in cols:
lbl = LabelEncoder()
lbl.fit(list(df_combined[cc].values)) #一个个来
df_combined[cc] = lbl.transform(list(df_combined[cc].values))
print('Shape df_combined: {}'.format(df_combined.shape))
Shape df_combined: (2918, 80)
df_combined = pd.get_dummies(df_combined)#变成独热编码
print('Shape df_combined after dummies: {}'.format(df_combined.shape))
Shape df_combined after dummies: (2918, 218)
到此为止特征工程部分结束,现将训练集和测试集再次分开。
ntrain = df_train.shape[0]
ntest = df_test.shape[0]
df1_train = df_combined[:ntrain]
df1_test =df_combined[ntrain:]
df1_train.drop("Id", axis = 1, inplace = True)
df1_test.drop("Id", axis = 1, inplace = True)
先用XGBoost简单试一下看看结果如何。
from sklearn.preprocessing import Imputer
from xgboost import XGBRegressor
ydata_train = SalePriceA #这里先试用未用log1p处理的因变量数据
xdata_train = df1_train.drop("SalePrice",axis=1)
df1_test.drop("SalePrice",axis=1,inplace=True)
#数据集备份
xtrain = xdata_train
xtest = df1_test
imp = Imputer()
trainX = imp.fit_transform(xtrain)
testX = imp.transform(xtest)
xgbr = XGBRegressor()
xgbr.fit(trainX, ydata_train)
testY = xgbr.predict(testX)
#extestY = np.expm1(testY)
submission = pd.DataFrame({'Id':test_ID,'SalePrice':testY})
submission.to_csv('C:\\Users\\rinnko\\Desktop\\learning\\MLlearning\\caseH\\Submission.csv',index=False,sep=',')
结果如下,还有很大的优化空间。
接下来会考虑使用Model Ensemble(模型融合)中的Stacking方法制作新的模型进行回归再次进行预测,详见Part2。
[1]Comprehensive data exploration with Python_PedroMarcelino
[2]Stacked Regressions : Top 4% on LeaderBoard_Serigne