This notebook compares the performance of four different boosting classifiers. The models attempt to predict which bank customers are likely to churn (exit) based on several customer-related features.

In [31]:
# import classifier algorithms
from lightgbm import LGBMClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import GradientBoostingClassifier
from catboost import CatBoostClassifier
from xgboost import XGBClassifier

# other imports
import pandas as pd 
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix as cm, f1_score, roc_auc_score
from sklearn.model_selection import cross_validate, cross_val_predict, StratifiedKFold

# supress warnings
import warnings
warnings.filterwarnings('ignore')

Read data¶

In [32]:
df = pd.read_csv('input/Churn_Modelling.csv')
In [33]:
df.head()
Out[33]:
id CustomerId LastName CreditScore Country Sex Age Tenure Balance NumberOfProducts HasCreditCard IsActiveMember SalaryEstimate Exited
0 1 15634602 Hargrave 619 Belgium Female 42 2 0.00 1 1 1 101348.88 1
1 2 15647311 Hill 608 Slovakia Female 41 1 83807.86 1 0 1 112542.58 0
2 3 15619304 Onio 502 Belgium Female 42 8 159660.80 3 1 0 113931.57 1
3 4 15701354 Boni 699 Belgium Female 39 1 0.00 2 0 0 93826.63 0
4 5 15737888 Mitchell 850 Slovakia Female 43 2 125510.82 1 1 1 79084.10 0

Data exploration¶

In [34]:
plt.figure(figsize=(5, 5))
plt.pie(
    df['Exited'].value_counts().sort_index(),
    labels=['Retained', 'Exited'],
    autopct='%0.2f%%',
    startangle=90,
    counterclock=False,
    colors=['#4C72B0', '#DD8452'],
    explode=[0, 0.05],
    wedgeprops={'edgecolor': 'white', 'linewidth': 2},
    textprops={'fontsize': 12},
)
plt.title('Customer Churn Distribution', fontsize=14, fontweight='bold')
plt.axis('equal')
plt.tight_layout()
plt.show()
No description has been provided for this image
In [35]:
plt.figure(figsize=(5, 5))
plt.pie(
    [(df['Balance'] == 0).sum(), (df['Balance'] > 0).sum()],
    labels=['Zero Balance', 'Positive Balance'],
    autopct='%0.2f%%',
    startangle=90,
    counterclock=False,
    colors=['#4C72B0', '#DD8452'],
    explode=[0, 0.05],
    wedgeprops={'edgecolor': 'white', 'linewidth': 2},
    textprops={'fontsize': 12},
)
plt.title('Zero vs Positive Balance', fontsize=14, fontweight='bold')
plt.axis('equal')
plt.tight_layout()
plt.show()
No description has been provided for this image
In [36]:
plt.figure(figsize=(8, 5))
plt.hist(df['Age'].dropna(), bins=20, edgecolor='black', color='#DD8452')
plt.title(f"Age Distribution (median age = {round(df['Age'].median())})")
plt.xlabel('Age')
plt.ylabel('Frequency')
plt.show()
No description has been provided for this image

Prepare data¶

Encoding¶

In [37]:
# one hot encode 'Country'
col_to_encode = 'Country'
df = pd.get_dummies(df, columns=[col_to_encode], drop_first=True, dtype='int8')

# binary encode 'Sex'
df['Sex'] = LabelEncoder().fit_transform(df['Sex'])
In [38]:
df.head()
Out[38]:
id CustomerId LastName CreditScore Sex Age Tenure Balance NumberOfProducts HasCreditCard IsActiveMember SalaryEstimate Exited Country_Netherlands Country_Slovakia
0 1 15634602 Hargrave 619 0 42 2 0.00 1 1 1 101348.88 1 0 0
1 2 15647311 Hill 608 0 41 1 83807.86 1 0 1 112542.58 0 0 1
2 3 15619304 Onio 502 0 42 8 159660.80 3 1 0 113931.57 1 0 0
3 4 15701354 Boni 699 0 39 1 0.00 2 0 0 93826.63 0 0 0
4 5 15737888 Mitchell 850 0 43 2 125510.82 1 1 1 79084.10 0 0 1

Define features and target¶

In [39]:
# features
base_cols = ['HasCreditCard','CreditScore','Sex','Age','Tenure','Balance','NumberOfProducts','IsActiveMember','SalaryEstimate']
geo_cols = [col for col in df.columns if col.startswith(col_to_encode+'_')]
X = df[base_cols + geo_cols]

# target
y = df['Exited']

Train and evaluate model¶

In [40]:
models = [
    CatBoostClassifier(verbose=0),
    AdaBoostClassifier(),
    GradientBoostingClassifier(),
    XGBClassifier()
]
In [41]:
def train_model(clf, X, y, cv):
    X_arr = np.asarray(X) 
    y_arr = np.asarray(y)

    fold_acc, fold_f1, fold_auc, fold_importance = [], [], [], []
    oof_pred = np.empty(len(y_arr), dtype=int)

    for train_idx, test_idx in cv.split(X_arr, y_arr):
        clf.fit(X_arr[train_idx], y_arr[train_idx])

        pred  = clf.predict(X_arr[test_idx])
        proba = clf.predict_proba(X_arr[test_idx])[:, 1]
        oof_pred[test_idx] = pred

        fold_acc.append(accuracy_score(y_arr[test_idx], pred))
        fold_f1.append(f1_score(y_arr[test_idx], pred))
        fold_auc.append(roc_auc_score(y_arr[test_idx], proba))
        fold_importance.append(clf.feature_importances_)

    importance = np.mean(fold_importance, axis=0)
    importance = importance / importance.sum()

    conf_matrix = cm(y_arr, oof_pred)

    return np.array(fold_acc), np.array(fold_f1), np.array(fold_auc), conf_matrix, importance
In [42]:
accuracy_scores, f1_scores, auc, algorithm_names = [], [], [], []
importances = {}

for clf in models:
    model_name = type(clf).__name__
    fold_acc, fold_f1, fold_auc, conf_matrix, importance = train_model(clf, X, y, StratifiedKFold(n_splits=5, shuffle=True, random_state=42))

    print(f"{model_name} confusion matrix:")
    print(conf_matrix)

    algorithm_names.append(model_name)
    accuracy_scores.append(fold_acc.mean())
    f1_scores.append(fold_f1.mean())
    auc.append(fold_auc.mean())
    importances[model_name] = importance
CatBoostClassifier confusion matrix:
[[7647  316]
 [1045  992]]
AdaBoostClassifier confusion matrix:
[[7586  377]
 [1112  925]]
GradientBoostingClassifier confusion matrix:
[[7671  292]
 [1081  956]]
XGBClassifier confusion matrix:
[[7527  436]
 [1045  992]]
In [43]:
df_performance = pd.DataFrame({'Algorithm': algorithm_names,
                               'Accuracy': accuracy_scores,
                               'F1 Score': f1_scores,
                                'AUC': auc}).sort_values('AUC', ascending=False)
df_performance
Out[43]:
Algorithm Accuracy F1 Score AUC
0 CatBoostClassifier 0.8639 0.592823 0.866548
2 GradientBoostingClassifier 0.8627 0.581493 0.866093
3 XGBClassifier 0.8519 0.572417 0.843575
1 AdaBoostClassifier 0.8511 0.553658 0.842957

Feature importance¶

In [44]:
df_importance = pd.DataFrame(importances, index=X.columns)

best_name = df_performance.iloc[0]['Algorithm']
best_imp = df_importance[best_name].sort_values(ascending=False).head(5)

plt.figure(figsize=(8, 6))
bars = plt.barh(best_imp.index[::-1], best_imp.values[::-1],
                color='#DD8452', edgecolor='black')
plt.bar_label(bars, fmt='%.3f', padding=3, fontsize=9)
plt.title(f'Top 5 Feature Importances From Best Model ({best_name})',
          fontsize=14, fontweight='bold')
plt.xlabel('Normalised importance')
plt.margins(x=0.1)
plt.tight_layout()
plt.show()
No description has been provided for this image