はじめに
以前の記事で回帰分析を行ったので、次は単純ベイズ(Naive Bayes)をPythonで実装してみようと思ったところ、Kaggleでちょうどいい銀行の顧客データセット 1 を見つけました。説明文にもある通り、データセットには、顧客が解約したかどうかを示すブール値を持っています。これは、顧客が解約する確率を予測するモデルを開発するためのターゲット変数として機能します。よって、このデータに単純ベイズを用いることで、銀行の顧客が解約するかどうかの予測をしようと思います。
その前段階として
後で具体的にデータを見ていくと分かりますが、この銀行の顧客データセットをそのまま用いることは出来ません。ある程度のデータ加工、専門用語で言うところの特徴量エンジニアリングが必要になります。特徴量エンジニアリングとはなんぞや?という事を述べたいのですが、それだけで記事の一本は書けるし、他のサイトに色々優れた記事があるのでそちらを参照されたい。この記事ではデータ分析における特徴量エンジニアリングを実際に行ってみる、ということに主眼を当てて書こうと思います。
データの読み込みと確認
# パッケージのインポート
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())
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 のどちらが入るかを予測します。
これは、バイナリクラスで予測するため、教師あり学習分類タスクになります。よって、それに向けてのデータを準備していきます。
次にデータの概要を確認します。
# データの概要の確認
print(df_original.info())
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" についても同様です。これらも予測に際してはさしたる影響を及ぼすと(一般的には)考えられないため、データセットから削除できます。
という事で、解約を予測するに際して不必要なデータ列の削除しておきます。
# RowNumber, CustomerId, Surname の列を削除して、新たなデータフレームを作成
churn_df = df_original.drop(['RowNumber', 'CustomerId', 'Surname'], axis=1)
print(churn_df.head())
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年に対する重みが違うことは容易に想像できるかと思います。そこで、顧客の人生の占める割合が長い場合は解約されにくい、という推察の元、
Loyalty = \frac{Tenure}{Age}
として、各顧客の生涯のうち顧客であった期間の割合を表す新しいカラムを作成します。
従属変数を削除する
単純ベイズのモデルでは、予測変数間が条件付き独立である場合に最もよく機能します。"Loyalty" 列は "Tenture", "Age" の列とは前述の式より条件付き独立ではありません。したがって、モデルの仮定に従うために、"Tenture" 保有期間と "Age" 年齢の列を削除します。この特徴抽出を行う事で次元削減を行えます。
# 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())
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" 性別について見ていくことにします。このカラムにはどれだけの種類のカテゴリ値があるかを確認します。
print(churn_df['Gender'].unique())
['Female' 'Male']
カテゴリ値としては Female と Male しかないので、pandas.get_dummies
関数を利用して0, 1のブール値に変換してしまいましょう。
# ダミー変数に変換されたデータフレームを受け取っておく
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(地域)のデータを同様に確認します。
print(churn_df['Geography'].unique())
['France' 'Spain' 'Germany']
こちらも3種類のカテゴリ値しか存在しないので、pandas.get_dummies
関数を利用して変換してしまいましょう。
# ダミー変数に変換されたデータフレームを受け取っておく
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())
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もフランス、ドイツ、スペインの3か国しかありません。よってドイツでもスペインでもない場合は自動的にフランスである事が分かります。よって
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 にスケーリングされます。その間の値は次式でスケーリングされます。
x_{𝑠𝑐𝑎𝑙𝑒𝑑} = \frac{x − 𝑥_{𝑚𝑖𝑛}}{𝑥_{𝑚𝑎𝑥}−𝑥_{𝑚𝑖𝑛}}
それではスケーリングしていきます。
# 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())
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
を指定してインデックス列が作成されないようにしておきます。
churn_df.to_csv('Churn_Modelling2.csv', index = False)
以上。
-
churn_modelling https://www.kaggle.com/datasets/jssunmathi/churn-modelling ↩