【Python】データ分析、機械学習実践(Kaggle)〜データ前処理編〜

# はじめに
前回の記事[
【Python】初めての データ分析・機械学習（Kaggle）](https://qiita.com/Julio_dev/items/8d77e3d18883e4bbbe24)
[【Python】初めての データ分析・機械学習（Kaggle）〜Part2〜](https://qiita.com/Julio_dev/items/4754a8dc545b7023e8c1)
に引き続き、Kaggleで比較的優しいコンペ「[House Prices: Advanced Regression Techniques](https://www.kaggle.com/c/house-prices-advanced-regression-techniques)」に挑戦しました！

今回のコンペは、住宅に関する情報の変数をもとに、住宅の価格を推定するというもの。
しかし、この住宅に関する変数が80もあり、いきなり怖気付きました、、(笑)

「こんなのできるのか、、？」て思いつつ、今回もしっかり先人の知恵を借りました！笑
参考コード↓↓↓

- [Stacked Regressions : Top 4% on LeaderBoard](https://www.kaggle.com/serigne/stacked-regressions-top-4-on-leaderboard)
- [Comprehensive data exploration with Python](https://www.kaggle.com/pmarcelino/comprehensive-data-exploration-with-python)

大まかな流れは、以下のようになっています。

1. 特徴エンジニアリング
    1. **Imputing missing values** 欠損値の穴埋め
    2. **Transforming** データ変換(log変換等)
    3. **Label Encoding** カテゴリカルデータのエンコード
    4. **Box Cox Transformation** : 正規分布に近づけさせるための変換
    5. **Getting dummy variables** カテゴリカルデータを数値データへ
2. モデリング(スタッキングアンサンブル学習)
    1. ベースモデル解析
    2. 第２モデル解析


そして、本記事では、 **特徴エンジニアリング** に焦点を当てていきます！

# データ取得・ライブラリインポート

```python
#ライブラリのimport

import numpy as np # 線形代数
import pandas as pd # データ処理、csvファイルの操作
%matplotlib inline
import matplotlib.pyplot as plt  # Matlab-style plotting
import seaborn as sns
color = sns.color_palette()
sns.set_style('darkgrid')
import warnings
def ignore_warn(*args, **kwargs):
    pass
warnings.warn = ignore_warn #不要な警告の無視(from sklearn and seaborn)


from scipy import stats
from scipy.stats import norm, skew #統計操作

pd.set_option('display.float_format', lambda x: '{:.3f}'.format(x)) #小数点の設定

from subprocess import check_output
print(check_output(["ls", "../input"]).decode("utf8")) #ファイルが有効かチェック
```

```text:出力
data_description.txt
sample_submission.csv
test.csv
train.csv
```

データフレーム作成

```python
#データの取得、データフレームの作成
train = pd.read_csv('../input/train.csv')
test = pd.read_csv('../input/test.csv')
```
データフレームを見てみよう！

```python
##データフレームの表示
train.head(5)
```

<img width="1309" alt="スクリーンショット 2020-03-16 16.03.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383306/b7e12759-71c7-abf3-4977-a95e454a1003.png">

```python
#サンプル数と特徴数
print("The train data size before dropping Id feature is : {} ".format(train.shape))
print("The test data size before dropping Id feature is : {} ".format(test.shape))

#'Id'カラムの保存
train_ID = train['Id']
test_ID = test['Id']

#予測のプロセスではIDは不必要なので削除
train.drop("Id", axis = 1, inplace = True)
test.drop("Id", axis = 1, inplace = True)

#IDが消えているかもう一度確認
print("\nThe train data size after dropping Id feature is : {} ".format(train.shape)) 
print("The test data size after dropping Id feature is : {} ".format(test.shape))
```

```text:出力
The train data size before dropping Id feature is : (1460, 81) 
The test data size before dropping Id feature is : (1459, 80) 

The train data size after dropping Id feature is : (1460, 80) 
The test data size after dropping Id feature is : (1459, 79) 
```
学習用データが1460個,特徴が80個
テスト用データが1459個,特徴が79個になりました！

え、**特徴多すぎ！！**
こんなんどうやって分析すればいいんか、、

**とりあえず、データの欠損値、歪度、カテゴリカルデータの数値化**をやっていきましょう！

# データ前処理

まずは、欠損値から見ていきます！
基本的に*データの欠損率が15%以上の特徴は消していいとのこと!!*

## 1. 欠損値

学習用データのtrainとテスト用データのtestで**まとめて欠損値を処理する**ために一度、**データを統合**します！

```python
ntrain = train.shape[0]
ntest = test.shape[0]
y_train = train.SalePrice.values
all_data = pd.concat((train, test)).reset_index(drop=True)
all_data.drop(['SalePrice'], axis=1, inplace=True)
print("all_data size is : {}".format(all_data.shape))
```

```text:出力
all_data size is : (2917, 79)
```

データ数が2917、特徴数が79なのでいい感じに統合できました！

---

欠損値を持つ特徴を効率よく洗い出すために、


**1. 全特徴の欠損率を算出して、欠損値率>０の特徴を新しいデータフレームに入れる**
**2. 欠損率を可視化**
**3. 特徴ごとに、削除or値の代入の検討をする**

この流れで欠損値の処理をしていきます！

**1. 全特徴の欠損率を算出して、欠損値率>０の特徴を新しいデータフレームに入れる**

```python
all_data_na = (all_data.isnull().sum() / len(all_data)) * 100
all_data_na = all_data_na.drop(all_data_na[all_data_na == 0].index).sort_values(ascending=False)[:30]#欠損値が含まれる変数のみ抽出
missing_data = pd.DataFrame({'Missing Ratio' :all_data_na})#データフレームに入れる。
missing_data.head(20)
```

出力
<img width="191" alt="スクリーンショット 2020-03-16 16.44.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383306/4dc81e53-f1ec-9515-2299-f305371cddbc.png">


**2. 欠損率を可視化**

```python
f, ax = plt.subplots(figsize=(15, 12))
plt.xticks(rotation='90')
sns.barplot(x=all_data_na.index, y=all_data_na)
plt.xlabel('Features', fontsize=15)
plt.ylabel('Percent of missing values', fontsize=15)
plt.title('Percent missing data by feature', fontsize=15)
```

<img width="755" alt="スクリーンショット 2020-03-17 14.42.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383306/0742f893-ea28-07b9-ecf1-00c861a0b3fb.png">


**3. 特徴ごとに、削除or値の代入の検討をする**
上のグラフに上がった欠損値を含む特徴に値を入れていきます！
特徴１つ１つ検討しながらなので、少し大変ですがやっていきます！

- **PoolQC** : ほとんどが欠損値。欠損値には「プールが無い」という意味でNoneを入れる

```python
all_data["PoolQC"] = all_data["PoolQC"].fillna("None")
```

- **MiscFeature** : データの説明によると、NAは「その他機能がない」という意味。欠損値にはNoneを入れます

```python
all_data["MiscFeature"] = all_data["MiscFeature"].fillna("None")
```

- **Alley** : データ説明によると、「路地へのアクセスなし」という意味。欠損値にはNoneを入れます

```python
all_data["Alley"] = all_data["Alley"].fillna("None")
```

- **Fence** : 「フェンスがない」という意味。欠損値にはNoneを入れます

```python
all_data["Fence"] = all_data["Fence"].fillna("None")
```

- **FireplaceQu** : 「暖炉がない」という意味。欠損値にはNoneを入れます

```python
all_data["FireplaceQu"] = all_data["FireplaceQu"].fillna("None")
```

- **LotFrontage** : 近所の家の平均値を欠損値に代入する
ちなみに、median()は平均値を取得する

```python
#近隣でグループ化し、グループのLotFrontageの平均値を欠損値に代入
all_data["LotFrontage"] = all_data.groupby("Neighborhood")["LotFrontage"].transform(
    lambda x: x.fillna(x.median()))
```


- **GarageType, GarageFinish, GarageQual and GarageCond** : 欠損値をNoneでうめる

```python
for col in ('GarageType', 'GarageFinish', 'GarageQual', 'GarageCond'):
    all_data[col] = all_data[col].fillna('None')
```

- **GarageYrBlt, GarageArea and GarageCars** : 欠損値に0を入れる

```python
for col in ('GarageYrBlt', 'GarageArea', 'GarageCars'):
    all_data[col] = all_data[col].fillna(0)
```

- **BsmtFinSF1, BsmtFinSF2, BsmtUnfSF, TotalBsmtSF, BsmtFullBath and BsmtHalfBath** : 欠損値に0を入れる

```python
for col in ('BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF','TotalBsmtSF', 'BsmtFullBath', 'BsmtHalfBath'):
    all_data[col] = all_data[col].fillna(0)
```

- **BsmtQual, BsmtCond, BsmtExposure, BsmtFinType1 and BsmtFinType2** :カテゴリカルデータの欠損値にNoneを入れる

```python
for col in ('BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2'):
    all_data[col] = all_data[col].fillna('None')
```

- **MasVnrArea and MasVnrType** : typeの方にはNone、Areaの方には0入れる

```python
all_data["MasVnrType"] = all_data["MasVnrType"].fillna("None")
all_data["MasVnrArea"] = all_data["MasVnrArea"].fillna(0)
```

- **MSZoning (The general zoning classification)** : ’RL’が平均値から最も遠い値である。そのため、欠損値にはRLを入れる

```python
all_data['MSZoning'] = all_data['MSZoning'].fillna(all_data['MSZoning'].mode()[0])
```

- **Utilities** :役に立たないから削除

```python
all_data = all_data.drop(['Utilities'], axis=1)
```

- **Functional** : NAはTypicalなのでTypを入れる

```python
all_data["Functional"] = all_data["Functional"].fillna("Typ")
```

- **Electrical** : 1つだけ欠損値がある。これには、sBkrkを入れる
mode():最頻値を取得する

```python
all_data['Electrical'] = all_data['Electrical'].fillna(all_data['Electrical'].mode()[0])
```

- **KitchenQual**: １つだけの欠損値には、最頻値を入れる

```python
all_data['KitchenQual'] = all_data['KitchenQual'].fillna(all_data['KitchenQual'].mode()[0])
```

- **Exterior1st and Exterior2nd** : 最頻値を欠損値に入れる

```python
all_data['Exterior1st'] = all_data['Exterior1st'].fillna(all_data['Exterior1st'].mode()[0])
all_data['Exterior2nd'] = all_data['Exterior2nd'].fillna(all_data['Exterior2nd'].mode()[0])
```

- **SaleType** :最頻値

```python
all_data['SaleType'] = all_data['SaleType'].fillna(all_data['SaleType'].mode()[0])
```

- **MSSubClass** : NoneをNanに入れる

```python
all_data['MSSubClass'] = all_data['MSSubClass'].fillna("None")
```

残りの欠損値の確認

```python
#欠損値が残ってないか確認
all_data_na = (all_data.isnull().sum() / len(all_data)) * 100
all_data_na = all_data_na.drop(all_data_na[all_data_na == 0].index).sort_values(ascending=False)
missing_data = pd.DataFrame({'Missing Ratio' :all_data_na})
missing_data.head()
```
欠損値なし
これで欠損値の処理は終わりです！
多かった、、

## 2. カテゴリーデータ処理
カテゴリーデータとは名義尺度や順序尺度で表されたデータのことです。
ざっくり言えば、数値以外のデータのことです！

カテゴリーデータのままだと分析や学習はできないので、数値化していきます！

###順序尺度データの処理
順序尺度のデータとは、順序にのみ意味のあるデータのことです。例えば、ファストフードの飲み物のサイズ「s, M, L」をs→0, M→1, L→2のように数値化されたもののことを指します。
順序データの注意点としては、**平均や標準偏差などの数値計算はできない**ことです。

まずは、順序データの数値を文字データに変換しておきます。（あとで、カテゴリデータをまとめて数値化するため）

```python
#MSSubClass=The building class
all_data['MSSubClass'] = all_data['MSSubClass'].apply(str)


#Changing OverallCond into a categorical variable
all_data['OverallCond'] = all_data['OverallCond'].astype(str)


#Year and month sold are transformed into categorical features.
all_data['YrSold'] = all_data['YrSold'].astype(str)
all_data['MoSold'] = all_data['MoSold'].astype(str)
```

### カテゴリデータを数値データに変換

LabelEncoder()で順序データと名義データをまとめて数値化していきます！
.fitでデータ選択、.transform()で数値に変換、
参考：[scikit-learnのLabelEncoderの使い方](https://qiita.com/yoshimo123/items/3717bd17ba74764dbc69)

```python

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 c in cols:
    lbl = LabelEncoder() 
    lbl.fit(list(all_data[c].values)) 
    all_data[c] = lbl.transform(list(all_data[c].values))

# shape        
print('Shape all_data: {}'.format(all_data.shape))
```

```text:出力
Shape all_data: (2917, 78)
```

###　補足1：新しい特徴の追加
全フロアの面積も重要であるため、TotalBsmtSFと1stSF,2ndFlrSFの合計値を新しい特陵に追加します！

```python
# Adding total sqfootage feature 
all_data['TotalSF'] = all_data['TotalBsmtSF'] + all_data['1stFlrSF'] + all_data['2ndFlrSF']
```

##3. データを正規分布へ

機械学習においては、**データが正規分布に従う方が精度がよくなる**と言われているらしいです！
なので、まず現在の**データの歪度（正規分布からどれだけ離れているか）を見て、Box Coxでデータを正規分布に従わせます！**

###1. まずはデータの歪度

```python
numeric_feats = all_data.dtypes[all_data.dtypes != "object"].index

# Check the skew of all numerical features
skewed_feats = all_data[numeric_feats].apply(lambda x: skew(x.dropna())).sort_values(ascending=False)
print("\nSkew in numerical features: \n")
skewness = pd.DataFrame({'Skew' :skewed_feats})
skewness.head(10)
```

<img width="179" alt="スクリーンショット 2020-03-18 14.57.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383306/df286780-d500-177b-bf06-9c56ae529725.png">

やはり、正規分布ではない特徴が複数見られますね！


###2. BoxCoxで正規分布へ変換！

```python
skewness = skewness[abs(skewness) > 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
for feat in skewed_features:
    #all_data[feat] += 1
    all_data[feat] = boxcox1p(all_data[feat], lam)
    
#all_data[skewed_features] = np.log1p(all_data[skewed_features])
```

## 4. 最後にカテゴリカルな特徴にダミー変数を追加！

ダミー変数を使うと、カテゴリカルデータを0,1で表すように変数がセットされます。
参考：[ダミー変数の作り方と注意点について](https://newtechnologylifestyle.net/%E3%83%80%E3%83%9F%E3%83%BC%E5%A4%89%E6%95%B0%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9%E3%81%A8%E6%B3%A8%E6%84%8F%E7%82%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/)

```python
all_data = pd.get_dummies(all_data)
print(all_data.shape)
```

```text:出力
(2917, 220)
```

#3. 特徴量エンジニアリング完了
これにて、データの前処理は完了しました！

合体させていた学習用とテスト用データを分割させましょう！

```python
train = all_data[:ntrain]
test = all_data[ntrain:]
```

#まとめ
今回はKaggleの住宅地の価格の予想のコンペの**データの前処理**にフォーカスしました！
80もの特徴があり、最初はかなり大変でしたが、以下の手順で進めることでしっかりと処理できたかなと思います！

データ前処理手順
**1. trainとtestデータの統合**
**2. 欠損値処理**
**3. カテゴリーデータ処理**

次の記事では、実際にモデルを用いて学習して予測していきます！！

ご視聴ありがとうございました！！