「データ前処理」-  Kaggle人気チュートリアル

# はじめに

Kaggleで人気の機械学習のチュートリアルをやってみたので共有します。
このチュートリアルでは、**データの前処理**について紹介していきます。

チュートリアルの原文はこちらです。
[Comprehensive data exploration with Python
](https://www.kaggle.com/pmarcelino/comprehensive-data-exploration-with-python)

使うデータはこちらでダウンロードしてください。
[House Prices: Advanced Regression Techniques / Data](https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data)

このデータは、**住宅の販売価格を予測するデータ分析コンペ**で使用されたものです。

# 導入

このチュートリアルでやることは次の通りです。

**1. 問題を理解する**
**2. 予測する変数(`SalePrice`)について詳しく見る**
**3. 変数同士の関係について詳しく見る**
**4. 欠損値・外れ値の取り扱い**
**5. 解析の事前確認**

まず、ライブラリとデータを読み込んで、データの中身を見ておきます。

```py
#invite people for the Kaggle party
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
```

```py
#bring in the six packs
df_train = pd.read_csv('../input/train.csv')
```

```py
#check the decoration
df_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')
```

全部で81個の特徴があります。
例えば、`Street`は住宅へのアクセスのタイプで、砂利道や舗装などのデータが含まれています。

# 1. 問題を理解する

まず、問題を理解するために変数を一つずつ見て、本当に価格に関連性がありそうなのかを考えます。
これによって、データ分析における「第六感」を養うことができます。

+ 本当に必要な変数なのか？  
例えば、家を購入する時に`ベニヤ板の種類(MasVnrType)`を気にすると思いますか？
+ その変数はどれくらい影響があるのか？  
例えば、「絶対、このベニヤ板がいい！」とか「できれば、このベニヤ板がいいな。」とか。
+ また、変数同士で関係がないか？  
例えば、似たような意味合いである`住宅の平坦度(LandContour)`と`住宅の傾斜(LandSlope)`とか。


このチュートリアルでは、`販売価格`に関係しそうな変数を以下の４つに絞って見ていきます。
（本来は、他の変数も分析する必要があります。）

+ `OverallQual`：素材・完成度
+ `YearBuilt`：建設日
+ `TotalBsmtSF`：地下の広さ
+ `GrLivArea`：地上の広さ

# 2. 予測する変数(`SalePrice`)について詳しく見る

予測する`SalePrice`について、詳しく見て見ます。

```py
#descriptive statistics summary
df_train['SalePrice'].describe()
```

```:出力
count      1460.000000
mean     180921.195890
std       79442.502883
min       34900.000000
25%      129975.000000
50%      163000.000000
75%      214000.000000
max      755000.000000
Name: SalePrice, dtype: float64
```

平均価格や最低価格、最大価格などが確認できます。

ヒストグラムを書いて見ます。

```py
#histogram
sns.distplot(df_train['SalePrice']);
```

<img width="500" alt="hist.png" src="https://qiita-image-store.s3.amazonaws.com/0/247972/86f517d8-7c39-4a0d-f9d2-5cc550c74394.png">

正規分布ではなく、少し歪んでいるようです。
また、ピークも確認できます。

歪度(どれくらい非対称性か)と尖度(どれくらいピークが鋭いか)を見てみます。

```py
#skewness and kurtosis
print("Skewness: %f" % df_train['SalePrice'].skew())
print("Kurtosis: %f" % df_train['SalePrice'].kurt())
``` 

```:出力
Skewness: 1.882876
Kurtosis: 6.536282
```

## 2-1.　`SalePrice`との関係

単純に、先ほど選んだ４つの変数との関係をプロットして見てみます。

+ `OverallQual`：素材・完成度
+ `YearBuilt`：建設日
+ `TotalBsmtSF`：地下の広さ
+ `GrLivArea`：地上の広さ

**「GrLivArea」との関係**

```py
#scatter plot grlivarea/saleprice
var = 'GrLivArea'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
```

<img width="498" alt="スクリーンショット 2018-08-09 19.03.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/247972/992f615b-b66e-768a-0a8b-56cd97570238.png">

線形の関係性がありそうです。

---

**「TotalBsmtSF」との関係**

```py
#scatter plot totalbsmtsf/saleprice
var = 'TotalBsmtSF'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
```

<img width="490" alt="スクリーンショット 2018-08-09 19.06.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/247972/efd76e81-c62b-da02-a3aa-029345562054.png">

線形の関係性がありそうです。

---

**「OverallQual」との関係**

```py
#box plot overallqual/saleprice
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);
```

<img width="610" alt="スクリーンショット 2018-08-09 19.09.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/247972/dfb643e2-9ad6-7f90-dc04-e006b197b593.png">

明らかに、関係がありそうです。

---

**「YearBuilt」との関係**

```py
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);
```

<img width="929" alt="スクリーンショット 2018-08-09 19.12.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/247972/f4051be3-412a-96d2-d122-05777842c93a.png">

強い傾向ではないが、新しい住宅の方が`SalePrice`が高いように見えます。

※ このデータの`販売価格`が、一定価格であるかは分からない。インフレによって`販売価格`は変化することに注意。

## 2-2.　ここまでの要約

+ `地上・地下の広さ`と`販売価格`の関係は、線形的で正の相関がありそう
+ `素材・完成度、建設日`と`販売価格`の間にも、関係性がありそう

# 3. 変数同士の関係について詳しく見る

今までは、主観的に変数を見てきました。
次は、客観的に変数を見ていきます。

見るものは、以下の３つです。

1. 全変数の相関行列
2. `SalePrice`と相関が高めの変数(トップ１０)に絞った相関行列
3. `SalePrice`と相関が高めの変数(トップ１０)に絞った散布図

**1. 全変数の相関行列**

まずは、全変数同士の相関行列を見てみます。
ちなみに、この相関行列はヒートマップとも呼ばれます。

```py
#correlation matrix
corrmat = df_train.corr()
f, ax = plt.subplots(figsize=(12, 9))
sns.heatmap(corrmat, vmax=.8, square=True);
```

![__results___30_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/c3868765-80de-1db2-f1c1-521fab8161e3.png)


この相関行列から、互いに相関している変数を見つけることができます。

例えば、`Garange系`の変数は、互いに`Garage系`の変数に相関していることが確認できます。
このことは、`ガレージに入る車の数`は`ガレージの広さ`によって決まると考えられるからです。

この様に、互いに相関している変数を**多重共線性を持つ**と言います。

---

**2. `SalePrice`と相関が高めの変数(トップ１０)に絞った相関行列**

次に、`SalePrice`と相関の高い変数トップ１０に注目して、相関行列を見てみます。

```py
#saleprice correlation matrix
k = 10 #number of variables for heatmap
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)
plt.show()
```

![__results___33_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/0e39e1f9-44ad-f8fe-2f8f-6b6dfa8cd48c.png)

これにより、どの変数が`SalePrice`に関係ありそうなのかを見ることができます。

厳密には、`YearBuilt`のような変数には時系列解析をするべきであると思われますが、今回はそのまま続けます。

---

**3. `SalePrice`と相関が高めの変数(トップ１０)に絞った散布図**

最後に、散布図を見てみます。

```py
#scatterplot
sns.set()
cols = ['SalePrice', 'OverallQual', 'GrLivArea', 'GarageCars', 'TotalBsmtSF', 'FullBath', 'YearBuilt']
sns.pairplot(df_train[cols], size = 2.5)
plt.show();
```

![__results___37_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/91a62681-14df-cc4b-0478-362261ee300a.png)

`TotalBsmtSF`と`GrLiveArea`の散布図を見ると、直線的な模様を見ることができます。
この直線より下の点は、予測のための変数としては期待できません。
なぜなら、地上より地下の面積が広いとは、あまり考えられないからです。

`SalePrice`と`YearBuilt`の間には、指数関数的な形が示されていることが確認できます。

# 4. 欠損値・外れ値の取り扱い

データに欠損値・外れ値はつきものですが、そのままでは解析できないので対処していきます。
以下の対処法は、ほんの一例です。

## 4-1. 欠損値

まず、欠損しているデータについて見ていきます。

```py
#missing data
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'])
missing_data.head(20)
```

<img width="203" alt="スクリーンショット 2018-08-22 11.49.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/247972/12a15f4e-dffa-942e-a290-05eea766ec66.png">

欠損したデータをどう処理するか考えてみます。

右列のPercent列は、データの欠損率を示しています。
まず、データが１５％以上欠損している変数は削除(解析には使わない)することにします。

次に、`Garage系`の変数を見ると、値が全て同じことから、同じ物件を参照していると考えられます。
そして、`Garage系`の変数は、前述した`GarageCars`で表現できる(多重共線性)と考えられるため、これも削除します。
同様のロジックで、`Bsmt系`の変数も削除します。

`MasVnrArea`(ベニヤ板の面積)と`MasVnrType`(ベニヤ板の種類)については、それほど重要な変数ではないと考えられます。
なぜなら、これらは`YearBuilt`と`OverallQual`と強い相関があるので、`MasVnr系`の変数を削除しても情報は失われないと考えられるからです。

`Electrical`については欠損値が１つだけなので、変数は削除せずに、この１軒の住宅データごと削除することにします。


まとめると、`Electrical`以外の変数において欠損値を含む変数を削除し、`Electrical`が欠損している１軒の住宅データを削除します。

```py
#dealing with missing data
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() #just checking that there's no missing data missing...
```

```:出力
0
```

## 4-2. 外れ値

次に、外れ値について見ていきます。
外れ値は予測に影響を及ぼすので、注意を払う必要があります。

### 4-2-1. 単変量解析

まずは、`SalePrice`のみについて外れ値を見てみます。
外れ値を調べるには外れ値とする域値を設定する必要があり、その前にデータを標準化します。
ここでの標準化とは、平均０および標準偏差１に変換することを意味します。

```py
#standardizing data
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)
```

```:出力
outer range (low) of the distribution:
[[-1.83820775]
 [-1.83303414]
 [-1.80044422]
 [-1.78282123]
 [-1.77400974]
 [-1.62295562]
 [-1.6166617 ]
 [-1.58519209]
 [-1.58519209]
 [-1.57269236]]

outer range (high) of the distribution:
[[3.82758058]
 [4.0395221 ]
 [4.49473628]
 [4.70872962]
 [4.728631  ]
 [5.06034585]
 [5.42191907]
 [5.58987866]
 [7.10041987]
 [7.22629831]]
```

この結果から、安い(低い)範囲の住宅は平均0に近いが、高い範囲の住宅はあまり平均０に近くないことが確認できます。
つまり、高い住宅にはばらつきがあるということです。
特に、下の２つ( [7.10041987]と[7.22629831] )は外れ値と考えることができますが、とりあえず今回はそのままにしておきます。

現実を考えても、高級マンションや豪邸など異常に高い住宅があることから想定できます。

### 4-2-2. 二変量解析

二変量解析は、前述した散布図を使用します。

まずは、`SalePrice` - `GrLivArea`間で見てみます。

```py
#bivariate analysis saleprice/grlivarea
var = 'GrLivArea'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
```

![__results___50_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/360f905a-c492-7e32-46b5-f8846e4a74ff.png)

この散布図から、右の方に外れ値が２つあることが確認できます。
おそらくこれらは、土地の安い農家だと思われます。
よって、これら２つのデータは削除します。

```py
#deleting points
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)
```

---

次に、`SalePrice` - `TotalBsmtSF`間で見てみます。

```py 
#bivariate analysis saleprice/grlivarea
var = 'TotalBsmtSF'
data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)
data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));
```

![__results___53_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/fe8d6fbe-e64a-d72c-64d1-fc0939dc226c.png)

`TotalBsmtSF` > 3000　の値を外れ値としたい気もしますが、そのままにしておきます。
要は、外れ値の定義は感覚です。

# 5. 解析の事前確認

ここまでで、データ整形を行ってきました。
最後に、`SalePrice`に多変量解析が適応できるかを確認します。

多変量解析を行う際には、次の4つの前提条件を確認する必要があります。

**① 正規性**
データが正規分布に従っている方が望まれます。

**② 等分散性**
全変数において分散が同じレベルであることが望まれます。

**③ 線型性**
散布図を見て、線形な関係になっていることが望まれます。
前述で、`SalePrice`と`OverallQual`・`YearBuilt`・`TotalBsmtSF`・`GrLivArea`との線形性を確認しました。

**④ 相関誤差の欠如(Absence of correlated error)**
時系列データの場合に注意すべきと書いてあるようです。
ちょっと、理解できませんでした。。。(誰か、教えてください。)
とりあえず、原文を載せておきます。

>
Correlated errors, like the definition suggests, happen when one error is correlated to another. For instance, if one positive error makes a negative error systematically, it means that there's a relationship between these variables. This occurs often in time series, where some patterns are time related. We'll also not get into this. However, if you detect something, try to add a variable that can explain the effect you're getting. That's the most common solution for correlated errors.

`③ 線形性`は確認済みで、`④ 相関誤差の欠如`は関係ない(らしい)ので、`① 正規性`と`② 等分散性`を確認します。

## 5-1. 正規性

ヒストグラムと正規確率プロットを使って正規性を確認します。
正規確率プロットは、プロットが一直線上に並んでいれば正規分布に従っていると考えられます。

```py 
#histogram and normal probability plot
sns.distplot(df_train['SalePrice'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)
```
![__results___59_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/a68b4953-5bea-5b82-fca5-8d1429867259.png)

![__results___59_1.png](https://qiita-image-store.s3.amazonaws.com/0/247972/2db2f4a5-f359-b3a5-58f4-5c45220b9bd0.png)

グラフを見ると、`SalePrice`は正規分布にあまり従っていない様です。
ですが、正の歪度を持つ場合は対数変換が有効なのでやってみます。

```py 
#applying log transformation
df_train['SalePrice'] = np.log(df_train['SalePrice'])
#transformed histogram and normal probability plot
sns.distplot(df_train['SalePrice'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['SalePrice'], plot=plt)
```

![__results___62_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/dc288752-14c1-b9e6-8cbb-7b623ea19dba.png)

![__results___62_1.png](https://qiita-image-store.s3.amazonaws.com/0/247972/4ca1b659-2832-4bd5-9d5d-f95216fda528.png)

うまく正規分布に従った様です。

---

`GriLivArea`について見てみます。

```py 
#histogram and normal probability plot
sns.distplot(df_train['GrLivArea'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['GrLivArea'], plot=plt)
```

![__results___64_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/d142a59b-3816-c010-c5d9-985417cad732.png)

![__results___64_1.png](https://qiita-image-store.s3.amazonaws.com/0/247972/86830360-0e14-450b-13c0-68b4e5e78143.png)


同じく、正の歪度がありそうなので対数変換します。

```py 
#data transformation
df_train['GrLivArea'] = np.log(df_train['GrLivArea'])
#transformed histogram and normal probability plot
sns.distplot(df_train['GrLivArea'], fit=norm);
fig = plt.figure()
res = stats.probplot(df_train['GrLivArea'], plot=plt)
```

![__results___67_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/aa899c53-b79a-c26d-5d3f-2512b7ca5d05.png)

![__results___67_1.png](https://qiita-image-store.s3.amazonaws.com/0/247972/1db1dc9e-e5d6-cd0a-5f10-d6cb93ede8f1.png)

うまく正規分布に従った様です。

## 5-2. 等分散性

言葉での説明が難しかったので、引用した図を載せておきます。

要は、プロットの広がりがどの場所も等しくなることが、等分散性であるといいます。

![r-54-638.jpg](https://qiita-image-store.s3.amazonaws.com/0/247972/9ff48625-5962-c18a-2656-af21493d9b53.jpeg)
引用：[臨床心理学におけるR入門: データハンドリングから統計解析まで](https://www.slideshare.net/YoshihikoKunisato/r-74207682)

では、対数変換後の`SalePrice`と`GrLivArea`のプロットを見てみます。


```py
#scatter plot
plt.scatter(df_train['GrLivArea'], df_train['SalePrice']);
```

![__results___76_0.png](https://qiita-image-store.s3.amazonaws.com/0/247972/17b62a86-ace8-3fc5-a447-690fee87ba16.png)

`3. 変数同士の関係について詳しく見る`で見た散布図と比較してみると、対数変換後の方が等分散性になっていることが確認できます。

---

次に、対数変換後の`SalePrice`と、`GrLivArea`のプロットを見てみます。

```py 
#scatter plot
plt.scatter(df_train[df_train['TotalBsmtSF']>0]['TotalBsmtSF'], df_train[df_train['TotalBsmtSF']>0]['SalePrice']);
```

![tmp.png](https://qiita-image-store.s3.amazonaws.com/0/247972/8b03d90a-16f5-3af4-13b7-5b8209d293f2.png)


等分散性になっていると言えるでしょう。

---

最後に、カテゴリカル変数を`男：０`、`女：１`のように変換して、このチュートリアルは終了です。

```py 
#convert categorical variable into dummy
df_train = pd.get_dummies(df_train)
```

# 最後に

この様に、長いデータの前処理を行って、やっとモデルの選択・学習へのステップに進むことができます。
ですが、モデルの選択・学習にも様々な手法があり、どれを使うのかはあなた次第です。

ちなみに、変数4つとランダムフォレストを使って簡単な予測を行い、結果をKaggleに提出したところ、スコア(RMSLE:小さい方が良い)が`0.17495`でした。
予測のサンプルプログラムは、[「GitHub」](https://github.com/hokuto-HIRANO/Kaggle_Sample_1)に載せておきます。

また、さらに強力な学習法・アンサンブル学習については、[「Pythonでアンサンブル(スタッキング)学習 & 機械学習チュートリアル in Kaggle」](https://qiita.com/hokuto_HIRANO/items/2c35a81fbc95f0e4b7c1)で紹介しているので、よければご覧ください。