[Kaggle競賽] Ames房價迴歸預測Part1:特徵工程+利用XGBoost進行房價預測

賽題原址: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()

corrSPmat
觀察最左列以查看與因變量關係最緊密的變量排名,變量“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();

paiplotSP
從多變量圖中可以看出以下幾點:

  1. 觀察(3,5)或者(5,3)可知:變量“TotRmsAbvGrd”與“GrLivArea”的點只佔據了半個平面,部分散點構成了一條直線作爲分界線,其餘散點則聚集在直線的單側。查看官方給的data_description可知:
    TotalBsmtSF: Total square feet of basement area
    GrLivArea: Above grade (ground) living area square feet
    該數據集內房屋的地上居住面積通常是大於地下居住面積的,這也符合生活常識,House的地下面積可以等於地上面積,但不會超過地上面積,畢竟沒有人願意住地堡

  2. 觀察(1,7)或者(7,1)可以察覺到,變量“YearBuilt”與因變量“SalePrice”之間的關係近似於指數型,散點圖顯示出類似於售價隨年份變化“上界”的存在。

  3. “SalePrice”和“GrLivArea”變量之間疑似線性關係。

進一步觀察SalePrice——探索數據分佈轉變的可能性

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

SPdist
在這裏插入圖片描述
由此可見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

SPdist2
SPqq2
平滑處理可以達到預期效果。

特徵工程

在對數據集有了一定的瞭解後,現在開始準備訓練和測試用的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個特徵存在缺失值。

  1. 分析缺失比例在15%以上的特徵,“PoolQC”、“MiscFeature”、 “Alley” 、“Fence”、 “FireplaceQu”和“LotFrontage”。查看官方給的data_description可知:
    PoolQC: Pool quality(大多數房子不帶泳池的)
    MiscFeature: Miscellaneous feature not covered in other categories(其他特徵?捨棄)
    Alley: Type of alley access to property(大多數房子不帶巷子的)
    Fence: Fence quality(有房子不設籬笆的)
    FireplaceQu: Fireplace quality(有房子不設Fireplace的)
    LotFrontage: Linear feet of street connected to property(距離街道的距離還是很重要的,這個特徵保留)
    可以發現捨棄的MiscFeature爲無關緊要的特徵,是常人在購房時不會考慮的方面,甚至存在過多的離羣值,刪掉也罷。而留下的特徵的處理辦法多爲在缺失值處填充“None”。對於LotFrontage可以考慮以Neighborhood類型groupby後以各組中位數填充。
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()))
  1. 接下來是Garage_系列,可知該系列變量有“GarageCond”、“GarageType” 、“GarageYrBlt”、“GarageFinish”、“GarageQual”、“GarageCars”、“GarageArea”。可以推測其中最重要的是變量“GarageCars”,與因變量關係最緊密。其餘Garage系列變量,缺失值均在5.5479%,可以假設缺失值是由於房子不帶車庫。可以將GarageType, GarageFinish, GarageQual and GarageCond缺失值用“None”填充,而GarageYrBlt, GarageArea and GarageCars這種數量型的缺失值用“0”填充。
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)
  1. 對Bsmt_系列變量的缺失值做類似處理。可知該系列變量有’BsmtQual’、 ‘BsmtCond’、
    ‘BsmtExposure’、‘BsmtFinType1’、‘BsmtFinSF1’、 ‘BsmtFinType2’、 ‘BsmtFinSF2’,、‘BsmtUnfSF’、‘TotalBsmtSF’、 ‘BsmtFullBath’、 ‘BsmtHalfBath’。
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)
  1. “MasVnrArea”和“MasVnrType”,缺失比例爲0.5479%,是代表牆面外貼磚的面積和類型,這兩個變量與變量“OverallQual” 的相關係數較大,但與因變量的相關係數也較大。缺失值只有8個,很可能是因爲沒有外貼磚,Type可以考慮補None,Area乾脆就補0。
df_combined["MasVnrType"] = df_combined["MasVnrType"].fillna("None")
df_combined["MasVnrArea"] = df_combined["MasVnrArea"].fillna(0)
  1. “MSZoning”: Identifies the general zoning classification of the sale,可知居住型RL最爲常見,可將4個缺失值以衆數“RL”填充。
df_combined['MSZoning'] = df_combined['MSZoning'].fillna(df_combined['MSZoning'].mode()[0])
  1. “Utilities”有2個缺失值,觀察一下該數據的內容:
Utilities
AllPub    2915
NoSeWa       1

其中就一個樣本爲NoSeWa類,這特徵毫無作用,刪了罷。

df_combined = df_combined.drop(['Utilities'], axis=1)
  1. “Functional”有2個缺失值,官方描述文件裏稱其爲Home functionality (Assume typical unless deductions are warranted),則缺失值均默認爲Typ。
df_combined["Functional"] = df_combined["Functional"].fillna("Typ")
  1. “Electrical”就1個缺失值,把缺失值對應的數據補“None”就好。
df_combined["Electrical"] = df_combined["Electrical"].fillna("None")
  1. “KitchenQual”、“SaleType ”、“Exterior1st”和“Exterior2nd”,各1個缺失值,衆數填充。
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])
  1. “MSSubClass”就1個缺失值,缺失值對應樣本補“None”即可。
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之間,更有利於觀察數據的分佈。

  1. SalePrice:因變量的Range如何?
#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”變量?
Gr2SP
圖右下角的兩個數據明顯脫離變化趨勢,可以作爲離羣值剔除,但從這張圖看,極高的兩個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+x1+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 時該變換爲對數變換,和我們對因變量做的變換log1p是一樣的。
可以考慮對每個偏分佈的變量都尋找其最優變換的λ\lambda值,本文統一選取λ=0.15\lambda=0.15,儘管部分變量變換後相關係數不盡人意,但大多數變量的分佈得到改善。

數據的轉換——dummies、categorize、scaling

用數字表示類別的特徵:先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

先用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=',')

結果如下,還有很大的優化空間。
EasyXGBoost
接下來會考慮使用Model Ensemble(模型融合)中的Stacking方法制作新的模型進行迴歸再次進行預測,詳見Part2。

參考文章

[1]Comprehensive data exploration with Python_PedroMarcelino
[2]Stacked Regressions : Top 4% on LeaderBoard_Serigne

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章