データ分析における特徴量エンジニアリングの実践

# はじめに
以前の記事で回帰分析を行ったので、次は単純ベイズ（Naive Bayes）をPythonで実装してみようと思ったところ、Kaggleでちょうどいい銀行の顧客データセット [^1] を見つけました。説明文にもある通り、データセットには、顧客が解約したかどうかを示すブール値を持っています。これは、顧客が解約する確率を予測するモデルを開発するためのターゲット変数として機能します。よって、このデータに単純ベイズを用いることで、銀行の顧客が解約するかどうかの予測をしようと思います。

## その前段階として
後で具体的にデータを見ていくと分かりますが、この銀行の顧客データセットをそのまま用いることは出来ません。ある程度のデータ加工、専門用語で言うところの特徴量エンジニアリングが必要になります。特徴量エンジニアリングとはなんぞや？という事を述べたいのですが、それだけで記事の一本は書けるし、他のサイトに色々優れた記事があるのでそちらを参照されたい。この記事ではデータ分析における特徴量エンジニアリングを実際に行ってみる、ということに主眼を当てて書こうと思います。

# データの読み込みと確認
```python
# パッケージのインポート
import numpy as np
import pandas as pd

# データの読み込み
df_original = pd.read_csv("Churn_Modelling.csv")

# データの確認
pd.set_option('display.expand_frame_repr', False)
pd.set_option('display.max_columns', 23)
print(df_original.head())
```
```markdown:出力例
   RowNumber  CustomerId   Surname  CreditScore Geography  Gender  Age  Tenure    Balance  NumOfProducts  HasCrCard  IsActiveMember  EstimatedSalary  Exited
0          1    15634602  Hargrave          619    France  Female   42       2       0.00              1          1               1        101348.88       1
1          2    15647311      Hill          608     Spain  Female   41       1   83807.86              1          0               1        112542.58       0
2          3    15619304      Onio          502    France  Female   42       8  159660.80              3          1               0        113931.57       1
3          4    15701354      Boni          699    France  Female   39       1       0.00              2          0               0         93826.63       0
4          5    15737888  Mitchell          850     Spain  Female   43       2  125510.82              1          1               1         79084.10       0

```
データセットを確認すると "Exited" という列があることを確認できます。これは、顧客が銀行を離れたかどうかを示す真偽値（ブール値）です (0 = 解約してない、1 = 解約した)。これがターゲット変数になります。つまり、単純ベイズを用いたモデルで、各顧客について "Exited" 列に 0 または 1 のどちらが入るかを予測します。

これは、バイナリクラスで予測するため、教師あり学習分類タスクになります。よって、それに向けてのデータを準備していきます。

次にデータの概要を確認します。
```python
# データの概要の確認
print(df_original.info())
```
```markdown:出力例
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 14 columns):
 #   Column           Non-Null Count  Dtype  
---  ------           --------------  -----  
 0   RowNumber        10000 non-null  int64  
 1   CustomerId       10000 non-null  int64  
 2   Surname          10000 non-null  object 
 3   CreditScore      10000 non-null  int64  
 4   Geography        10000 non-null  object 
 5   Gender           10000 non-null  object 
 6   Age              10000 non-null  int64  
 7   Tenure           10000 non-null  int64  
 8   Balance          10000 non-null  float64
 9   NumOfProducts    10000 non-null  int64  
 10  HasCrCard        10000 non-null  int64  
 11  IsActiveMember   10000 non-null  int64  
 12  EstimatedSalary  10000 non-null  float64
 13  Exited           10000 non-null  int64  
dtypes: float64(2), int64(9), object(3)
memory usage: 1.1+ MB
```
null値は無さそうなので、欠損値の処理は必要なさそうです。

# 特徴量エンジニアリング
## 特徴量選択
特徴量選択とは、学習モデルの構築のため、特徴集合のうち意味のある部分集合だけを選択する手法のことです。

例えば、データセットを確認すると 1列目の "RowNumber" は単なる行番号であり、顧客が解約したかどうかを予測するには不要な列であることが分かります。もちろん、古いデータほど若い番号に存在していることもあり、相関関係を持っていることを否定することはできません。しかし、 "Tenure" の列、つまり保有期間の列があるので、こちらを利用する方が良さそうです。また、管理目的で顧客ごとに割り当てられた顧客番号 "CustomerID" と、顧客の姓である "Surname" についても同様です。これらも予測に際してはさしたる影響を及ぼすと（一般的には）考えられないため、データセットから削除できます。

という事で、解約を予測するに際して不必要なデータ列の削除しておきます。
```python
# RowNumber, CustomerId, Surname の列を削除して、新たなデータフレームを作成
churn_df = df_original.drop(['RowNumber', 'CustomerId', 'Surname'],  axis=1)

print(churn_df.head())
```
```markdown:出力例
   CreditScore Geography  Gender  Age  Tenure    Balance  NumOfProducts  HasCrCard  IsActiveMember  EstimatedSalary  Exited
0          619    France  Female   42       2       0.00              1          1               1        101348.88       1
1          608     Spain  Female   41       1   83807.86              1          0               1        112542.58       0
2          502    France  Female   42       8  159660.80              3          1               0        113931.57       1
3          699    France  Female   39       1       0.00              2          0               0         93826.63       0
4          850     Spain  Female   43       2  125510.82              1          1               1         79084.10       0
```

## 特徴量抽出
特徴量抽出とは、数学的な変換を用いて、元のデータを最適な新しい特徴量にマッピングする手法です。

このデータセットを見ると "Age" の列と "Tenure" の列、つまり年齢の列と保有期間の列と年月に関わる列が二つある事が分かります。10歳の1年と、60歳の1年とでは、1年に対する重みが違うことは容易に想像できるかと思います。そこで、顧客の人生の占める割合が長い場合は解約されにくい、という推察の元、
```math
Loyalty = \frac{Tenure}{Age}
```
として、各顧客の生涯のうち顧客であった期間の割合を表す新しいカラムを作成します。

### 従属変数を削除する
単純ベイズのモデルでは、予測変数間が条件付き独立である場合に最もよく機能します。"Loyalty" 列は "Tenture", "Age" の列とは前述の式より条件付き独立ではありません。したがって、モデルの仮定に従うために、"Tenture" 保有期間と "Age" 年齢の列を削除します。この特徴抽出を行う事で次元削減を行えます。
```python
# Loyalty（忠誠心）列の追加
churn_df['Loyalty'] = churn_df['Tenure'] / churn_df['Age']

# Tenure（保有期間）とAge（年齢）の列を落として次元削減をする
churn_df = churn_df.drop(['Tenure', 'Age'], axis=1)
print(churn_df.head())
```
```markdown:出力例
   CreditScore Geography  Gender    Balance  NumOfProducts  HasCrCard  IsActiveMember  EstimatedSalary  Exited   Loyalty
0          619    France  Female       0.00              1          1               1        101348.88       1  0.047619
1          608     Spain  Female   83807.86              1          0               1        112542.58       0  0.024390
2          502    France  Female  159660.80              3          1               0        113931.57       1  0.190476
3          699    France  Female       0.00              2          0               0         93826.63       0  0.025641
4          850     Spain  Female  125510.82              1          1               1         79084.10       0  0.046512
```

## 特徴量変換
特徴量変換とは、ある種類の特徴量を、特定のモデルにとっての可読性がより高い、別の形式に変換する手法の事です。例えば、カテゴリ変数のエンコードすることなどがあります。この時点でデータセットには、カテゴリ変数が2種類あります。一つは性別でもう一つは地理です。

### 性別カラム
まずは "Gender" 性別について見ていくことにします。このカラムにはどれだけの種類のカテゴリ値があるかを確認します。
```python
print(churn_df['Gender'].unique())
```
```markdown:出力例
['Female' 'Male']
```
カテゴリ値としては Female と Male しかないので、`pandas.get_dummies` 関数を利用して0, 1のブール値に変換してしまいましょう。

```python
# ダミー変数に変換されたデータフレームを受け取っておく
churn_gender_df = pd.get_dummies(churn_df['Gender'], drop_first=True)

# 元のデータフレームと結合
churn_df = pd.concat([churn_df, churn_gender_df], axis=1)

# カラム名を（好みで）変更しておく
churn_df = churn_df.rename(columns={'Male': 'Sex'})

# 元のカラムを落としておく
churn_df = churn_df.drop(['Gender'],  axis=1)
```
#### 補足
- 列指定をしない方がまとめて変換してくれるので楽なんですが、私個人としては列指定をする方が丁寧で好きなのと、意図しない変換を避ける為にそうしています。
- 今回は男性か女性かしか性別のカラムには存在しなかったことにより、男性でない場合は女性である、という事が成り立ちます。よって `drop_first = True` とすることで、冗長なカラムを生成するのを避けます。

### 地域
続いてGeography（地域）のデータを同様に確認します。
```python
print(churn_df['Geography'].unique())
```
```markdown:出力例
['France' 'Spain' 'Germany']
```
こちらも3種類のカテゴリ値しか存在しないので、`pandas.get_dummies` 関数を利用して変換してしまいましょう。
```python
# ダミー変数に変換されたデータフレームを受け取っておく
churn_geo_df = pd.get_dummies(churn_df['Geography'], prefix='Geography', drop_first=True)

# 元のデータフレームと結合
churn_df = pd.concat([churn_df, churn_geo_df], axis=1)

# 元のカラムを落としておく
churn_df = churn_df.drop(['Geography'],  axis=1)

# 結果を確認
print(churn_df.head())
```
```markdown:出力例
   CreditScore    Balance  NumOfProducts  HasCrCard  IsActiveMember  EstimatedSalary  Exited   Loyalty  Sex  Geography_Germany  Geography_Spain
0          619       0.00              1          1               1        101348.88       1  0.047619    0                  0                0
1          608   83807.86              1          0               1        112542.58       0  0.024390    0                  0                1
2          502  159660.80              3          1               0        113931.57       1  0.190476    0                  0                0
3          699       0.00              2          0               0         93826.63       0  0.025641    0                  0                0
4          850  125510.82              1          1               1         79084.10       0  0.046512    0                  0                1
```
#### 補足
- Geographyもフランス、ドイツ、スペインの３か国しかありません。よってドイツでもスペインでもない場合は自動的にフランスである事が分かります。よって `drop_first = True` とすることで、冗長なカラムを生成するのを避けます。
- `prefix` オプションを使用することで、接頭語を付与することが出来ます。使用しないと Germany, Spain という一見すると分からない列名になってしまいます。

## 特徴量スケーリング
作成した Loyalty もそうですが、NumOfProductsと、Balance や EstimatedSalary などの他の変数とはまったく異なるスケールになっています。NumOfProducts の最大値は 4 ですが、Balance の最大値は 250898.09, EstimatedSalary の最大値は 199992.48 となっており、桁数が大きく異なります。

よって、`sklearn.preprocessing` モジュールから `MinMaxScaler` 関数を使用して、各列を正規化します。これで、各列の値が [0, 1] の範囲に収まります。つまり、列の最大値は 1 にスケーリングされ、最小値は 0 にスケーリングされます。その間の値は次式でスケーリングされます。
```math
x_{𝑠𝑐𝑎𝑙𝑒𝑑} = \frac{x − 𝑥_{𝑚𝑖𝑛}}{𝑥_{𝑚𝑎𝑥}−𝑥_{𝑚𝑖𝑛}}
```
それではスケーリングしていきます。
```python
# MinMaxScaler関数のインポート
from sklearn.preprocessing import MinMaxScaler

# インスタンス化
scaler = MinMaxScaler()

# スケーラーにデータを学習させる。
scaler.fit(churn_df)

# データをスケーリングさせる。
# スケーラーの戻り型がnumpy.ndarrayなので、pandas.core.frame.DataFrameにする。
churn_df[['CreditScore','Balance','NumOfProducts','HasCrCard','IsActiveMember','EstimatedSalary','Exited','Loyalty','Sex','Geography_Germany','Geography_Spain']] \
 = scaler.transform(churn_df)
 print(churn_df.head())
```
```markdown:出力例
   CreditScore   Balance  NumOfProducts  HasCrCard  IsActiveMember  EstimatedSalary  Exited   Loyalty  Sex  Geography_Germany  Geography_Spain
0        0.538  0.000000       0.000000        1.0             1.0         0.506735     1.0  0.085714  0.0                0.0              0.0
1        0.516  0.334031       0.000000        0.0             1.0         0.562709     0.0  0.043902  0.0                0.0              1.0
2        0.304  0.636357       0.666667        1.0             0.0         0.569654     1.0  0.342857  0.0                0.0              0.0
3        0.698  0.000000       0.333333        0.0             0.0         0.469120     0.0  0.046154  0.0                0.0              0.0
4        1.000  0.500246       0.000000        1.0             1.0         0.395400     0.0  0.083721  0.0                0.0              1.0
```
無事にスケーリングされました。
# CSV出力
最後に後続のモデルの構築用にCSVファイルとして出力して保存しておきます。保存する際には`index = False`を指定してインデックス列が作成されないようにしておきます。
```python
churn_df.to_csv('Churn_Modelling2.csv', index = False)
```

以上。
[^1]: churn_modelling https://www.kaggle.com/datasets/jssunmathi/churn-modelling