House Price してみた

#自己紹介
はじめまして。プログラミング初心者の大学生がKaggleのHousePriceをしてみたいと思います。

##必要なライブラリをインポート
まずは分析に必要なライブラリをインストール。

```python
import numpy as np 
import pandas as pd 
from pandas import Series, DataFrame
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
```
##データ確認
trainデータとtestデータのサイズを確認

```python
print(train.shape)
print(test.shape)
#出力結果
(1460, 81)
(1459, 80)
```
特徴量を確認

```python
train.columns
#出力結果
Index(['Id', 'MSSubClass', 'MSZoning', 'LotFrontage', 'LotArea', 'Street',
       'Alley', 'LotShape', 'LandContour', 'Utilities', 'LotConfig',
       'LandSlope', 'Neighborhood', 'Condition1', 'Condition2', 'BldgType',
       'HouseStyle', 'OverallQual', 'OverallCond', 'YearBuilt', 'YearRemodAdd',
       'RoofStyle', 'RoofMatl', 'Exterior1st', 'Exterior2nd', 'MasVnrType',
       'MasVnrArea', 'ExterQual', 'ExterCond', 'Foundation', 'BsmtQual',
       'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinSF1',
       'BsmtFinType2', 'BsmtFinSF2', 'BsmtUnfSF', 'TotalBsmtSF', 'Heating',
       'HeatingQC', 'CentralAir', 'Electrical', '1stFlrSF', '2ndFlrSF',
       'LowQualFinSF', 'GrLivArea', 'BsmtFullBath', 'BsmtHalfBath', 'FullBath',
       'HalfBath', 'BedroomAbvGr', 'KitchenAbvGr', 'KitchenQual',
       'TotRmsAbvGrd', 'Functional', 'Fireplaces', 'FireplaceQu', 'GarageType',
       'GarageYrBlt', 'GarageFinish', 'GarageCars', 'GarageArea', 'GarageQual',
       'GarageCond', 'PavedDrive', 'WoodDeckSF', 'OpenPorchSF',
       'EnclosedPorch', '3SsnPorch', 'ScreenPorch', 'PoolArea', 'PoolQC',
       'Fence', 'MiscFeature', 'MiscVal', 'MoSold', 'YrSold', 'SaleType',
       'SaleCondition', 'SalePrice'],
      dtype='object')
```
必要のない特徴量Idをひとまず削除

```python
train=train.drop(['Id'],axis=1)
test=test.drop(['Id'],axis=1)
```
目的変数の確認

```python
sns.distplot(train.SalePrice, bins = 25)
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/326720/fd5d7bda-5aba-a1d5-80f3-65a1fed99cd2.png)
低所得者が多く、グラフ全体が左に寄っており、経済的不平等が起こっている。
##欠損値処理
まずは欠損値の数を確認

```python
#trainデータ
train.isnull().sum()[train.isnull().sum()>0]
#出力結果
LotFrontage      259
Alley           1369
MasVnrType         8
MasVnrArea         8
BsmtQual          37
BsmtCond          37
BsmtExposure      38
BsmtFinType1      37
BsmtFinType2      38
Electrical         1
FireplaceQu      690
GarageType        81
GarageYrBlt       81
GarageFinish      81
GarageQual        81
GarageCond        81
PoolQC          1453
Fence           1179
MiscFeature     1406
dtype: int64

#testデータ
train.isnull().sum()[train.isnull().sum()>0]
#出力結果
MSZoning           4
LotFrontage      227
Alley           1352
Utilities          2
Exterior1st        1
Exterior2nd        1
MasVnrType        16
MasVnrArea        15
BsmtQual          44
BsmtCond          45
BsmtExposure      44
BsmtFinType1      42
BsmtFinSF1         1
BsmtFinType2      42
BsmtFinSF2         1
BsmtUnfSF          1
TotalBsmtSF        1
BsmtFullBath       2
BsmtHalfBath       2
KitchenQual        1
Functional         2
FireplaceQu      730
GarageType        76
GarageYrBlt       78
GarageFinish      78
GarageCars         1
GarageArea         1
GarageQual        78
GarageCond        78
PoolQC          1456
Fence           1169
MiscFeature     1408
SaleType           1
dtype: int64
```
データの数に対して欠損値の数が極端に多い変数がいくつかあるので、内容を確認してみる。

```python
train['FireplaceQu'].value_counts()
#出力結果
Gd    380
TA    313
Fa     33
Ex     24
Po     20
Name: FireplaceQu, dtype: int64
```
Kaggleのページでダウンロードできるdata_descriptionには
FireplaceQu: Fireplace quality
　Ex :      Excellent - Exceptional Masonry Fireplace
　Gd   :    Good - Masonry Fireplace in main level
　TA  :     Average - Prefabricated Fireplace in main living area or Masonry Fireplace in basement
　Fa    :   Fair - Prefabricated Fireplace in basement
　Po     :  Poor - Ben Franklin Stove
　NA     :  No Fireplace
とあるので、Na:No Fireplaceも欠損値扱いされてしまっている。
そのような特徴量の欠損値を'None'で補完

```python
fillnaNone_list = ['Alley','BsmtCond','BsmtQual','BsmtExposure','BsmtFinType1', 'BsmtFinType2','FireplaceQu','GarageType', 'GarageFinish','GarageQual', 'GarageCond','PoolQC','Fence','MiscFeature']
 
for i in fillnaNone_list:
    train[i].fillna('None',inplace=True)
    test[i].fillna('None',inplace=True)
```
同様に、値が数値の特徴量の欠損値を0で補完(ただし、'GarageYrBlt'だけは0としてしまっては後々築年数に変換した時に面倒なので0で補完しない)

```python
zero_list=['LotFrontage','MasVnrArea','BsmtFinSF1','BsmtFinSF2','BsmtUnfSF','TotalBsmtSF','BsmtFullBath','BsmtHalfBath','GarageCars','GarageArea']
for i in zero_list:
    train[i].fillna(0,inplace=True)
    test[i].fillna(0,inplace=True)
```
YearBuiltでGarageYrBltを補完するのが一番思いつきやすいため、補完して良いか確認するため関係を可視化

```python
plt.plot(test.YearBuilt, test.GarageYrBlt,'.', alpha=0.5)
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/326720/0076799c-7dfe-7250-49ea-3362c2742ac9.png)
補完して良さそうなのでYearBuiltで補完

```python
train['GarageYrBlt'].fillna(train.YearBuilt,inplace=True)
test['GarageYrBlt'].fillna(test.YearBuilt,inplace=True)
```
ここで一旦残っている欠損値を確認

```python
#trainデータ
train.isnull().sum()[train.isnull().sum()>0]
#出力結果
MasVnrType    8
Electrical    1
dtype: int64

#testデータ
test.isnull().sum()[test.isnull().sum()>0]
#出力結果
MSZoning        4
Utilities       2
Exterior1st     1
Exterior2nd     1
MasVnrType     16
KitchenQual     1
Functional      2
SaleType        1
dtype: int64
```
平均値で処理すると外れ値に影響を受けやすく、カテゴリー変数に対応できないので最頻値で補完

```python
n_list=['Electrical', 'MasVnrType', 'MSZoning', 'Functional', 'Utilities','Exterior1st', 'Exterior2nd', 'KitchenQual', 'SaleType']
for i in n_list:
    train[i].fillna(train[i].mode()[0],inplace=True)
    test[i].fillna(test[i].mode()[0],inplace=True)
```
これで欠損値が無くなった。
##特徴量
普段家を購入するとき、築年数を気にするので、西暦で書かれている特徴量を経過した年数に変換する。

```python
#築年数、改修からの年数、売り出されてからの年数に変更する。
train['elapsed']=2018-train['YearBuilt']
train['remod_elapsed']=2018-train['YearRemodAdd']
train['sold_elapsed']=2018-train['YrSold']
train['garage_elapsed']=2018-train['GarageYrBlt']
test['elapsed']=2018-test['YearBuilt']
test['remod_elapsed']=2018-test['YearRemodAdd']
test['sold_elapsed']=2018-test['YrSold']
test['garage_elapsed']=2018-test['GarageYrBlt']
#必要なくなった列をカラムを指定して削除
drop_columns=['YearBuilt','YearRemodAdd','YrSold','GarageYrBlt']
train=train.drop(drop_columns,axis=1)
test=test.drop(drop_columns,axis=1)
```
家を購入するとき最も気にするのは何と言っても広さなので、広さを表す特徴量を作る

```python
#部屋サイズ
train['TotalSF']= train['TotalBsmtSF']+ train['1stFlrSF']+train['2ndFlrSF']
test['TotalSF']= test['TotalBsmtSF']+ test['1stFlrSF']+ test['2ndFlrSF']
#ベランダサイズ
train['Porch']= train['EnclosedPorch']+ train['3SsnPorch']+ train['ScreenPorch']
+ train['WoodDeckSF']+ train['OpenPorchSF']
test['Porch']= test['EnclosedPorch']+ test['3SsnPorch']+ test['ScreenPorch']
+ test['WoodDeckSF']+ test['OpenPorchSF']

drop_columnsSF=['TotalBsmtSF','1stFlrSF','2ndFlrSF','EnclosedPorch','3SsnPorch','ScreenPorch','WoodDeckSF','OpenPorchSF']
train = train(drop_columnsSF,axis=1)
test= test(drop_columnsSF,axis=1)
```
特徴量が正規分布に従っていた方が精度が良くなるので歪度の高いものは偏りをなくす

```python
#数値の偏りをなくす為に、数値データを取り出す
numerical_features = train.select_dtypes(exclude = ["object"]).columns
train_num = train[numerical_features]
#歪度を計算
from scipy.stats import skew 
skewness = train_num.apply(lambda x: skew(x))
skew_features = skewness[abs(skewness)>0.5]
#基本的に左に寄っていることを確認
for i in skew_features.index:
    sns.distplot(train[i],bins = 25)
    plt.xlabel(i)
    plt.show()
```
e.g.)elapsed
![image.png](https://qiita-image-store.s3.amazonaws.com/0/326720/bd3bf5da-4b87-e83e-6f45-c27fe26c65df.png)
可視化して確かめると、全ての特徴量が上のelapsedのように左に寄っていることがわかった。従って、対数変換をしたいが、値が0未満だとlogを取れないので0未満の値がないかを確認。

```python
print('train')
for i in skew_features.index:
    print(train[train[i]<0].index)
print('test')
for i in skew_features.index.drop('SalePrice'):
    print(test[test[i]<0].index)
#出力結果
train
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
test
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([1132], dtype='int64')
Int64Index([], dtype='int64')
Int64Index([], dtype='int64')
```
garage_elapsedのIndex:1132が0未満だということが判明

```python
test.loc[1132,'garage_elapsed']
#出力結果
-189.0
```
経過年数が負の値は明らかにおかしいので、築年数で補完する

```python
test.loc[1132,'garage_elapsed']=test.loc[1132,'elapsed']
```
これで0未満の値は消えたので、対数log(y+1)をとる。

```python
for x in skew_features.index:
    train[x]=np.log1p(train[x])
for y in skew_features.index.drop('SalePrice'):
    test[y]=np.log1p(test[y])
```
次に、カテゴリー変数を数量データに変換する。
One-Hot-encodingでダミー変数化すると、特徴量が多くなりすぎてしまう。そこで、どんな特徴量にも少なからず順序関係があるのでは、と考えた。

例えばNeighborhood
この地域にことを全く知らない私たちには順序が分からないが、グラフで表示してみると

```python
Neighborhood_meanSP = train.groupby('Neighborhood')['SalePrice'].mean()
Neighborhood_meanSP = Neighborhood_meanSP.sort_values()
sns.pointplot(x = train.Neighborhood.values, y = train.SalePrice.values, order =Neighborhood_meanSP.index) 
plt.xticks(rotation=45)
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/326720/d0176617-988d-7604-a742-eae977aa2bb7.png)

と、上図のように明らかに順序関係があることがわかる。従って、 各変数の順序関係に従って、エンコーディングすれば良いと考えた

```python
for i in train.loc[:, train.dtypes == 'object'].columns:
    num=-1
    mapping={}
    for j in train.groupby(i)['SalePrice'].mean().sort_values().index:
        num+=1
        mapping[j]=num  
    train[i]= train[i].map(mapping)
    test[i]=test[i].map(mapping)
```
object型の変数が消えたことを確認

```python
#trainデータ
train.dtypes.value_counts()
#出力結果
int64      50
float64    24
dtype: int64

##testデータ
test.dtypes.value_counts()
#出力結果
int64      48
float64    25
dtype: int64
```
これで全て数字になった
##外れ値を消す
外れ値を消すために、全て可視化して、目視で外れ値を消していく

```python
for i in train.columns:
    plt.scatter(train[i],train['SalePrice'])
    plt.xlabel(i)
    plt.show()
```

外れ値を消す

```python
train = train.drop(train[(data_train['TotalSF']>8.7) & (train['SalePrice']<12.3)].index)
train = train.drop(data_train[(train['GrLivArea']>8.3) & (train['SalePrice']<13)].index)
train = train.drop(train[(train train['GarageArea']>1200) & (train['SalePrice']<12.5)].index)
```

##特徴量を工夫

ひとまず説明変数と目的変数に分ける

```python
Y_train=train['SalePrice']
X_train=train.drop('SalePrice',axis=1)
X_test=test
```
特徴量を減らすため、特徴量の重要度を求める

```python
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(n_estimators=80)
rf = rf.fit(X_train, Y_train)

ranking = np.argsort(-rf.feature_importances_)
f, ax = plt.subplots(figsize=(11, 9))
sns.barplot(x=rf.feature_importances_[ranking], y=X_train.columns.values[ranking], orient='h')
ax.set_xlabel("feature importance")
plt.tight_layout()
plt.show()
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/326720/984ae1bc-32a5-a0cd-68d2-31ef83395b0b.png)
このうち上位30この特徴量を使用

```python
X_train = X_train.iloc[:,ranking[:30]]
X_test = X_test.iloc[:,ranking[:30]]
```
また、OverallQualとTotalSFが飛び抜けているので、２つの変数を掛け合わせた新しい特徴量を作る。

```python
X_train['QualSF']=X_train['OverallQual']*X_train['TotalSF']
X_test['QualSF']=X_test['OverallQual']*X_test['TotalSF']
```
正規化

```python
mean = X_train.mean(axis=0)
std = X_train.std(axis=0)
X_train = (X_train - mean) / std
X_test = (X_test - mean) / std
```

多重共線性をなくすために、相関の強い特徴量を削除する

```python
correlation = X_train.corr()
f , ax = plt.subplots(figsize = (20,20))
sns.heatmap(correlation,square = True,  vmax=0.8,annot=True)
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/326720/a74dce55-b17f-87f0-1c22-9f274d1f3b4f.png)

```python
X_train=X_train.drop(['GrLivArea','garage_elapsed','GarageCars','remod_elapsed'],axis=1)
X_test=X_test.drop(['GrLivArea','garage_elapsed','GarageCars','remod_elapsed'],axis=1)
```
##モデリング
データ数が少ないので、交差検証法、グリッドサーチを用い、複数のモデルで検証し、最も結果の良かったものを採用する。

```python
# KFold
from sklearn.model_selection import KFold
kf_3 = KFold(n_splits=6, shuffle=True, random_state=0)

#モデルの呼び出し
from sklearn.linear_model import Lasso
from sklearn.ensemble import RandomForestRegressor
from sklearn.svm import SVR
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import ElasticNet
from sklearn.linear_model import Ridge
from xgboost import XGBRegressor

# 各モデルのインスタンスを生成
ridge=Ridge()
elastic= ElasticNet()
lasso = Lasso()
rf = RandomForestRegressor()
svr = SVR()
XGB= XGBRegressor()

#グリッドサーチ用パラメータの設定
elasticnet_parameters= {'alpha':[0.1, 0.5, 1],'l1_ratio':[0.3,0.6,0.9]}
lasso_parameters = {'alpha':[0.1, 0.5, 1]}
rf_parameters= {'n_estimators':[100, 500, 2000], 'max_depth':[3, 5, 10]}
svr_parameters = {'C':[1e-1, 1e+1, 1e+3], 'epsilon':[0.05, 0.1, 0.3]}
ridge_parameters = {'alpha':[0.1, 0.5, 1]}
xgb_parameters = {"learning_rate":[0.1,0.3,0.5],"max_depth": [2,3,5,10],"subsample":[0.5,0.8,0.9,1],"colsample_bytree": [0.5,1.0],}

#グリッドサーチ
elastic_gs=GridSearchCV(elastic, elasticnet_parameters,cv=kf_3)
elastic_gs.fit(x_train,y_train)
lasso_gs = GridSearchCV(lasso, lasso_parameters,cv=kf_3)
lasso_gs.fit(x_train,y_train)
rf_gs = GridSearchCV(rf, rf_parameters,cv=kf_3)
rf_gs.fit(x_train,y_train)
svr_gs = GridSearchCV(svr, svr_parameters,cv=kf_3)
svr_gs.fit(x_train,y_train)
ridge_gs=GridSearchCV(ridge, ridge_parameters,cv=kf_3)
ridge_gs.fit(x_train,y_train)
xgb_gs = GridSearchCV(XGB, xgb_parameters, cv = kf_3)
xgb_gs.fit(x_train,y_train)

#平均二乗誤差を計算
from sklearn.metrics import mean_squared_error

def RMSE(instance, modelname):
    Y_pred = instance.predict(x_test)
    print("{}でのRMSE:{:.4f}".format(modelname ,np.sqrt(mean_squared_error(y_test, Y_pred))))

RMSE(elastic_gs,'ElasticNet')
RMSE(lasso_gs,'ラッソ回帰')
RMSE(rf_gs,'ランダムフォレスト')
RMSE(svr_gs,'SVR')
RMSE(ridge_gs,'Ridge')
RMSE(xgb_gs,'XGBoost')

#出力結果
ElasticNetでのRMSE:0.1630
ラッソ回帰でのRMSE:0.1689
ランダムフォレストでのRMSE:0.1349
SVRでのRMSE:0.4008
RidgeでのRMSE:0.1178
XGBoostでのRMSE:0.1272
```
この結果より、ridgeを採用しました。
<img width="945" alt="スクリーンショット 2018-12-30 16.33.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/325530/905bb03f-9674-a028-ac5e-75c61103092b.png">

順位もぼちぼちでは無いでしょうか(12/30/2018)

#Keras
せっかくKerasも習ったのでKerasで多層ニューラルネットモデル（MLP）を作成して回帰分析をしてみました。
##numpy.ndarrayに変換
全結合層に入力するためにpandas.DataFrameをnumpy.ndarrayに変換します。

```python
x_train=np.array(x_train)
y_train=np.array(y_train)
x_test=np.array(x_test)
y_test=np.array(y_test)
X_test=np.array(X_test)
```
##ネットワークの定義
```python
from keras.models import Sequential

model = Sequential()
model.add(Dense(10, input_shape=(x_train.shape[1],), activation='relu'))
model.add(Dense(15))
model.add(Activation('relu'))
model.add(Dense(1))
 
from keras import optimizers
optimizer = optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08)
 
model.compile(loss='mse',
                optimizer=optimizer,
                metrics=['mae'])
model.summary()
```
今回は以下の様なネットワークの定義を定義してみました。
<img width="487" alt="スクリーンショット 2018-12-30 15.09.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/325530/b2008b3d-83c4-9654-83d6-2652111d281f.png">

##EarlyStoppingのインスタンスを生成
過学習を抑えるため、Kerasのearly stoppingを利用しました。

```python
from keras.callbacks import EarlyStopping

early_stop = keras.callbacks.EarlyStopping(monitor='val_loss', patience=20,verbose=0, mode='min')
```
##学習の実行

```python
history = model.fit(x_train, y_train, epochs=500,
                    validation_data=(x_test,y_test), verbose=1,
                    callbacks=[early_stop])
```
## 損失値(Loss)の遷移のプロット
プロットして損失値の推移を視覚化します。

```python
def plot_history_loss(history):
    plt.plot(history.history['mean_absolute_error'],label="loss for training")
    plt.plot(history.history['val_mean_absolute_error'],label="loss for validation")
    plt.title('model loss')
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.legend(loc='best')
    plt.show()

plt.ylim([0,2])
plot_history_loss(history)
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/325530/72243e1f-d007-868c-ed9c-7a2587bec00e.png)

##testデータで予測
先ほど定義した関数で、平均二乗誤差(RMSE)を出力します。

```python
Y_pred = model.predict(x_test)
print("{}でのRMSE:{:.4f}".format('MLP' ,np.sqrt(mean_squared_error(y_test, Y_pred))))
#出力結果
MLPでのRMSE:0.1780
```
あまり結果は良くありませんでした。今度はもう少し層を深くしてみます
##ネットワークの定義②
活性化関数やOptimizers等は変えずに、層を深くし、過学習を抑えるためにDropoutレイヤーを追加しました。

```python
model = Sequential()
model.add(Dense(10, input_shape=(x_train.shape[1],), activation='relu'))
model.add(Dense(15))
model.add(Activation('relu'))
model.add(Dropout(0.50))
model.add(Dense(100))
model.add(Activation('relu'))
model.add(Dropout(0.50))
model.add(Dense(100))
model.add(Activation('relu'))
model.add(Dropout(0.50))
model.add(Dense(100))
model.add(Activation('relu'))
model.add(Dropout(0.50))
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dense(1))
```
<img width="475" alt="スクリーンショット 2018-12-30 16.20.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/325530/36d80d6a-5e20-3139-1105-4d07132a250a.png">
あとは先ほどと同じ様に損失値(Loss)の遷移をプロット
![image.png](https://qiita-image-store.s3.amazonaws.com/0/325530/1c06c456-c26b-3e1d-8674-a1eadb073bb2.png)
平均二乗誤差を出力すると、

```python
MLPでのRMSE:0.2434
```
...むしろ悪くなってしまいました。過学習をしてしまったのかもしれません。