Создание модели оттока кредитной карты

С постоянно растущим числом эмитентов кредитных карт банковская отрасль сталкивается с жесткой конкуренцией. Поэтому удержание клиентов стало приоритетом для банков. Одной из серьезных проблем, с которыми сталкиваются банки, является высокий уровень текучести среди держателей кредитных карт.

Отток клиентов по кредитным картам или процесс прекращения клиентами использования кредитных карт может оказать пагубное влияние на прибыльность банка. Поэтому создание точной модели, которая может прогнозировать отток клиентов, имеет важное значение для банков.

В предыдущей статье мы подробно рассмотрели типичный набор банковских данных и провели исследовательский анализ данных для набора данных об оттоке кредитных карт от Kaggle (см.: набор данных Kaggle об оттоке кредитных карт).

Вы можете прочитать мою статью об исследовательском анализе данных в этом наборе данных здесь: -

https://medium.com/@joshiakash89/solving-the-attrition-puzzle-an-analytics-primer-for-banking-customer-data-8f8f988bde91

В этой статье мы обсудим, как создать и оценить модель оттока кредитных карт с помощью конвейера моделей. В окончательной модели, созданной для набора данных, мы смогли получить показатель точности 94,7 % и показатель полноты 92,2 %. Конвейеры модели содержат следующие этапы:

  1. Предварительная обработка данных. Первым шагом является предварительная обработка данных. Это включает в себя обработку нулевых данных и выбросов, масштабирование числовых данных и подготовку данных для моделирования.
  2. Разработка признаков. Следующим шагом является создание признаков, которые могут повысить точность модели. Разработка функций включает в себя создание новых функций из существующих.
  3. Решение проблемы несбалансированного набора данных. При моделировании оттока количество клиентов, которые остаются в компании, обычно намного превышает количество клиентов, которые уходят. Это может привести к несбалансированному набору данных, когда модель смещена в сторону класса большинства. Мы будем оценивать решения этой проблемы, используя передискретизацию.
  4. Выбор модели. После создания объектов следующим шагом будет выбор подходящей модели. Первый шаг — определить тип проблемы — классификация или прогнозирование. Для задач классификации есть несколько моделей на выбор. Мы будем реализовывать как ансамблевые, так и неансамблевые модели, чтобы выбрать лучшую.
  5. Настройка гиперпараметров. После выбора модели следующим шагом будет настройка ее гиперпараметров. Это включает в себя настройку параметров модели для улучшения ее производительности. Настройка гиперпараметров имеет решающее значение для достижения максимально возможной производительности модели. Мы пропустили этот шаг ниже для упрощения, поскольку для каждой модели необходимо настроить разные гиперпараметры.
  6. Оценка модели. Последний шаг – оценка производительности модели. Это включает в себя тестирование модели на наборе данных проверки, чтобы определить ее точность, достоверность, полноту и оценку AUC. Производительность модели следует сравнить с производительностью других моделей, чтобы определить, какая из них работает лучше всего.

Конвейер создания модели истощения в Python

#import main libs
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# pip install seaborn
import seaborn as sns
from sklearn.model_selection import train_test_split
# Graphics in retina format are more sharp and legible
%config InlineBackend.figure_format = 'retina'
%matplotlib inline
#import libraries for making a pipeline
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import OrdinalEncoder
from sklearn.preprocessing import MinMaxScaler
from sklearn.pipeline import Pipeline,make_pipeline
from sklearn.feature_selection import SelectKBest,chi2
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn import tree
from sklearn.compose import make_column_selector
from sklearn.model_selection import cross_val_score
#import data to dataframe
df = pd.read_csv("datasets/BankChurners.csv")
#drop last 2 columns as not required
df.drop(labels={'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'},axis=1,inplace = True)
df.shape#(10127, 21)
#create numerical target var
df['Attrition_Flag']=df['Attrition_Flag'].apply(lambda x: 0 if x=='Existing Customer' else 1)
df_copy=df.copy()
df_copy=df_copy.drop('CLIENTNUM',axis=1)
df_copy.shape
df_copy['Attrition_Flag'].value_counts()

Основные этапы конвейера: -

  1. Ввод значений (отсутствующих или выпадающих). Этот шаг включает обработку отсутствующих или выпадающих значений в наборе данных. Однако в этом примере у нас нет пропущенных значений, поэтому мы можем пропустить этот шаг.
  2. Кодировать переменные. На втором этапе переменные кодируются, чтобы сделать их подходящими для моделей машинного обучения. На этом этапе мы используем два разных метода, основанных на типе переменной — OneHotEncoding для категориальных переменных и OrdinalEncoding для порядковых переменных. ColumnTransformer из sklearn.compose используется для применения этих методов к определенным столбцам набора данных.
  3. Создание и выбор функций.Разработка функций — это важный шаг в построении модели машинного обучения. Этот шаг включает в себя создание новых функций из существующих, которые могут повысить точность модели. Наконец, мы можем использовать SelectKBest из sklearn.feature_selection, чтобы выбрать наиболее важные функции на основе F-статистики и p-значения.
  4. Подгонка модели и прогнозирование.Последний шаг — подгонка модели машинного обучения к преобразованным данным и создание прогнозов. В этом примере мы используем классификатор случайного леса из sklearn.ensemble в качестве нашей модели машинного обучения. Конвейер обучается на данных обучения с использованием метода подгонки и делает прогнозы на данных тестирования с использованием метода прогнозирования.
all categorical datatype as category
cat_collist=[         
#'Months_Inactive_12_mon',  #consider the var as numeric
#'Contacts_Count_12_mon',   #consider the var as numeric
'Total_Relationship_Count',  
'Gender',                   
'Dependent_count',          
'Marital_Status',            
'Card_Category']
ord_collist=['Education_Level',        
'Income_Category']
df_copy[cat_collist] = df_copy[cat_collist].astype('category')
#convert ordinal data using Ordinalencoder as it is diffcult to use in pipeline
df_copy[['Education_Level']] = OrdinalEncoder(categories=
               [['Unknown','Uneducated','High School','Graduate','College','Post-Graduate','Doctorate']]).fit_transform(df_copy[['Education_Level']])
df_copy[['Income_Category']] = OrdinalEncoder(categories=
               [['Unknown','Less than $40K','$40K - $60K','$60K - $80K','$80K - $120K','$120K +']]).fit_transform(df_copy[['Income_Category']])
#Feature pipeline here based on univariate DA in previous program
new_features=['MOB_lbl','Ina_12_cnt_lbl','Credit_Limit_lbl','util_lbl','Trans_Amt_lbl','Trans_Ct_lbl']
df_copy['MOB_lbl'] = pd.cut(df_copy['Months_on_book'], bins=[0, 24, 36, float('Inf')], 
                              labels=['<=24', '24-36', '>36'],include_lowest = True)
df_copy['Ina_12_cnt_lbl'] = pd.cut(df_copy['Months_Inactive_12_mon'], bins=[0, 3, float('Inf')],
                                     labels=['<=3', '>3'],include_lowest = True)
df_copy['Credit_Limit_lbl'] = pd.cut(df_copy['Credit_Limit'], bins=[0, 5000, 10000,15000,30000,float('Inf')], 
                                       labels=['0-5k', '5-10k','10-15k','15-30k','>30k'],include_lowest = True)
df_copy['util_lbl'] = pd.cut(df_copy['Avg_Utilization_Ratio'], bins=[0, 0.2,0.5, float('Inf')], 
                                       labels=['0-0.2','0.2-0.5','>0.5'],include_lowest = True)
df_copy['Trans_Amt_lbl'] = pd.cut(df_copy['Total_Trans_Amt'], bins=[0, 5000, 12500, float('Inf')], 
                                       labels=['0-5k','5-12.5k','>12.5k'],include_lowest = True)
df_copy['Trans_Ct_lbl'] = pd.cut(df_copy['Total_Trans_Ct'], bins=[0, 50,100, float('Inf')], 
                                       labels=['0-50','50-100','>100'],include_lowest = True)
#create test and train data
df_train_data,df_test_data,df_train_target,df_test_target = train_test_split(df_copy.drop('Attrition_Flag',axis=1),df_copy['Attrition_Flag'],test_size=0.3,
                                                                           random_state=42,stratify=df_copy['Attrition_Flag'])
print(df_train_data.shape,' and ',df_test_data.shape)
#(7088, 19)  and  (3039, 19)
#Create Pipe components to be used to create the final PipeLine to evaluate models in next step
#Categorical pipeline to one-hot encode categorical vars
cat_pipeline = Pipeline([
    ('one_hot', OneHotEncoder(sparse=False,handle_unknown='ignore',drop='first'))
])
#numerical transformer to normalize/scale numerical data
Num_trf = Pipeline([
    ('scale',MinMaxScaler())
])
#column transformer to use cat_pipelone and ignore non cat Variables
trf1 = ColumnTransformer([
        ("pass", "passthrough", make_column_selector(dtype_exclude="category")),
        ("cat", cat_pipeline, make_column_selector(dtype_include="category"))
])
#Modification of above transformer with scaling of numerical vars
trf11 = ColumnTransformer([
        ("Num_trf", Num_trf, make_column_selector(dtype_exclude="category")),
        ("cat", cat_pipeline, make_column_selector(dtype_include="category"))
])
def pipe_call_allmodels(model_list,X_train,Y_train,X_test,Y_test):

    #create List to store model stats
    model_stats=[]
    
    for clf in model_list:
        #create pipe for the model 
        pipe_clf = Pipeline([
            ('trf1',trf11), #scaler + one hot
            ('clf',clf) #apply Model
        ])
        print("Training model: ",clf)
        
        df = pipe_clf.fit(X_train,Y_train)
        y_pred=df.predict(X_test)
        
        #display confusion Matrix
        ConfusionMatrixDisplay.from_predictions(df_test_target,y_pred)
        #display AUC curve of the model
        RocCurveDisplay.from_predictions(df_test_target,y_pred)
        
        #enumerate all stat outputs
        TN, FP, FN, TP = confusion_matrix(Y_test,y_pred).ravel()
        FPR_F1_err = 100*FP/(FP+TN)
        FNR_F2_err = 100*FN/(FN+TP)
        precision = 100*TP/(TP+FP)
        Recall = 100*TP/(TP+FN)
        Accuracy = 100*(TP+TN)/(TP+FP+TN+FN)
        F1_score = 2*precision*Recall/(precision+Recall)
        
        stat_output={
            "clf":clf,
            "TN":TN,
            "FP":FP,
            "FN":FN,
            "TP":TP,
            "FPR_F1_err":FPR_F1_err,
            "FNR_F2_err":FNR_F2_err,
            "F1_score":F1_score,
            "precision":precision,
            "Recall":Recall,
            "Accuracy":Accuracy,
            "roc_auc_score":100*roc_auc_score(Y_test,y_pred)        
        }
        model_stats.append(stat_output)
        
    #create output dataframe    
    Model_stat_df=pd.DataFrame(model_stats)
    return Model_stat_df;
#Import various models to be evaluated
from sklearn.linear_model import LogisticRegression,SGDClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import LinearSVC,SVC
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier,VotingClassifier,AdaBoostClassifier,GradientBoostingClassifier
from sklearn.neighbors import KNeighborsClassifier
from xgboost import XGBClassifier
from sklearn.ensemble import ExtraTreesClassifier

#create model pipelines
#1) linear Logistic Reg with L2 penalty (SGD is a linear incremental version of LR)
clf_LR = LogisticRegression(penalty='l2',random_state=42,solver='liblinear')
#2) SVM classifier
clf_SVM = LinearSVC( penalty='l2',loss='squared_hinge',random_state=42)
#3) Naive Bayes Classifier
clf_NB = GaussianNB()
#5) KNN classifier(learn)
clf_KNN = KNeighborsClassifier()
#4) Random forest Classifier(train similar to Decision Tree)
clf_RF = RandomForestClassifier(n_estimators=200,min_samples_leaf=10,random_state=42,criterion='gini')
#6) linear SGD with L2 penalty (SGD is a linear incremental version of LR)
clf_SGD = SGDClassifier(penalty='l2',loss='hinge',random_state=42)
#7) Decision tree with L2 penalty
clf_DT = DecisionTreeClassifier(min_samples_leaf=100,max_depth=5,random_state=42,criterion='entropy')
#8) voting tree with above models

#ensemble models
clf_vote = VotingClassifier(estimators=[('clf_LR',clf_LR),('clf_RF',clf_RF),('clf_KNN',clf_KNN),
                                        ('clf_DT',clf_DT)],voting='soft')
clf_ET = ExtraTreesClassifier(n_estimators=200,min_samples_leaf=10,random_state=42,criterion='gini')
clf_ADA = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1),random_state=42)
clf_GB = GradientBoostingClassifier(random_state=42)
clf_XGB=XGBClassifier()

#Classification Models - basic(including RF)
model_list_basic=[clf_LR,clf_SVM,clf_NB,clf_KNN,clf_RF,clf_SGD,clf_DT]
#Classification models-ensemble advanced
model_list_ensemble = [clf_vote,clf_ET,clf_ADA,clf_GB,clf_XGB]

Вызов функции для оценки базовых моделей

#Create model performance DF from function Pipe call - Basic models without hyperparameter tuning
df_stat=pipe_call_allmodels(model_list_basic,df_train_data,df_train_target,df_test_data,df_test_target)

#output table sorted in order of AUC score
df_stat.sort_values(by="roc_auc_score",ascending=False)

  • Лучше всего работает классификатор Random Forest. Лучший показатель отзыва (правильно идентифицированные сбрасыватели составляют 70%)
  • Дерево решений и линейный SVM являются следующими лучшими исполнителями
  • Несбалансированный набор данных, ведущий к низкому отзыву
#Create model performance DF from function Pipe call - Ensemble models
df_stat=pipe_call_allmodels(model_list_ensemble,df_train_data,df_train_target,df_test_data,df_test_target)

#output table sorted in order of AUC score
df_stat.sort_values(by="roc_auc_score",ascending=False)

  • Мы можем достичь более высокой точности, используя ансамблевые модели по сравнению с неансамблевыми моделями.
  • XGBoost работает лучше всего. Наилучший показатель отзыва 86,5% (правильно идентифицированные сбрасыватели - 86,5%)
  • ADAboost и Gradient Boost — следующие лучшие исполнители
  • Несбалансированный набор данных, ведущий к низкому отзыву

Работа с набором данных о дисбалансе для лучшего прогнозирования оттока

#pip install imblearn
#library for SMOTETomek oversampling
from imblearn.combine import SMOTETomek
#library for random oversampling
from imblearn.over_sampling import RandomOverSampler, SMOTE
#library for undersampling(not effective for small datasets due to loss of info)
from imblearn.under_sampling import NearMiss
#Use pipeline from imblearn and not Sklearn as it doesnt support oversampling
from imblearn.pipeline import Pipeline
#from sklearn.decomposition import PCA
#oversampler (ratio= majority class/minority class)
os =  RandomOverSampler(random_state=42) #OS performs better than smk for this dataset
#pca=PCA(random_state=42)
smk = SMOTETomek(random_state=42)
#Add oversampling in pipeline
def pipe_call_os_allmodels(model_list,X_train,Y_train,X_test,Y_test):

    #create List to store model stats
    model_stats=[]
    
    for clf in model_list:
        #create pipe for the model with Sampling
        pipe_clf = Pipeline([
            ('trf1',trf11), #scaler + one hot
            ('sampler',os), #Random oversampling with ratio=1(equal class sizes)
            #('pca',pca),#pca
            ('clf',clf) #apply Model
        ])
        print("Training model with Oversampling: ",clf)
        
        df = pipe_clf.fit(X_train,Y_train)
        y_pred=df.predict(X_test)
        
        #display confusion Matrix
        ConfusionMatrixDisplay.from_predictions(df_test_target,y_pred)
        #display AUC curve of the model
        RocCurveDisplay.from_predictions(df_test_target,y_pred)
        
        #enumerate all stat outputs
        TN, FP, FN, TP = confusion_matrix(Y_test,y_pred).ravel()
        FPR_F1_err = 100*FP/(FP+TN)
        FNR_F2_err = 100*FN/(FN+TP)
        precision = 100*TP/(TP+FP)
        Recall = 100*TP/(TP+FN)
        Accuracy = 100*(TP+TN)/(TP+FP+TN+FN)
        F1_score = 2*precision*Recall/(precision+Recall)
        
        stat_output={
            "clf":clf,
            "TN":TN,
            "FP":FP,
            "FN":FN,
            "TP":TP,
            "FPR_F1_err":FPR_F1_err,
            "FNR_F2_err":FNR_F2_err,
            "F1_score":F1_score,
            "precision":precision,
            "Recall":Recall,
            "Accuracy":Accuracy,
            "roc_auc_score":100*roc_auc_score(Y_test,y_pred)        
        }
        model_stats.append(stat_output)
        #For Loop ends here
        
    #create output dataframe    
    Model_stat_df=pd.DataFrame(model_stats)
    return Model_stat_df;
#Create model performance DF from function Pipe call - Ensemble models
df_stat=pipe_call_os_allmodels(model_list_ensemble,df_train_data,df_train_target,df_test_data,df_test_target)
#output table sorted in order of AUC score
df_stat.sort_values(by="roc_auc_score",ascending=False)

  • Повышение ADA достигает AUC 93,7% и точности 94,7% (отзыв = 92,2%).
  • Классификатор XGB достигает AUC 93,1% и точности 96,6% (отзыв = 88%

Наиболее важной метрикой для моделей истощения является способность модели правильно прогнозировать клиентов, которые могут уйти. Этот показатель измеряется с помощью чувствительности модели, также известной как отзыв или истинный положительный показатель.

В модели истощения цель состоит в том, чтобы определить клиентов, которые могут уйти, и принять соответствующие меры для их удержания. Если модель не может идентифицировать оттока клиентов, это может привести к более высокому уровню оттока, и поэтому крайне важно установить приоритет чувствительности модели над другими показателями, такими как точность или аккуратность. Следовательно, модель ADA Boost с отзывом 92,2% является наиболее эффективной моделью для прогнозирования будущих оттоков.

Рекомендации