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()
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()
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()
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()