4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

クレジットカードの解約予測(Predict_CreditCard_Churn)_kaggle DataSet

Last updated at Posted at 2023-03-18

目次

はじめに
データ表示と可視化
ベースラインモデルの学習と評価
探索的データ分析(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()

image.png

欠損値が無いか確認を行います。

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

データの中に欠損値は無いようです。
image.png

カテゴリカルデータの統計量表示を行います。

display(data.describe(exclude='number'))

image.png
※ferq:最頻値の頻度(出現回数)

データの特徴を表示

データ全体の特性を確認した後はカラムごとのデータを確認します。
下記のコードでデータの特徴をグラフとして可視化を行います。

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%程度と推測できる
image.png

男女の数値
男女比は若干女性が多い
image.png

就学レベルの分布
Graduate(大学院卒)の比率が多い
image.png
婚姻状況
既婚者の割合が比較的多い
image.png

収入カテゴリ
収入は少ない人ほど割合が多い
image.png
カードの種類分布
カードの種類はほとんどがブルーカード
image.png

ベースモデルの学習と正解率の確認

上記のデータからベースとするモデルでの学習を行い、ベースとなる予測精度を割り出します。
今回はロジスティック回帰で特徴量を学習し、正解率を算出します。
まずは目的変数と学習しないカラムを削除して学習データを準備します。

目的変数の定義とカラムの削除
# 目的変数
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

image.png

正しく学習できるように説明変数を標準化を行います。

データの標準化
# 標準化するカラムを指定
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

image.png
数値の大きいデータを標準化することができました。

次に正解率を表示するための学習、検証データに分割します。

train、valデータの作成
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   
)

ロジスティック回帰での学習と評価
学習、検証用データを準備できたら学習と精度の出力を行います。

LogisticRegressionの実装
# ロジスティック回帰の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) 

年齢分布
年齢分布に大きな差は見られない
image.png

扶養家族人数
解約者は扶養人数が2、3の比率が高い
image.png

就学レベルごとの解約分布
就学レベルごとに大きな比率の差は見られない
image.png

第4四半期から第1四半期のカード利用頻度の変化
解約顧客は利用頻度が下がりやすい傾向にある
image.png

過去12か月間に顧客が非アクティブだった月数
image.png

利用限度額
image.png

リボ払いによって滞納している残高の合計額
解約していない人は1500ドルを中心に分散しているが解約する人は同じくらいの比率
image.png

クレジット限度額に対する平均残高比率
image.png

合計取引回数
分散が狭く、中央値が40に寄っている
image.png

上記データを可視化して把握できた内容をまとめると下記の3点を把握できました。

  1. 解約が多い比率は扶養人数2、3に寄っている
  2. 解約顧客は利用頻度が下がりやすい傾向にある
  3. 解約ユーザーは合計取引回数の平均が低く40回の周辺に分布している

特徴量エンジニアリング

探索的データ分析(EDA)で可視化してわかった考察を元に新たに特徴量を作成します。

特徴量データを作成

今回はこちらの4つの項目でカテゴリカル変数を作成します。

  1. 扶養人数をカテゴリカル変数に変換
  2. 年齢のカテゴリ変数を変換
  3. 利用頻度の変化のカテゴリ変数を作成
  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

image.png

モデルの学習と評価
データ準備後は学習、検証データを作成して精度の出力を行う

特徴量を作成したデータでの学習と評価
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

image.png

次にKFoldを実装します。
KFold:データセットをk個に分割し、1つを評価用、残りk-1個を訓練データとして分割します。
分割した分だけ学習と評価を繰り返し、得られるk個のモデルと性能評価から平均性能を算出する手法です。
→複数データを変えるので安定し、全データで学習するので汎化性能の高いモデルが作成できます。

KFoldの実装コード
# 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で再度学習し、平均正解率を出力します。

LogisticRegressionの実装
# 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の正解率を算出していきます。

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)の学習と評価

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の学習と評価

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の学習と評価を行います。

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()で定義する)

x^2を最小にするxを最適化する場合の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)

newplot (3).png

パラメータの重要度の可視化
optuna.visualization.plot_param_importances(study)

newplot (4).png

パラメータのチューニング

今回利用してきたデータから、LightGBMのハイパーパラメータをoptunaでチューニングしていきます。

optunaを利用したLightGBMのパラメータの最適化
# ライブラリの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}

image.png
image.png

上記の結果から、最大の正解率とパラメータを取得することができました。

最大の正解率と最適化されたパラメータ
best_value: 0.9743265450379633
best_params: {'max_depth': 20, 'num_leaves': 21, 'learning_rate': 0.21633604144297186}

また、下記のコードより試行回数ごとの正解率と最高値のグラフを可視化することができます。

試行回数ごとの正解率の出力
optuna.visualization.plot_optimization_history(study)

image.png

チューニングしたパラメータの重要度をそれぞれ可視化することも可能です。

パラメータ重要度の出力
optuna.visualization.plot_param_importances(study)

image.png
今回は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"
         ]
        ]

image.png

問題なく予測できているようでした。

まとめ

今回はkaggleに用意されているデータセットを利用して学習と予測を行いました。
その過程で、探索的データ分析(EDA)を利用することでデータの特徴を理解し、
特徴量エンジニアリングを行うことで精度の向上を図ることができました。

kaggleの予測精度向上を図る上で必要な作業だと分かりましたので今後も精度の高いモデルの予測を目指したいと思います。

こちらに、本記事で紹介した以外の取り組みも含めた実際のkaggleのnotebookで実装したコードが記載されてますのでご確認ください。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?