Pythonでのデータの前処理、使えそうな処理をとにかく記録することにした

###～データ前処理に対する抵抗感を払しょくしてやる！～

 
##はじめに
　
データの前処理までも自動でやってくれる**Pycaret**というライブラリもありますが、やはり、ある程度のデータの前処理はできないとダメだから・・・とにかく使えそうな処理を記録することにしました。
　
　
***
###実行条件など
>・**Google colab**で実行
・**Kaggleのタイタニックデータ**で実行
・※これ以外でも使えそうという処理はどんどん追加してゆきます
 
https://toukei-lab.com/google-colaboratory

###タイタニックのデータセットについて

以下サイト（Kaggle）の「train.csv」を使わせていただいた。

https://www.kaggle.com/c/titanic/data

<details><summary>**タイタニックデータセットの項目と内容**</summary><div>

>|項目|内容|
|:-----------|:------------------|
|PassengerId|乗客ユニークID|
|Survived|生存フラグ（0=死亡,1=生存）|
|Pclass|チケットクラス（1=上級,2=中級,3=下級）|
|Name|乗客名|
|Sex|性別（male=男性,female＝女性）|
|Age|年齢|
|SibSp|同乗している兄弟,配偶者の数|
|parch|同乗している親,子供の数|
|ticket|チケット番号|
|fare|料金|
|cabin|客室番号|
|Embarked|出港地（C=Cherbourg,Q=Queenstown,S=Southampton）|

</div></details>




##前処理してみよう！

###1. ライブラリのインストールおよびインポート
以前もある記事で描いたが、使用するライブラリをいちいち検討するのは面倒なので、基本以下としています。乱暴ですいません。

```:ライブラリのインストールおよびインポート
pip install japanize-matplotlib
```
```Python
# Import required libraries 
%matplotlib inline 
import pandas as pd 
import numpy as np 
import matplotlib.pyplot as plt 
import japanize_matplotlib 
import seaborn as sns 
sns.set(font="IPAexGothic")  
from scipy.stats import norm
```

##2. データの読み込み

```:csvデータの読込み
df=pd.read_csv('train.csv')
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/23e0e852-b6d2-f08a-2432-cf759b4269b6.png)


##3. 基礎データ確認
まず、データのカラム、欠損値有無、データ型を確認しました。

```Python
df.info()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/8e4d2821-86cc-11b3-f210-b262e73f7929.png)

各カラムの欠損数も確認しました。

```:各カラムの欠損数確認
df.isnull().sum()
```
Age、Cabin、Embarkedには欠損値があり、何らかの処置が必要なことがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/eb917bb4-70d8-95f9-5ece-b9f5699b00ca.png)


###その他の基礎データ確認

```:データ型の確認
df.dtypes
```

```:重複データの確認
df.duplicated().any()
```

```:基本統計量の確認（ただし数値データのみ）
df.describe()
```


```:画面表示できるデータを最大に
pd.set_option('display.max_rows', None)
df['列の名前'].value_counts()
```

##4. データ確認
欠損値があったCabinはどのようなデータなのかについて見てみました。

```Python
df['Cabin'].value_counts()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/67c3d265-63d8-667c-6050-884eefbd4b2a.png)

Cabinの中身（データ）はバラバラですね。
一応グラフ化もしてみましたが、これから特徴や傾向をつかむことはむつかいように思います。

```Python
sns.countplot(y="Cabin", data=df)
df.head()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/9de64092-58ce-b813-ad9d-ce147b009aa7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/b262b0e9-2f25-ca1a-6550-28db54e3985b.png)
↑ こちらはPandas-Profilingでのグラフです。こちらの方がバラバラ感がよくわかります。

```:散布図行列
pd.scatter_matrix(df)
```
###その他のデータ確認

```:データを文字列に変換→すべての列の統計量を表示
df.astype('str').describe()
```

```:条件抽出
df[(df[‘列名1’] == ‘A’ ) & (df[’列名2‘]>123)]
#列名1がA かつ 列名2が123以上の条件で抽出
```

```:クロス集計(質的データ×質的データ)
# 第1引数：縦列、第2引数：横列、[margins=True：合計値カラムの表示]
pd.crosstab(df['列名A'], df['列名B'])
```

```:クロス表×統計量比較
train_data =train[['Sex', 'Survived']].groupby(['Sex'])
train_data.describe()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/de11f848-6456-bf7b-e754-301ac9164c77.png)

```Python
#グラフでも確認
sns.countplot('Sex', data=train, hue='Survived')
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/4dbded5a-182a-b7f8-83e7-3e9abca19379.png)

***
###参考
ここではこれ以上データ確認については述べませんが、以下ライブラリが便利がと思います。

https://qiita.com/hima2b4/items/65129b3c3c4a1090afae

https://qiita.com/hima2b4/items/da29530c1f58c7211d44

##5. データ分析に関連しない列の削除
次に、カテゴリー化できないものや、データ分析に関連しないデータは列ごと削除しました。
ここでは、先ほどのCabinと、objectデータのName，Ticketは（データ分析には活かせそうにないので）列ごと削除することにしました。

```Python
# 特定列削除
df.drop("Cabin", axis = 1, inplace = True)
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/b308bde6-48b2-b4b2-91e8-7ac337ec10f7.png)

```Python
# 特定列削除
df.drop("Name", axis = 1, inplace = True)
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/05dc5329-ffd6-2d5a-6e04-84652196d81c.png)

```Python
# 特定列削除
df.drop("Name", axis = 1, inplace = True)
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/ad05b2a1-46cb-7240-6925-6a1e33087950.png)

ずいぶん、すっきりしました。
　
###その他の削除

```:数値の列は残し、文字列だけを削除
df = df.select_dtypes(exclude='object')
```

```:column名の変更
df_past = df_past.rename(columns={'Pclass': 'Class'})
```
　
##6. 欠損値の処理
再度、データのカラム、欠損値有無、データ型を確認しました。
項目数は11→8となり、すこしスッキリしました。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/b3ecf798-3988-206a-5a6e-41ce3a1a79e0.png)

次は、AgeとEmbarkedの欠損値処理を行うことにします。

まずはAgeです。ヒストグラムでこのデータの傾向をみてみます。一山の分布となっています。ピークはすこし左よりの分布となっているように思います。

```Python
sns.distplot( 
    df['Age'],bins=10, color='#123456',label='data', 
    kde_kws={'label': 'kde','color':'k'}, 
    fit=norm,fit_kws={'label': 'norm','color':'red'}, 
    rug=False 
) 
plt.legend() 
plt.grid() 
plt.show()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/2cc391eb-d8cc-ff1b-edcd-e06a0572da31.png)

左よりの分布を考慮し、欠損値には「中央値」を補完しました。

```Python
df['Age'].fillna(df['Age'].median(), inplace=True)
```

再度、データのカラム、欠損値有無、データ型を確認してみます。 Ageの欠損値はなくなりました。

```Python
df.info()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/5521f868-2056-0514-e5e0-51a04a7d0b26.png)


次は、Embarkedです。まず傾向をグラフで見てみました。これは欠損値以外が表示されたものですが、S,C,Q の3パターンのでデータであることがわかります。

```Python
sns.countplot(y="Embarked", data=df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/6f1af62c-a538-64b9-d70c-ce3f67eb7249.png)

Embarkedの欠損値処理は、もっとも多い「S」に補完しました。以下infoの通り、補完によって欠損値がなくなったことがわかります。

```Python
df["Embarked"] = df["Embarked"].fillna("S")
df.info()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/e5447441-f828-b4fe-4547-c73db5f3c01c.png)

https://3pysci.com/python-sklearn-16/


###他の欠損値処理について
実務では、中途半端に欠損値を補ったりせず、欠損データは削除することも多いでしょう。以下はそのような場合のコードです。

```:欠損値のある行を削除（※how='any'は欠損がひとつでもあれば、その行を削除）
df = df.dropna(how='any')
 
#別の方法
df.dropna(subset=[‘列名‘], axis=0, inplace=True) #axis=0(デフォルト)で行、inplace=Trueでdf変更反映
```

```:[‘-‘]を本来の欠損値(nan)に変換
df = df[‘列名‘].replace(‘-‘,np.nan)
```

```:すべての欠損値を0で置換
df.fillna(0)
```

```:すべての値が欠損している列を削除
nonnull_list = []
for col in df.columns:
    nonnull = df[col].count()
    if nonnull = 0:
        nonnull_list.append(col)
df.drop(nonnull_list, axis=1)
```

```:ある列を削除する
df.drop('列名', axis=1)
```

```:欠損値を平均値や最頻値等で穴埋め（列毎の値を算出して穴埋め）
df.fillna(df.mean()) # 平均値で補完
```

```:特定列の欠損値を中央値で穴埋め
df['Age'].fillna(df['Age'].median(), inplace=True)  # 中央値で補完
```

```:特定列の欠損値をある値•文字で穴埋め
df['Embarked'].fillna('S', inplace=True)         # 'S'で補完
```

##7. ラベルエンコーディング（カテゴリーデータの数値変換）
SexとEmbarkedはカテゴリーデータなので、ラベルエンコーディングを行いました。まずはSexです。以下のコードの実行でmale=0に、female=1となりました。

```:LabelEncoder
from sklearn.preprocessing import LabelEncoder
 
le = LabelEncoder()
le.fit(df['Sex'])
df['Sex'] = le.transform(df['Sex'])
 
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/1bdfdef2-25aa-cfd6-b5e0-f3563f24d22a.png)

つぎはEmbarkedです。以下のコードの実行でEmbarkedも0,1,2にエンコーディングできました。

```:LabelEncoder
from sklearn.preprocessing import LabelEncoder
 
le = LabelEncoder()
le.fit(df['Embarked'])
df['Embarked'] = le.transform(df['Embarked'])
 
df.head()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/bde51b36-7a93-15ff-1633-a992673764ab.png)

###その他のエンコーディング

```:データに混在する文字列（文字データA）を任意の値（10）に変換
df['列の名前'] = df['列の名前'].replace('文字データA', 10). astype(float)
df['列の名前'].value_counts()
```

```:データに混在する文字データ⁽A,B,C)を任意の値(45,75,120)に変換
dic = {
    '文字データA': 45
    '文字データB': 75
    '文字データC': 120}
df['列の名前'] = df['列の名前'].replace(dic). astype(float)
df['列の名前'].value_counts()
```

```:[平成20年]から数値（20）だけを取り出す
df["和暦"].value_counts().keys() [1].split("平成")[1].split("年")[0)
#20
```

```:和暦から年を取り出して西暦に換算
y_list = {}
for 1 in df["和暦"].value_counts().keys():
    if "平成" in i:
        num = float(i.split("平成")[1].split("年")[0])
        year = 33 - num
for 1 in df["和暦"].value_counts().keys():
    if "令和" in i:
        num = float(i.split("令和")[1].split("年")[0])
        year = 3 - num
for 1 in df["和暦"].value_counts().keys():
    if "昭和" in i:
        num = float(i.split("昭和")[1].split("年")[0])
        year = 96 - num
y_list(i) = year
df("和暦") = df("和暦").replace(y_list)
```


```:2020年第1四半期→2020.25に変換
"2020年第1四半期".replace("年第1四半期",  ".25 ") #第一引数に置換元文字列、第二引数に置換先文字列を指定
```

```:2020年第n四半期→2020.25,2020.50,2020.75,2020.99に
year = {
    "年第1四半期": ".25 ",
    "年第2四半期": ".50",
    "年第3四半期": ".75",
    "年第4四半期": ".99"
}

#year = {k,j} 
year_list = {}
for i in df[”取引時点"].value_counts().keys():
    for k, j in year. items():
        if k in i:
            year_rep = i.replace(k, j)
    year_list [i] = year_rep
df[”取引時点"] = df[”取引時点"].replace(year_list).astype(float)
df.info()
```

```:カウントエンコーディング
import collections

counter = collections.Counter(df['Ticket'].values)
count_dict = dict(counter.most_common())
encoded = df['Ticket'].map(lambda x: count_dict[x]).values
df['TicketID'] = encoded

print('エンコード結果: ', encoded)
```

```:dtypeを変更
df['Age']=df['Age'].astype(int)
df.head()
```

```Python
# 'size'を'XL','L','M' → 整数(1, 2, 3)に変換
size_mapping = {'XL': 3, 'L': 2, 'M': 1}  
df['size'] = df['size'].map(size_mapping)
df.head()
```

```:category_encoders
pip install category_encoders

#Ordinal_Encode
import category_encoders as ce

# Eoncodeしたい列をリストで指定。もちろん複数指定可能。
list_cols = ['列A','列B','列C','列D']

# 序数をカテゴリに付与して変換
ce_oe = ce.OrdinalEncoder(cols=list_cols,handle_unknown='impute')
df_ce_ordinal = ce_oe.fit_transform(df)

df_ce_ordinal.head()
```

##8. 新たな特徴量の生成
SibSpは、兄弟,配偶者の数。 Parchは、両親,子供の数 です。この2つの説明変数はいわば家族の数です。またこの2つ相関も高く、このまま分析に使うのはよくありませんので、足し合わせて新しい変数にしました。（これは「Kaggleスタートブック」に紹介されていた内容です）

```Python
df['FamilySize'] = df['Parch'] + df['SibSp'] + 1
sns.countplot(y='FamilySize', data = df, hue='Survived')

df.head()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/40802a2f-a6e7-fda5-1ec0-1146bdc38d28.png)

これも「Kaggleスタートブック」に紹介されていた内容ですが、FamilySizeが1という方が大部分です。またグラフの通り、FamilySizeが1の方は生存率が低い傾向もみられ案す。このことからFamilySizeが1であるか否かを特徴として取り上げてもよさそうです、

```Python
df['IsAlone'] = 0
df.loc[df['FamilySize'] == 1, 'IsAlone'] = 1
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/3c02efa7-365e-abd8-dd53-123a8cd040c9.png)

最後に、FamilySizeに統合したSibSpとParchのデータは列ごと削除しました。

```:SipSp列の削除
df.drop("SibSp", axis = 1, inplace = True)
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/17b93bf7-12cc-d0f0-7ed2-bc74a262f23b.png)

```:Parch列の削除
df.drop("Parch", axis = 1, inplace = True)
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/240dc46c-fe62-2939-f5aa-16a0181a35d5.png)

```Python
df.info()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/772be669-cf06-dd5a-1918-2dfe8837941d.png)

```:欠損値合計の確認
df.isnull().sum()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/d60fb899-73e9-984e-da96-275d56b954aa.png)

これで欠損値もなくなり、前処理は無事に終了しました。
ここでは「PassengerId」は残していますが、これも削除して問題ないと思います。


###参考：**Name**から敬称を抽出し特徴量に
```Python
#Nameの確認
df = pd.read_csv('train.csv')
df['Name'].head()
```
>0                              Braund, Mr. Owen Harris
1    Cumings, Mrs. John Bradley (Florence Briggs Th...
2                               Heikkinen, Miss. Laina
3         Futrelle, Mrs. Jacques Heath (Lily May Peel)
4                             Allen, Mr. William Henry
Name: Name, dtype: object

```Python
#Nameを[family‗name,honorific.name]に区分（引数2はなくても実行されたがNone列含む4分割となった）
name_df = df["Name"].str.split("[,.]",2, expand=True)

#columnを設定
name_df.columns = ["family_name", "honorific", "name"]
name_df.head()
```
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/90ee3b4f-cd00-7db0-01d0-07db170172d0.png)

```Python
#各列にstrip()を用いることで先頭と末尾の空白文字が削除される（念のため各列に適用）
name_df["family_name"] = name_df["family_name"].str.strip()
name_df["honorific"] = name_df["honorific"].str.strip()
name_df["name"] = name_df["name"].str.strip()
name_df.head()
```

```Python
#dfにname_dfを列結合
df = pd.concat([df, name_df], axis=1)
```

```Python
#グラフで確認する
sns.boxplot(x="honorific", y="Age", data=df)
```
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/3bcae100-4192-052a-fba1-7816c147b630.png)

```:
#Mr、Miss、Mrs、Master以外の敬称はotherとして統合
df.loc[~((df ["honorific"] =="Mr")|(df [ "honorific"] =="Miss")|(df[ "honorific"] =="Mrs")|(df [ "honorific"]  =="Master")),
"honorific"] = "other"
df.head()
```

```:
pip install scikit-learn
```

```:
from sklearn.preprocessing import LabelEncoder

#honorificをラベルエンコード
le = LabelEncoder()
le = le.fit(df ["honorific"])
df["honorific"] = le.transform(df["honorific"])
df.head()
```

```:ラベルエンコード後のグラフ化
sns.boxplot(x="honorific", y="Age", data=df)
```
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/0200d9ec-6d8d-966f-395f-730a83284710.png)

私は実行していないが、年齢の欠損値を、敬称ごとの平均年齢で補完されている例があった。
敬称ごとの平均年齢を求め、年齢が欠損しているデータに敬称の平均年齢を補完。
※補完後、敬称ごとの平均年齢を求めたhonorific_Age は不要のため削除。

```Python
#敬称ごとの平均年齢で年齢が欠損しているデータを穴埋め
honorific_age_mean = df [["honorific", "Age"]].groupby("honorific").mean().reset_index()
honorific_age_mean.columns = ["honorific", "honorific_Age" ]
 
df = pd. merge (df, honorific_age_nean,on="honorific", how="left")
df.loc [(df ["Age"].isnull()), "Age"] = df["honorific_Age"]
df = df.drop([ "honorific_Age"), axis=1)
```

##その他の便利なコード記述
説明変数と目的変数の設定を一発で

```:df右端のcolumnを目的変数、他のcolumnsを説明変数に設定
FEATURES = df.columns[:-1]
TARGET = df.columns[-1]
```

```:上記設定の上でX,yを以下の設定すれば個別設定の必要なし
X = df.loc[:, FEATURES]
y = df.loc[:, TARGET]
```

```:[列A]を先頭列に移動
first_column = df.pop('列A')

df.insert(0,'列A',first_column)

df
```

##最後に
前処理はとても億劫であったが、やってみたら・・・まぁやれなくないなという感じです。　
これで完璧！といえるかどうかは別として、このあたりまで処理した後、Pycaret活用・・・といった流れの方が現実的なのかもしれません。
　


###参考サイト

https://note.nkmk.me/python-pandas-nan-dropna-fillna/

https://data-bunseki.com/2019/06/22/pandas-dataframe-%E3%83%80%E3%83%9F%E3%83%BC%E5%A4%89%E6%95%B0get_gummies/

https://qiita.com/squash/items/667f8cda16c76448b0f4

https://yolo-kiyoshi.com/2018/12/23/post-1016/

https://lp-tech.net/articles/0QUUd

###参考書籍

https://www.amazon.co.jp/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E7%89%B9%E5%BE%B4%E9%87%8F%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%83%AA%E3%83%B3%E3%82%B0-%E2%80%95%E3%81%9D%E3%81%AE%E5%8E%9F%E7%90%86%E3%81%A8Python%E3%81%AB%E3%82%88%E3%82%8B%E5%AE%9F%E8%B7%B5-%E3%82%AA%E3%83%A9%E3%82%A4%E3%83%AA%E3%83%BC%E3%83%BB%E3%82%B8%E3%83%A3%E3%83%91%E3%83%B3-Alice-Zheng/dp/4873118689

https://www.amazon.co.jp/dp/B088R992TJ/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1