目次
はじめに
データ表示と可視化
ベースラインモデルの学習と評価
探索的データ分析(EDA)
特徴量エンジニアリング
学習モデルの比較
ハイパーパラメータチューニング
予測値の出力
まとめ
はじめに
初めまして、私はあるSaasサービスを展開する企業でクライアントのマーケティング課題を解決するカスタマーサクセスを行いながら、SaaSサービスのログデータを利用したデータ分析を行なっています。
クライアントに対して活用提案やレポート提示を行う中で、データをより有効的に使いたいと思いデータ分析、機械学習を学び始めました。
今回の分析データについて
今回の分析はkaggleという機械学習のコンペサイトにある、
クレジットカードの解約者予測を行うデータセットです。
実行環境はkaggle内のnotebookを利用して分析を行います。
データ表示と可視化
各カラムの内容
利用データの詳細が多く格納されている
カラム名 | 内容 | データ型 |
---|---|---|
CLIENTNUM | 顧客ID | int |
Attrition_Flag | 顧客が離脱したかを示すフラグ | bool |
Customer_Age | 年齢 | int |
Gender | 性別 | str |
Dependent_count | 扶養家族の人数 | int |
Education_Level | 教育レベル | str |
Marital_Status | 婚姻状況 | str |
Income_Category | 収入カテゴリ | str |
Card_Category | 保有するカードの種類 | str |
Months_on_book | サービス利用し続けている期間の長さ | int |
Total_Relationship_Count | 複数サービス利用合計数 | int |
Months_Inactive_12_mon | 過去12か月間に顧客が非アクティブだった月数 | int |
Contacts_Count_12_mon | 12か月間にカスタマーサービスに連絡した回数 | int |
Credit_Limit | 顧客の与信限度 | int |
Total_Revolving_Bal | リボ払いによって滞納している残高の合計額 | int |
Avg_Open_To_Buy | クレジット限度額までの利用可能残高平均 | int |
Total_Amt_Chng_Q4_Q1 | 第4四半期から第1四半期のカード利用金額の変化 | int |
Total_Trans_Amt | 総取引金額 | int |
Total_Trans_Ct | 合計取引回数 | int |
Total_Ct_Chng_Q4_Q1 | 第4四半期から第1四半期のカード利用頻度の変化 | int |
Avg_Utilization_Ratio | クレジット限度額に対する平均残高比率 | int |
Naive_Bayes_Classifier_Attrition_Flag Card_Category_Contacts_Count 12_mon_Dependent_count_Education_ Level_Months_Inactive_12_mon_1 |
誰かが解約するかどうかを予測するためのNaive Bayes分類器 | int |
データの確認
はじめにデータをImportして表示します。
# data import
data = pd.read_csv('/kaggle/input/predicting-credit-card-customer-attrition-with-m/BankChurners.csv')
data.head()
欠損値が無いか確認を行います。
data_details = {
'unique': data.nunique(),
'dtype': data.dtypes,
'null': data.isna().sum(),
'null%': data.isna().sum()/len(data)
}
data_details = pd.DataFrame(data_details)
data_details
カテゴリカルデータの統計量表示を行います。
display(data.describe(exclude='number'))
データの特徴を表示
データ全体の特性を確認した後はカラムごとのデータを確認します。
下記のコードでデータの特徴をグラフとして可視化を行います。
import matplotlib.pyplot as plt
import seaborn as sns
cat_col = data.select_dtypes(include='object').columns
for i, column in enumerate(cat_col):
counts = data[column].value_counts()
plt.figure(figsize=(8,4))
plt.bar(counts.index, counts.values,width=0.5)
plt.xlabel(column)
plt.ylabel('Count')
plt.title('{} Distribution'.format(column))
利用顧客と解約顧客の人数
全体の解約率は10〜20%程度と推測できる
就学レベルの分布
Graduate(大学院卒)の比率が多い
婚姻状況
既婚者の割合が比較的多い
収入カテゴリ
収入は少ない人ほど割合が多い
カードの種類分布
カードの種類はほとんどがブルーカード
ベースモデルの学習と正解率の確認
上記のデータからベースとするモデルでの学習を行い、ベースとなる予測精度を割り出します。
今回はロジスティック回帰で特徴量を学習し、正解率を算出します。
まずは目的変数と学習しないカラムを削除して学習データを準備します。
# 目的変数
target = data['Attrition_Flag']
# One-hot-encodingによりカテゴリ変数の変換
dummies_col = [
'Gender',
'Education_Level',
'Marital_Status',
'Income_Category',
'Card_Category',
]
data = pd.get_dummies(data, columns= dummies_col)
# 学習に利用しないカラムを削除する
drop_col = [
'CLIENTNUM',
'Attrition_Flag',
'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2',
]
data = data.drop(drop_col, axis=1) # inplace = Trueを指定すると元のデータが入れ替わる
# カラムを確認しやすくするために転置して出力
data.head().T
正しく学習できるように説明変数を標準化を行います。
# 標準化するカラムを指定
scaling_columns = [
'Customer_Age',
'Months_on_book',
'Credit_Limit',
'Total_Revolving_Bal',
'Avg_Open_To_Buy',
'Total_Trans_Amt',
'Total_Trans_Ct'
]
# データの標準化
for column in scaling_columns:
data[column] = (data[column] - data[column].mean())/data[column].std()
# カラムを確認しやすくするために転置して出力
data.head().T
次に正解率を表示するための学習、検証データに分割します。
from sklearn.model_selection import train_test_split
# trainデータの一部を分割しvalデータを作成
X_train, X_val, y_train, y_val = train_test_split(
data, target,
test_size=0.3, shuffle=True, random_state=0
)
ロジスティック回帰での学習と評価
学習、検証用データを準備できたら学習と精度の出力を行います。
# ロジスティック回帰のimport
from sklearn.linear_model import LogisticRegression
# 正解率を算出するaccuracy_scoreのimport
from sklearn.metrics import accuracy_score
# modelの定義と学習
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
# trainデータの予測と正答率の算出
y_pred = model.predict(X_train)
print('train_acc:',accuracy_score(y_train,y_pred))
# valデータの正解率の算出
y_pred_val = model.predict(X_val)
val_acc = accuracy_score(y_val, y_pred_val)
print('val_acc:', val_acc)
>>>
train_acc: 0.9067437923250564
val_acc: 0.9039157617637381
元の標準データでの学習を行うと約90%程度の正解率で獲得できることが分かりました。
この基準数値を元に、他の手法で学習精度を高められるか検証を行います。
探索的データ分析(EDA)
探索的データ分析(EDA)ではデータに含まれる特徴を探し出します。
利用顧客と解約顧客ごとにそれぞれ分けて、データの可視化を行います。
# 可視化するカラム
columns = [
'Customer_Age',# 年齢
'Dependent_count',# 扶養家族の人数
'Education_Level',# 就学レベル
'Total_Ct_Chng_Q4_Q1',# 第4四半期から第1四半期のカード利用金額の変化
'Months_Inactive_12_mon',# 12ヶ月の非アクティブ期間
'Credit_Limit',# クレジット限度額
'Total_Revolving_Bal',# リボ払いによって滞納している残高の合計額
'Avg_Utilization_Ratio',# クレジット限度額に対する平均残高比率
'Total_Trans_Ct' # 合計取引回数
]
bins = [30, 6, 7, 30, 5, 30, 30, 30, 30]
# 利用顧客、解約顧客に分けてそれぞれのカラムを可視化する
for i, column in enumerate(columns):
fig = sns.FacetGrid(data, col='Attrition_Flag', hue='Attrition_Flag', height=5)
fig.map(sns.histplot, column, bins=bins[i], kde=False)
就学レベルごとの解約分布
就学レベルごとに大きな比率の差は見られない
第4四半期から第1四半期のカード利用頻度の変化
解約顧客は利用頻度が下がりやすい傾向にある
リボ払いによって滞納している残高の合計額
解約していない人は1500ドルを中心に分散しているが解約する人は同じくらいの比率
上記データを可視化して把握できた内容をまとめると下記の3点を把握できました。
- 解約が多い比率は扶養人数2、3に寄っている
- 解約顧客は利用頻度が下がりやすい傾向にある
- 解約ユーザーは合計取引回数の平均が低く40回の周辺に分布している
特徴量エンジニアリング
探索的データ分析(EDA)で可視化してわかった考察を元に新たに特徴量を作成します。
特徴量データを作成
今回はこちらの4つの項目でカテゴリカル変数を作成します。
- 扶養人数をカテゴリカル変数に変換
- 年齢のカテゴリ変数を変換
- 利用頻度の変化のカテゴリ変数を作成
- 合計取引回数のカテゴリ変数を作成
# 扶養人数レベルの作成
data['DepS'] = data['Dependent_count'].map(lambda s: 1 if s <= 1 else 0)
data['DepM'] = data['Dependent_count'].map(lambda s: 1 if 2 <= s <= 3 else 0)
data['DepL'] = data['Dependent_count'].map(lambda s: 1 if s >= 4 else 0)
# 年齢のカテゴリ変数を作成
data['AgeBand'] = pd.qcut(
data['Customer_Age'],
5,
labels=['AgeBand_1','AgeBand_2','AgeBand_3','AgeBand_4','AgeBand_5'])
# 利用頻度の変化のカテゴリ変数を作成
data['Ct_ChngS'] = data['Total_Ct_Chng_Q4_Q1'].map(lambda s:1 if s < data['Total_Ct_Chng_Q4_Q1'].mean() else 0)
data['Ct_ChngL'] = data['Total_Ct_Chng_Q4_Q1'].map(lambda s:1 if s >= data['Total_Ct_Chng_Q4_Q1'].mean() else 0)
# 合計取引回数のカテゴリ変数を作成
data['Trans_CtS'] = data['Total_Trans_Ct'].map(lambda s:1 if s < data['Total_Trans_Ct'].mean() else 0)
data['Trans_CtL'] = data['Total_Trans_Ct'].map(lambda s:1 if s < data['Total_Trans_Ct'].mean() else 0)
ベースモデルの再評価
特徴量データを作成したら、ベースモデルの学習と正解率の確認と同様に学習、検証データを作成して正解率の出力を実行します。
# 目的変数
target = data['Attrition_Flag_01']
# カテゴリカルデータをone-hot-encodingで変換
dummies_col = [
'Gender',
'Marital_Status',
'Income_Category',
'Card_Category',
'AgeBand'
]
data = pd.get_dummies(data, columns= dummies_col)
# 学習に必要のないデータを削除
drop_col = [
'CLIENTNUM',
'Attrition_Flag',
'Education_Level',
'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2',
'Attrition_Flag_01',
]
data = data.drop(drop_col, axis=1) # inplace = Trueを指定すると元のデータが入れ替わる
# 標準化するカラムの指定
scaling_columns = [
'Customer_Age',
'Months_on_book',
'Credit_Limit',
'Total_Revolving_Bal',
'Avg_Open_To_Buy',
'Total_Trans_Amt',
'Total_Trans_Ct'
]
# データの標準化
for column in scaling_columns:
data[column] = (data[column] - data[column].mean())/data[column].std()
# カラムを確認しやすくするために転置して出力
data.head().T
モデルの学習と評価
データ準備後は学習、検証データを作成して精度の出力を行う
from sklearn.model_selection import train_test_split
# 訓練データの一部を分割し検証データを作成
X_train, X_val, y_train, y_val = train_test_split(
data, target,
test_size=0.3, shuffle=True, random_state=0
)
# ロジスティック回帰の学習と評価
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
# modelの定義と学習
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
# 訓練データの予測と正答率の選出
y_pred = model.predict(X_train)
print('train_acc:',accuracy_score(y_train,y_pred))
# valの正解率
y_pred_val = model.predict(X_val)
val_acc = accuracy_score(y_val, y_pred_val)
print('val_acc:', val_acc)
>>>
train_acc: 0.9108352144469526
val_acc: 0.9081934846989141
実行結果より、多少正解率が上昇していることが確認できます。
学習モデルの比較
次にさまざまなモデルで学習し、正解率の高いモデルを探索します。
その際に、KFoldを利用して平均的な精度を出力を行います。
まずは先ほどと同様に学習データを作成します。
# data import
data = pd.read_csv('/kaggle/input/predicting-credit-card-customer-attrition-with-m/BankChurners.csv')
# 01変換
data['Attrition_Flag_01'] = data['Attrition_Flag'].replace({'Existing Customer': 0.0, 'Attrited Customer': 1.0})
# 学歴ごとの解約率
data['Education_Level_num'] = data['Education_Level'].replace({
'Unknown':0,
'Uneducated': 1,
'High School': 2,
'College':3,
'Post-Graduate':4,
'Graduate':5,
'Doctorate':6})
# 特徴量の作成
# 扶養人数レベルの作成
data['DepS'] = data['Dependent_count'].map(lambda s: 1 if s <= 1 else 0)
data['DepM'] = data['Dependent_count'].map(lambda s: 1 if 2 <= s <= 3 else 0)
data['DepL'] = data['Dependent_count'].map(lambda s: 1 if s >= 4 else 0)
# 年齢のカテゴリ変数を作成
data['AgeBand'] = pd.qcut(data['Customer_Age'],5,labels=['AgeBand_1','AgeBand_2','AgeBand_3','AgeBand_4','AgeBand_5'])
# 変更された合計金額のカテゴリ変数を作成
data['Ct_ChngS'] = data['Total_Ct_Chng_Q4_Q1'].map(lambda s:1 if s < data['Total_Ct_Chng_Q4_Q1'].mean() else 0)
data['Ct_ChngL'] = data['Total_Ct_Chng_Q4_Q1'].map(lambda s:1 if s >= data['Total_Ct_Chng_Q4_Q1'].mean() else 0)
# 合計取引回数のカテゴリ変数を作成
data['Trans_CtS'] = data['Total_Trans_Ct'].map(lambda s:1 if s < data['Total_Trans_Ct'].mean() else 0)
data['Trans_CtL'] = data['Total_Trans_Ct'].map(lambda s:1 if s < data['Total_Trans_Ct'].mean() else 0)
# 目的変数
target = data['Attrition_Flag_01']
# カテゴリカル変数の変換
dummies_col = ['Gender','Marital_Status','Income_Category','Card_Category', 'AgeBand']
data = pd.get_dummies(data, columns= dummies_col)
# 学習に不要なカラムの削除
drop_col = [
'CLIENTNUM',
'Attrition_Flag',
'Education_Level',
'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_1',
'Naive_Bayes_Classifier_Attrition_Flag_Card_Category_Contacts_Count_12_mon_Dependent_count_Education_Level_Months_Inactive_12_mon_2',
'Attrition_Flag_01',
]
data = data.drop(drop_col, axis=1) # inplace = Trueを指定すると元のデータが入れ替わる
# データの標準化
scaling_columns = ['Customer_Age','Months_on_book','Credit_Limit','Total_Revolving_Bal','Avg_Open_To_Buy','Total_Trans_Amt','Total_Trans_Ct']
# データの標準化
for column in scaling_columns:
data[column] = (data[column] - data[column].mean())/data[column].std()
train = data
train.head().T
次にKFoldを実装します。
KFold
:データセットをk個に分割し、1つを評価用、残りk-1個を訓練データとして分割します。
分割した分だけ学習と評価を繰り返し、得られるk個のモデルと性能評価から平均性能を算出する手法です。
→複数データを変えるので安定し、全データで学習するので汎化性能の高いモデルが作成できます。
# KFoldのimport
from sklearn.model_selection import KFold
# n_splitsで分割数が指定(3,5,10分割がよく用いられる分割数)
cv = KFold(n_splits=3, random_state=0, shuffle=True)
# fold毎に学習データのインデックスと評価データのインデックスが得られる
for i ,(trn_index, val_index) in enumerate(cv.split(train, target)):
print(f'Fold : {i}')
# データ全体(Xとy)を学習データと評価データに分割
X_train ,X_val = train.loc[trn_index], train.loc[val_index]
y_train ,y_val = target[trn_index], target[val_index]
# 学習と検証データのサイズ表示
print(f'Train : {X_train.shape}')
print(f'Valid : {X_val.shape}')
>>>
Fold : 0
Train : (594, 25)
Valid : (297, 25)
Fold : 1
Train : (594, 25)
Valid : (297, 25)
Fold : 2
Train : (594, 25)
Valid : (297, 25)
上記のシェイプのようにランダムに分割されるので、それぞれで学習と予測を行い平均値を出力してモデルごとの精度を確認します。
KFoldでのロジスティック回帰の学習と評価
先ほど学習したロジスティック回帰をKFoldで再度学習し、平均正解率を出力します。
# KFoldのimport
from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
train_acc_list = []
val_acc_list = []
cv = KFold(n_splits=5, random_state=0, shuffle=True)
# foldごとに学習と評価
for i, (train_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[train_index], train.loc[val_index]
y_train, y_val = target[train_index], target[val_index]
# modelの学習
model_logi = LogisticRegression(max_iter=2000)
model_logi.fit(X_train,y_train)
# trainの正解率
y_pred = model_logi.predict(X_train)
train_acc = accuracy_score(y_train, y_pred)
print('train_acc:',train_acc)
train_acc_list.append(train_acc)
# valの正解率
y_pred_val = model_logi.predict(X_val)
val_acc = accuracy_score(y_val, y_pred_val)
print('val_acc:', val_acc)
val_acc_list.append(val_acc)
print('----------------------------')
print('-'*10 + 'Result' +'-'*10)
print('train_acc:{}\n'.format(train_acc_list),'Ave:{}'.format(np.mean(train_acc_list)))
print('val_acc:{}\n'.format(val_acc_list),'Ave:{}'.format(np.mean(val_acc_list)))
>>>
Fold:0
train_acc: 0.910381434390816
val_acc: 0.9076999012833169
----------------------------
Fold:1
train_acc: 0.9112455252437971
val_acc: 0.9047384007897334
----------------------------
Fold:2
train_acc: 0.908911379906196
val_acc: 0.9116049382716049
----------------------------
Fold:3
train_acc: 0.9102690693655887
val_acc: 0.9150617283950617
----------------------------
Fold:4
train_acc: 0.9119970377684522
val_acc: 0.9071604938271605
----------------------------
----------Result----------
train_acc:[0.910381434390816, 0.9112455252437971, 0.908911379906196, 0.9102690693655887, 0.9119970377684522]
Ave:0.91056088933497
val_acc:[0.9076999012833169, 0.9047384007897334, 0.9116049382716049, 0.9150617283950617, 0.9071604938271605]
Ave:0.9092530925133755
valの平均正解率Ave:0.9092530925133755
を基準として他のモデルも確認していきます。
SVMの学習と評価
次に同様のデータにてSVMの正解率を算出していきます。
# KFoldのimport
from sklearn.model_selection import KFold
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
train_acc_list = []
val_acc_list = []
cv = KFold(n_splits=5, random_state=0, shuffle=True)
# foldごとに学習と評価
for i, (train_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[train_index], train.loc[val_index]
y_train, y_val = target[train_index], target[val_index]
# modelの学習
model_svc = SVC(random_state=0)
model_svc.fit(X_train,y_train)
# trainの正解率
y_pred = model_svc.predict(X_train)
train_acc = accuracy_score(y_train, y_pred)
print('train_acc:',train_acc)
train_acc_list.append(train_acc)
# valの正解率
y_pred_val = model_svc.predict(X_val)
val_acc = accuracy_score(y_val, y_pred_val)
print('val_acc:', val_acc)
val_acc_list.append(val_acc)
print('----------------------------')
print('-'*10 + 'Result' +'-'*10)
print('train_acc:{}\n'.format(train_acc_list),'Ave:{}'.format(np.mean(train_acc_list)))
print('val_acc:{}\n'.format(val_acc_list),'Ave:{}'.format(np.mean(val_acc_list)))
>>>
Fold:0
train_acc: 0.9312430564127886
val_acc: 0.9220138203356367
----------------------------
Fold:1
train_acc: 0.9317368226144921
val_acc: 0.9210266535044422
----------------------------
Fold:2
train_acc: 0.9308812638854603
val_acc: 0.9269135802469136
----------------------------
Fold:3
train_acc: 0.9289064428536163
val_acc: 0.9288888888888889
----------------------------
Fold:4
train_acc: 0.9322389533448531
val_acc: 0.9244444444444444
----------------------------
----------Result----------
train_acc:[0.9312430564127886, 0.9317368226144921, 0.9308812638854603, 0.9289064428536163, 0.9322389533448531]
Ave:0.931001307822242
val_acc:[0.9220138203356367, 0.9210266535044422, 0.9269135802469136, 0.9288888888888889, 0.9244444444444444]
Ave:0.9246574774840651
上記結果から、SVMのvalの平均正解率が高いことが分かります。
以降他のモデルについても同様に学習と評価を行なっていきます。
マルチパーセプトロン(MLP)の学習と評価
# KFoldのimport
from sklearn.model_selection import KFold
import tensorflow as tf
import random
import os
from tensorflow.keras.utils import to_categorical
# 乱数を固定
tf.random.set_seed(0)
np.random.seed(0)
random.seed(0)
os.environ["PYTHONHASHSEED"] = "0"
train_acc_list = []
val_acc_list = []
cv = KFold(n_splits=5, random_state=0, shuffle=True)
# foldごとに学習と評価
for i, (train_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[train_index], train.loc[val_index]
y_train, y_val = target[train_index], target[val_index]
# modelの学習
model_mlp = tf.keras.models.Sequential([
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(32, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(2, activation='softmax')
])
model_mlp.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),
loss='categorical_crossentropy',
metrics=['accuracy'])
model_mlp.fit(
X_train, to_categorical(y_train),
batch_size=256, epochs=100, verbose=False
)
# trainの正解率
y_pred = np.argmax(model_mlp.predict(X_train), axis=1)
train_acc = accuracy_score(y_train, y_pred)
print('train_acc:',train_acc)
train_acc_list.append(train_acc)
# valの正解率
y_pred_val = np.argmax(model_mlp.predict(X_val), axis=1)
val_acc = accuracy_score(y_val, y_pred_val)
print('val_acc:', val_acc)
val_acc_list.append(val_acc)
print('----------------------------')
print('-'*10 + 'Result' +'-'*10)
print('train_acc:{}\n'.format(train_acc_list),'Ave:{}'.format(np.mean(train_acc_list)))
print('val_acc:{}\n'.format(val_acc_list),'Ave:{}'.format(np.mean(val_acc_list)))
>>>
Fold:0
254/254 [==============================] - 0s 1ms/step
train_acc: 0.9476607826194297
64/64 [==============================] - 0s 1ms/step
val_acc: 0.9264560710760118
----------------------------
Fold:1
254/254 [==============================] - 0s 1ms/step
train_acc: 0.9577829897543513
64/64 [==============================] - 0s 1ms/step
val_acc: 0.9299111549851925
----------------------------
Fold:2
254/254 [==============================] - 0s 1ms/step
train_acc: 0.9511231794618613
64/64 [==============================] - 0s 1ms/step
val_acc: 0.9417283950617283
----------------------------
Fold:3
254/254 [==============================] - 0s 966us/step
train_acc: 0.9455689953098001
64/64 [==============================] - 0s 984us/step
val_acc: 0.934320987654321
----------------------------
Fold:4
254/254 [==============================] - 0s 1ms/step
train_acc: 0.954455689953098
64/64 [==============================] - 0s 1ms/step
val_acc: 0.9367901234567901
----------------------------
----------Result----------
train_acc:[0.9476607826194297, 0.9577829897543513, 0.9511231794618613, 0.9455689953098001, 0.954455689953098]
Ave:0.9513183274197081
val_acc:[0.9264560710760118, 0.9299111549851925, 0.9417283950617283, 0.934320987654321, 0.9367901234567901]
Ave:0.9338413464468088
SVMよりもさらに正解率が高いことが分かります。
RandomForestの学習と評価
# KFoldのimport
from sklearn.model_selection import KFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
train_acc_list = []
val_acc_list = []
cv = KFold(n_splits=5, random_state=0, shuffle=True)
# foldごとに学習と評価
for i, (train_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[train_index], train.loc[val_index]
y_train, y_val = target[train_index], target[val_index]
# modelの学習
model_rf = RandomForestClassifier(random_state=0)
model_rf.fit(X_train,y_train)
# trainの正解率
y_pred = model_rf.predict(X_train)
train_acc = accuracy_score(y_train, y_pred)
print('train_acc:',train_acc)
train_acc_list.append(train_acc)
# valの正解率
y_pred_val = model_rf.predict(X_val)
val_acc = accuracy_score(y_val, y_pred_val)
print('val_acc:', val_acc)
val_acc_list.append(val_acc)
print('----------------------------')
print('-'*10 + 'Result' +'-'*10)
print('train_acc:{}\n'.format(train_acc_list),'Ave:{}'.format(np.mean(train_acc_list)))
print('val_acc:{}\n'.format(val_acc_list),'Ave:{}'.format(np.mean(val_acc_list)))
>>>
Fold:0
train_acc: 1.0
val_acc: 0.9471865745310958
----------------------------
Fold:1
train_acc: 1.0
val_acc: 0.945705824284304
----------------------------
Fold:2
train_acc: 1.0
val_acc: 0.9565432098765432
----------------------------
Fold:3
train_acc: 1.0
val_acc: 0.96
----------------------------
Fold:4
train_acc: 1.0
val_acc: 0.951604938271605
----------------------------
----------Result----------
train_acc:[1.0, 1.0, 1.0, 1.0, 1.0]
Ave:1.0
val_acc:[0.9471865745310958, 0.945705824284304, 0.9565432098765432, 0.96, 0.951604938271605]
Ave:0.9522081093927095
LightGBMの学習と評価
最後にLightGBMの学習と評価を行います。
# KFoldのimport
from sklearn.model_selection import KFold
import lightgbm as lgb
from sklearn.metrics import accuracy_score
train_acc_list = []
val_acc_list = []
cv = KFold(n_splits=5, random_state=0, shuffle=True)
lgb_params = {
"objective":"binary",
"metrics":"binary_error",
"force_row_wise":True,
"verbose":-1,
"seed":0
}
verbose_eval = 10
# foldごとに学習と評価
for i, (train_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[train_index], train.loc[val_index]
y_train, y_val = target[train_index], target[val_index]
# lightgbmのデータセットを定義
lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid = lgb.Dataset(X_val, y_val)
# modelの学習
model_lgb = lgb.train(
params = lgb_params,
train_set = lgb_train,
valid_sets = [lgb_train, lgb_valid],
num_boost_round = 10000,
callbacks = [
lgb.early_stopping(stopping_rounds=10, verbose=True),
lgb.log_evaluation(verbose_eval)
]
)
# trainの正解率
y_pred = model_lgb.predict(X_train)
train_acc = accuracy_score(
y_train,
np.where(y_pred>=0.5, 1, 0)
)
print('train_acc:',train_acc)
train_acc_list.append(train_acc)
# valの正解率
y_pred_val = model_lgb.predict(X_val)
val_acc = accuracy_score(
y_val,
np.where(y_pred_val>=0.5, 1, 0)
)
print('val_acc:', val_acc)
val_acc_list.append(val_acc)
print('----------------------------')
print('-'*10 + 'Result' +'-'*10)
print('train_acc:{}\n'.format(train_acc_list),'Ave:{}'.format(np.mean(train_acc_list)))
print('val_acc:{}\n'.format(val_acc_list),'Ave:{}'.format(np.mean(val_acc_list)))
>>>
Fold:0
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.043328 valid_1's binary_error: 0.0513327
[20] training's binary_error: 0.0269103 valid_1's binary_error: 0.0325765
[30] training's binary_error: 0.0185162 valid_1's binary_error: 0.0306022
[40] training's binary_error: 0.0120973 valid_1's binary_error: 0.0286278
[50] training's binary_error: 0.00876435 valid_1's binary_error: 0.0261599
[60] training's binary_error: 0.00530799 valid_1's binary_error: 0.0276407
Early stopping, best iteration is:
[51] training's binary_error: 0.00864091 valid_1's binary_error: 0.0256663
train_acc: 0.9913590914701889
val_acc: 0.9743336623889437
----------------------------
Fold:1
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0404888 valid_1's binary_error: 0.0641658
[20] training's binary_error: 0.0245649 valid_1's binary_error: 0.0538006
[30] training's binary_error: 0.0171584 valid_1's binary_error: 0.0463968
[40] training's binary_error: 0.0117269 valid_1's binary_error: 0.0409674
[50] training's binary_error: 0.00777682 valid_1's binary_error: 0.0365252
[60] training's binary_error: 0.00456734 valid_1's binary_error: 0.0340573
[70] training's binary_error: 0.00234539 valid_1's binary_error: 0.0350444
Early stopping, best iteration is:
[66] training's binary_error: 0.00333292 valid_1's binary_error: 0.0330701
train_acc: 0.9966670781385014
val_acc: 0.9669299111549852
----------------------------
Fold:2
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0412244 valid_1's binary_error: 0.0533333
[20] training's binary_error: 0.0256727 valid_1's binary_error: 0.0345679
[30] training's binary_error: 0.0191311 valid_1's binary_error: 0.0301235
[40] training's binary_error: 0.0129598 valid_1's binary_error: 0.028642
[50] training's binary_error: 0.00876327 valid_1's binary_error: 0.0266667
Early stopping, best iteration is:
[46] training's binary_error: 0.0103678 valid_1's binary_error: 0.0266667
train_acc: 0.9896321895828191
val_acc: 0.9733333333333334
----------------------------
Fold:3
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0428289 valid_1's binary_error: 0.0523457
[20] training's binary_error: 0.0255492 valid_1's binary_error: 0.0404938
[30] training's binary_error: 0.0171563 valid_1's binary_error: 0.037037
Early stopping, best iteration is:
[23] training's binary_error: 0.0213528 valid_1's binary_error: 0.037037
train_acc: 0.9786472475931869
val_acc: 0.9629629629629629
----------------------------
Fold:4
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0417181 valid_1's binary_error: 0.0553086
[20] training's binary_error: 0.0264132 valid_1's binary_error: 0.0449383
[30] training's binary_error: 0.0180202 valid_1's binary_error: 0.04
[40] training's binary_error: 0.0128363 valid_1's binary_error: 0.0360494
[50] training's binary_error: 0.00925697 valid_1's binary_error: 0.0340741
Early stopping, best iteration is:
[49] training's binary_error: 0.00962725 valid_1's binary_error: 0.0325926
train_acc: 0.9903727474697606
val_acc: 0.9674074074074074
----------------------------
----------Result----------
train_acc:[0.9913590914701889, 0.9966670781385014, 0.9896321895828191, 0.9786472475931869, 0.9903727474697606]
Ave:0.9893356708508915
val_acc:[0.9743336623889437, 0.9669299111549852, 0.9733333333333334, 0.9629629629629629, 0.9674074074074074]
Ave:0.9689934554495265
それぞれの正解率を比較し、各モデルの中でLightGBMの正解率が一番高いことが分かります。
今回はLightGBMを利用して正解率の向上を図ります。
ハイパーパラメータチューニング
モデルのハイパーパラメータの最適化を行うことで予測精度を向上させることができます。
ハイパーパラメーターにチューニング方法にはいくつかの手法があります。
グリッドサーチ
:指定した区間内で最適な値を網羅的に探索します。総当たり探索するので、時間がかかる 似た方法で、指定区間内をランダムに選び検証するランダムサーチという手法もあります。
ベイズ最適化
:過去の評価履歴から、次の探索を決定していく手法です。
今回はベイズ最適化の一種である、「TPE (Tree-structured Parzen Estimator)」を用いたライブラリの「optuna」を用いて、LightGBMのハイパーパラメータを探索します。
optunaのパラメータチューニングの方法
optunaでは下記の1〜4の手順でチューニングを実装します。
1.最適化関数を定義
最適化関数はハイパーパラメータの設定値を引数とし、それに基づいて評価指標を計算し最適化するべき指標を返す(objective()で定義する)
import optuna
def objective(trial):
x = trial.suggest_uniform('x', -10, 10)
return x**2
2.最適化を行う
create_study()
でStudyオブジェクトを作成し、
optimize()
メソッドで最適化を行う
study = optuna.create_study()
study.optimize(objective, n_trials=100) # objective関数と試行回数(n_trials)
3.最適化した数値を取得する
study.best_params # {'x': -0.0004995436945168217}
study.best_value # 2.4954379631098334e-07
4.探索過程とパラメータの重要度の可視化
化
optuna.visualization.plot_optimization_history(study)
optuna.visualization.plot_param_importances(study)
パラメータのチューニング
今回利用してきたデータから、LightGBMのハイパーパラメータをoptunaでチューニングしていきます。
# ライブラリのimport
import optuna
import lightgbm as lgb
from sklearn.metrics import accuracy_score
from sklearn.model_selection import KFold
# 関数の定義
def objective(trial):
cv = KFold(n_splits=5, random_state=0, shuffle=True)
val_acc_list = []
lgb_params = {
"objective":"binary",
"metric":"binary_error",
"force_row_wise":True,
"seed":0,
"verbose":-1,
"max_depth":trial.suggest_int("max_depth", 1, 20),
"num_leaves":trial.suggest_int("num_leaves", 21, 41),
"learning_rate":trial.suggest_float("learning_rate", 0.1, 0.9)
}
verbose_eval = 100
for i, (trn_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[trn_index], train.loc[val_index]
y_train, y_val = target[trn_index], target[val_index]
# lightgbmのデータセットを定義
lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid = lgb.Dataset(X_val, y_val)
# modelの学習
model = lgb.train(
params = lgb_params,
train_set = lgb_train,
valid_sets = [lgb_train, lgb_valid],
num_boost_round = 10000,
callbacks = [
lgb.early_stopping(stopping_rounds=10,verbose=True),
lgb.log_evaluation(verbose_eval)
]
)
y_pred = model.predict(X_train)
y_pred_val = model.predict(X_val)
val_acc = accuracy_score(y_val, np.where(y_pred_val>=0.5 ,1 , 0))
val_acc_list.append(val_acc)
return np.mean(val_acc_list)
# オブジェクトの定義とoptimize()の呼び出し
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)
# 最適化した値とパラメータの出力
print("best_value:", study.best_value)
print("best_params:", study.best_params)
# 最適化の過程を表示 & 可視化
optuna_log_df = study.trials_dataframe(attrs=("number", "value", "params"))
display(optuna_log_df)
plt.scatter(optuna_log_df["params_max_depth"], optuna_log_df["value"])
[I 2023-03-18 08:08:29,911] A new study created in memory with name: no-name-1896f14d-ccb6-4ec8-bd42-f2322ced924c
Fold:0
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[54] training's binary_error: 0 valid_1's binary_error: 0.0271471
Fold:1
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[12] training's binary_error: 0.0029626 valid_1's binary_error: 0.0335637
Fold:2
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[31] training's binary_error: 0 valid_1's binary_error: 0.0271605
Fold:3
Training until validation scores don't improve for 10 rounds
Early stopping, best iteration is:
[26] training's binary_error: 0 valid_1's binary_error: 0.0276543
Fold:4
Training until validation scores don't improve for 10 rounds
......
[I 2023-03-18 08:11:24,866] Trial 99 finished with value: 0.9720553300915263 and parameters: {'max_depth': 19, 'num_leaves': 23, 'learning_rate': 0.24235671397550543}. Best is trial 41 with value: 0.9743265450379633.
Early stopping, best iteration is:
[33] training's binary_error: 0.00567761 valid_1's binary_error: 0.0311111
best_value: 0.9743265450379633
best_params: {'max_depth': 20, 'num_leaves': 21, 'learning_rate': 0.21633604144297186}
上記の結果から、最大の正解率とパラメータを取得することができました。
best_value: 0.9743265450379633
best_params: {'max_depth': 20, 'num_leaves': 21, 'learning_rate': 0.21633604144297186}
また、下記のコードより試行回数ごとの正解率と最高値のグラフを可視化することができます。
optuna.visualization.plot_optimization_history(study)
チューニングしたパラメータの重要度をそれぞれ可視化することも可能です。
optuna.visualization.plot_param_importances(study)
今回はmax_depth
がパラメータの中で精度の向上に大きく貢献していることがわかりました。
最適化したパラメータでの学習
optunaのパラメータのチューニングより最適化したパラメータを利用して再度学習を行います。
# ライブラリのimport
from sklearn.model_selection import KFold
import lightgbm as lgb
from sklearn.metrics import accuracy_score
# 正解率を格納するリストの定義
train_acc_list = []
val_acc_list = []
cv = KFold(n_splits=5, random_state=0, shuffle=True)
# lgbのパラメータの定義
lgb_params = {
"objective":"binary",
"metrics":"binary_error",
"force_row_wise":True,
"verbose":-1,
"seed":0,
'max_depth': 11,
'num_leaves': 29,
'learning_rate': 0.543391802986801
}
verbose_eval = 10
# foldごとに学習と評価
for i, (train_index, val_index) in enumerate(cv.split(train, target)):
print('Fold:{}'.format(i))
# train,valに分割
X_train, X_val = train.loc[train_index], train.loc[val_index]
y_train, y_val = target[train_index], target[val_index]
# lightgbmのデータセットを定義
lgb_train = lgb.Dataset(X_train, y_train)
lgb_valid = lgb.Dataset(X_val, y_val)
# modelの学習
model_lgb = lgb.train(
params = lgb_params,
train_set = lgb_train,
valid_sets = [lgb_train, lgb_valid],
num_boost_round = 10000,
callbacks = [
lgb.early_stopping(stopping_rounds=10, verbose=True),
lgb.log_evaluation(verbose_eval)
]
)
# trainの正解率
y_pred = model_lgb.predict(X_train)
train_acc = accuracy_score(
y_train,
np.where(y_pred>=0.5, 1, 0)
)
train_acc_list.append(train_acc)
# valの正解率
y_pred_val = model_lgb.predict(X_val)
val_acc = accuracy_score(
y_val,
np.where(y_pred_val>=0.5, 1, 0)
)
val_acc_list.append(val_acc)
print('----------------------------')
print('-'*10 + 'Result' +'-'*10)
print('train_acc:{}\n'.format(train_acc_list),'Ave:{}'.format(np.mean(train_acc_list)))
print('val_acc:{}\n'.format(val_acc_list),'Ave:{}'.format(np.mean(val_acc_list)))
Fold:0
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.00938156 valid_1's binary_error: 0.0345508
[20] training's binary_error: 0 valid_1's binary_error: 0.0301086
[30] training's binary_error: 0 valid_1's binary_error: 0.0271471
[40] training's binary_error: 0 valid_1's binary_error: 0.0271471
[50] training's binary_error: 0 valid_1's binary_error: 0.0256663
Early stopping, best iteration is:
[44] training's binary_error: 0 valid_1's binary_error: 0.023692
----------------------------
Fold:1
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.00888779 valid_1's binary_error: 0.0370188
[20] training's binary_error: 0 valid_1's binary_error: 0.0310958
[30] training's binary_error: 0 valid_1's binary_error: 0.0291214
[40] training's binary_error: 0 valid_1's binary_error: 0.029615
[50] training's binary_error: 0 valid_1's binary_error: 0.0286278
Early stopping, best iteration is:
[45] training's binary_error: 0 valid_1's binary_error: 0.0276407
----------------------------
Fold:2
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0103678 valid_1's binary_error: 0.0330864
[20] training's binary_error: 0.000123426 valid_1's binary_error: 0.028642
[30] training's binary_error: 0 valid_1's binary_error: 0.025679
[40] training's binary_error: 0 valid_1's binary_error: 0.0251852
Early stopping, best iteration is:
[35] training's binary_error: 0 valid_1's binary_error: 0.0246914
----------------------------
Fold:3
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0109849 valid_1's binary_error: 0.0335802
[20] training's binary_error: 0 valid_1's binary_error: 0.0320988
[30] training's binary_error: 0 valid_1's binary_error: 0.0306173
[40] training's binary_error: 0 valid_1's binary_error: 0.0246914
[50] training's binary_error: 0 valid_1's binary_error: 0.0232099
[60] training's binary_error: 0 valid_1's binary_error: 0.0271605
Early stopping, best iteration is:
[50] training's binary_error: 0 valid_1's binary_error: 0.0232099
----------------------------
Fold:4
Training until validation scores don't improve for 10 rounds
[10] training's binary_error: 0.0102444 valid_1's binary_error: 0.0390123
[20] training's binary_error: 0 valid_1's binary_error: 0.0335802
[30] training's binary_error: 0 valid_1's binary_error: 0.0296296
[40] training's binary_error: 0 valid_1's binary_error: 0.0271605
[50] training's binary_error: 0 valid_1's binary_error: 0.0251852
[60] training's binary_error: 0 valid_1's binary_error: 0.0271605
Early stopping, best iteration is:
[50] training's binary_error: 0 valid_1's binary_error: 0.0251852
----------------------------
----------Result----------
train_acc:[1.0, 1.0, 1.0, 1.0, 1.0]
Ave:1.0
val_acc:[0.9763079960513327, 0.9723593287265548, 0.9753086419753086, 0.9767901234567902, 0.9748148148148148]
Ave:0.9751161810049602
問題なく学習でき、検証データで平均95.51%
の精度を獲得できました。
予測値の出力
最後に学習したモデルを利用して予測を行います。
予測した値をmodel_predict_Flag
に追加して解約フラグとの差分を出力してみます。
# データのimport
data = pd.read_csv('/kaggle/input/predicting-credit-card-customer-attrition-with-m/BankChurners.csv')
# model_predict_Flag列に予測した値をそれぞれ格納
data["model_predict_Flag"] = np.where(
model_lgb.predict(train)>=0.5,
"Attrited Customer",
"Existing Customer"
)
# dataから顧客ID,解約フラグ,予測した解約フラグを抽出して出力
data.loc[:,
[
"CLIENTNUM",
"Attrition_Flag",
"model_predict_Flag"
]
]
問題なく予測できているようでした。
まとめ
今回はkaggleに用意されているデータセットを利用して学習と予測を行いました。
その過程で、探索的データ分析(EDA)を利用することでデータの特徴を理解し、
特徴量エンジニアリングを行うことで精度の向上を図ることができました。
kaggleの予測精度向上を図る上で必要な作業だと分かりましたので今後も精度の高いモデルの予測を目指したいと思います。
こちらに、本記事で紹介した以外の取り組みも含めた実際のkaggleのnotebookで実装したコードが記載されてますのでご確認ください。