1. Inserting data¶

Import libraries¶

In [1]:
import pandas as pd # data manipulation and analysis
import numpy as np # numerical
import matplotlib.pyplot as plt # visualisations
import seaborn as sns # visualisation (advanced)
from sklearn.model_selection import train_test_split, GridSearchCV # model selector/tuner
from sklearn.preprocessing import StandardScaler, LabelEncoder # data preprosessing
from sklearn.linear_model import LogisticRegression # logistic regression
from sklearn.svm import SVC # Support Vector Classifier
from sklearn.metrics import (accuracy_score, # model performance tools
                            precision_score, 
                            recall_score, 
                            f1_score, 
                            roc_auc_score, 
                            roc_curve)
from sklearn.metrics import confusion_matrix # confusion metrics

Insert the file using the path location¶

In [5]:
df = pd.read_excel('Telco_Customer_Churn.xlsx')

2. Data exploration¶

Check the first 10 rows of the dataset¶

In [7]:
df.head(10)
Out[7]:
CustomerID Count Country State City Zip Code Lat Long Latitude Longitude Gender ... Contract Paperless Billing Payment Method Monthly Charges Total Charges Churn Label Churn Value Churn Score CLTV Churn Reason
0 3668-QPYBK 1 United States California Los Angeles 90003 33.964131, -118.272783 33.964131 -118.272783 Male ... Month-to-month Yes Mailed check 53.85 108.15 Yes 1 86 3239 Competitor made better offer
1 9237-HQITU 1 United States California Los Angeles 90005 34.059281, -118.30742 34.059281 -118.307420 Female ... Month-to-month Yes Electronic check 70.70 151.65 Yes 1 67 2701 Moved
2 9305-CDSKC 1 United States California Los Angeles 90006 34.048013, -118.293953 34.048013 -118.293953 Female ... Month-to-month Yes Electronic check 99.65 820.5 Yes 1 86 5372 Moved
3 7892-POOKP 1 United States California Los Angeles 90010 34.062125, -118.315709 34.062125 -118.315709 Female ... Month-to-month Yes Electronic check 104.80 3046.05 Yes 1 84 5003 Moved
4 0280-XJGEX 1 United States California Los Angeles 90015 34.039224, -118.266293 34.039224 -118.266293 Male ... Month-to-month Yes Bank transfer (automatic) 103.70 5036.3 Yes 1 89 5340 Competitor had better devices
5 4190-MFLUW 1 United States California Los Angeles 90020 34.066367, -118.309868 34.066367 -118.309868 Female ... Month-to-month No Credit card (automatic) 55.20 528.35 Yes 1 78 5925 Competitor offered higher download speeds
6 8779-QRDMV 1 United States California Los Angeles 90022 34.02381, -118.156582 34.023810 -118.156582 Male ... Month-to-month Yes Electronic check 39.65 39.65 Yes 1 100 5433 Competitor offered more data
7 1066-JKSGK 1 United States California Los Angeles 90024 34.066303, -118.435479 34.066303 -118.435479 Male ... Month-to-month No Mailed check 20.15 20.15 Yes 1 92 4832 Competitor made better offer
8 6467-CHFZW 1 United States California Los Angeles 90028 34.099869, -118.326843 34.099869 -118.326843 Male ... Month-to-month Yes Electronic check 99.35 4749.15 Yes 1 77 5789 Competitor had better devices
9 8665-UTDHZ 1 United States California Los Angeles 90029 34.089953, -118.294824 34.089953 -118.294824 Male ... Month-to-month No Electronic check 30.20 30.2 Yes 1 97 2915 Competitor had better devices

10 rows × 33 columns

Check the last 10 rows of the dataset¶

In [9]:
df.tail(10)
Out[9]:
CustomerID Count Country State City Zip Code Lat Long Latitude Longitude Gender ... Contract Paperless Billing Payment Method Monthly Charges Total Charges Churn Label Churn Value Churn Score CLTV Churn Reason
7033 0871-OPBXW 1 United States California Twentynine Palms 92277 34.17211, -115.769773 34.172110 -115.769773 Female ... Month-to-month Yes Mailed check 20.05 39.25 No 0 80 5191 NaN
7034 3605-JISKB 1 United States California Twentynine Palms 92278 34.457829, -116.139589 34.457829 -116.139589 Male ... One year No Credit card (automatic) 60.00 3316.1 No 0 71 4212 NaN
7035 9767-FFLEM 1 United States California Westmorland 92281 33.03679, -115.60503 33.036790 -115.605030 Male ... Month-to-month Yes Credit card (automatic) 69.50 2625.25 No 0 35 4591 NaN
7036 8456-QDAVC 1 United States California Winterhaven 92283 32.852947, -114.850784 32.852947 -114.850784 Male ... Month-to-month Yes Bank transfer (automatic) 78.70 1495.1 No 0 20 2464 NaN
7037 7750-EYXWZ 1 United States California Yucca Valley 92284 34.159534, -116.425984 34.159534 -116.425984 Female ... One year No Electronic check 60.65 743.3 No 0 24 3740 NaN
7038 2569-WGERO 1 United States California Landers 92285 34.341737, -116.539416 34.341737 -116.539416 Female ... Two year Yes Bank transfer (automatic) 21.15 1419.4 No 0 45 5306 NaN
7039 6840-RESVB 1 United States California Adelanto 92301 34.667815, -117.536183 34.667815 -117.536183 Male ... One year Yes Mailed check 84.80 1990.5 No 0 59 2140 NaN
7040 2234-XADUH 1 United States California Amboy 92304 34.559882, -115.637164 34.559882 -115.637164 Female ... One year Yes Credit card (automatic) 103.20 7362.9 No 0 71 5560 NaN
7041 4801-JZAZL 1 United States California Angelus Oaks 92305 34.1678, -116.86433 34.167800 -116.864330 Female ... Month-to-month Yes Electronic check 29.60 346.45 No 0 59 2793 NaN
7042 3186-AJIEK 1 United States California Apple Valley 92308 34.424926, -117.184503 34.424926 -117.184503 Male ... Two year Yes Bank transfer (automatic) 105.65 6844.5 No 0 38 5097 NaN

10 rows × 33 columns

Summary of numerical features¶

In [11]:
df.describe()
Out[11]:
Count Zip Code Latitude Longitude Tenure Months Monthly Charges Churn Value Churn Score CLTV
count 7043.0 7043.000000 7043.000000 7043.000000 7043.000000 7043.000000 7043.000000 7043.000000 7043.000000
mean 1.0 93521.964646 36.282441 -119.798880 32.371149 64.761692 0.265370 58.699418 4400.295755
std 0.0 1865.794555 2.455723 2.157889 24.559481 30.090047 0.441561 21.525131 1183.057152
min 1.0 90001.000000 32.555828 -124.301372 0.000000 18.250000 0.000000 5.000000 2003.000000
25% 1.0 92102.000000 34.030915 -121.815412 9.000000 35.500000 0.000000 40.000000 3469.000000
50% 1.0 93552.000000 36.391777 -119.730885 29.000000 70.350000 0.000000 61.000000 4527.000000
75% 1.0 95351.000000 38.224869 -118.043237 55.000000 89.850000 1.000000 75.000000 5380.500000
max 1.0 96161.000000 41.962127 -114.192901 72.000000 118.750000 1.000000 100.000000 6500.000000

Info about dataset (check data types and missing values)¶

In [13]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 33 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   CustomerID         7043 non-null   object 
 1   Count              7043 non-null   int64  
 2   Country            7043 non-null   object 
 3   State              7043 non-null   object 
 4   City               7043 non-null   object 
 5   Zip Code           7043 non-null   int64  
 6   Lat Long           7043 non-null   object 
 7   Latitude           7043 non-null   float64
 8   Longitude          7043 non-null   float64
 9   Gender             7043 non-null   object 
 10  Senior Citizen     7043 non-null   object 
 11  Partner            7043 non-null   object 
 12  Dependents         7043 non-null   object 
 13  Tenure Months      7043 non-null   int64  
 14  Phone Service      7043 non-null   object 
 15  Multiple Lines     7043 non-null   object 
 16  Internet Service   7043 non-null   object 
 17  Online Security    7043 non-null   object 
 18  Online Backup      7043 non-null   object 
 19  Device Protection  7043 non-null   object 
 20  Tech Support       7043 non-null   object 
 21  Streaming TV       7043 non-null   object 
 22  Streaming Movies   7043 non-null   object 
 23  Contract           7043 non-null   object 
 24  Paperless Billing  7043 non-null   object 
 25  Payment Method     7043 non-null   object 
 26  Monthly Charges    7043 non-null   float64
 27  Total Charges      7043 non-null   object 
 28  Churn Label        7043 non-null   object 
 29  Churn Value        7043 non-null   int64  
 30  Churn Score        7043 non-null   int64  
 31  CLTV               7043 non-null   int64  
 32  Churn Reason       1869 non-null   object 
dtypes: float64(3), int64(6), object(24)
memory usage: 1.8+ MB

Unique values¶

In [15]:
df.nunique()
Out[15]:
CustomerID           7043
Count                   1
Country                 1
State                   1
City                 1129
Zip Code             1652
Lat Long             1652
Latitude             1652
Longitude            1651
Gender                  2
Senior Citizen          2
Partner                 2
Dependents              2
Tenure Months          73
Phone Service           2
Multiple Lines          3
Internet Service        3
Online Security         3
Online Backup           3
Device Protection       3
Tech Support            3
Streaming TV            3
Streaming Movies        3
Contract                3
Paperless Billing       2
Payment Method          4
Monthly Charges      1585
Total Charges        6531
Churn Label             2
Churn Value             2
Churn Score            85
CLTV                 3438
Churn Reason           20
dtype: int64

3. Data Cleaning¶

Drop columns¶

In [17]:
df = df.drop(['Churn Value','Lat Long','CustomerID','City','Count', 'Country', 'State', 'Zip Code', 'Latitude', 'Longitude'], axis=1)

Creating numerical columns¶

In [19]:
num_cols=['Tenure Months','Monthly Charges','Churn Score','CLTV','Total Charges']

Creating categorical variables¶

In [21]:
cat_cols=['Gender','Senior Citizen','Partner','Dependents','Phone Service','Multiple Lines','Internet Service','Online Security','Online Backup','Device Protection','Tech Support','Streaming TV','Streaming Movies','Contract','Paperless Billing','Payment Method','Churn Label','Churn Reason']

Convert 'Total Charges' to numeric and handle errors¶

In [23]:
# coerce means that numeric values in the column are changed to NaN
df['Total Charges'] = pd.to_numeric(df['Total Charges'], errors='coerce') 
# NaN values change to the column mean value
df['Total Charges'] = df['Total Charges'].fillna(df['Total Charges'].mean())

Create a copy of the DataFrame for model training¶

In [25]:
df_model = df.copy()

One-hot encoding categorical columns¶

In [31]:
# pd.get_dummies changes categorical into indicators from cat_cols
# drop_first=True drops the first category of each cat_cols variable to stop variable trap
df_encoded = pd.get_dummies(df, columns=cat_cols, drop_first=True)

Define features (X) and target (y)¶

In [33]:
# Drop churn label from independent variables
X = df_encoded.drop('Churn Label_Yes', axis=1)
# Make churn label the dependent variable
y = df_encoded['Churn Label_Yes']

Split the data into training and testing sets¶

In [35]:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, # 20% testing
                 random_state=42, # reproducibility with fixed seed for random splitting
                 stratify=y) # proportioned split for target and training

Standardize the numerical features¶

In [37]:
scaler = StandardScaler() # mean = 0 and standard deviation = 1
X_train[num_cols] = scaler.fit_transform(X_train[num_cols]) # scale training data
X_test[num_cols] = scaler.transform(X_test[num_cols]) # scale numerical columns without fitting for consistency 

4. Exploratory Data Analysis (EDA)¶

Univariate analysis of numerical columns¶

Checking summary statistics¶

In [39]:
df[num_cols].describe().T
Out[39]:
count mean std min 25% 50% 75% max
Tenure Months 7043.0 32.371149 24.559481 0.00 9.000 29.00 55.00 72.00
Monthly Charges 7043.0 64.761692 30.090047 18.25 35.500 70.35 89.85 118.75
Churn Score 7043.0 58.699418 21.525131 5.00 40.000 61.00 75.00 100.00
CLTV 7043.0 4400.295755 1183.057152 2003.00 3469.000 4527.00 5380.50 6500.00
Total Charges 7043.0 2283.300441 2265.000258 18.80 402.225 1400.55 3786.60 8684.80

Univariate analysis for categorical variables¶

Printing the % sub categories of each category¶

In [41]:
# printing proportions for each category
for i in cat_cols:
    print(df[i].value_counts (normalize=True))
    print('*'*40)
Gender
Male      0.504756
Female    0.495244
Name: proportion, dtype: float64
****************************************
Senior Citizen
No     0.837853
Yes    0.162147
Name: proportion, dtype: float64
****************************************
Partner
No     0.516967
Yes    0.483033
Name: proportion, dtype: float64
****************************************
Dependents
No     0.76899
Yes    0.23101
Name: proportion, dtype: float64
****************************************
Phone Service
Yes    0.903166
No     0.096834
Name: proportion, dtype: float64
****************************************
Multiple Lines
No                  0.481329
Yes                 0.421837
No phone service    0.096834
Name: proportion, dtype: float64
****************************************
Internet Service
Fiber optic    0.439585
DSL            0.343746
No             0.216669
Name: proportion, dtype: float64
****************************************
Online Security
No                     0.496663
Yes                    0.286668
No internet service    0.216669
Name: proportion, dtype: float64
****************************************
Online Backup
No                     0.438450
Yes                    0.344881
No internet service    0.216669
Name: proportion, dtype: float64
****************************************
Device Protection
No                     0.439443
Yes                    0.343888
No internet service    0.216669
Name: proportion, dtype: float64
****************************************
Tech Support
No                     0.493114
Yes                    0.290217
No internet service    0.216669
Name: proportion, dtype: float64
****************************************
Streaming TV
No                     0.398978
Yes                    0.384353
No internet service    0.216669
Name: proportion, dtype: float64
****************************************
Streaming Movies
No                     0.395428
Yes                    0.387903
No internet service    0.216669
Name: proportion, dtype: float64
****************************************
Contract
Month-to-month    0.550192
Two year          0.240664
One year          0.209144
Name: proportion, dtype: float64
****************************************
Paperless Billing
Yes    0.592219
No     0.407781
Name: proportion, dtype: float64
****************************************
Payment Method
Electronic check             0.335794
Mailed check                 0.228880
Bank transfer (automatic)    0.219225
Credit card (automatic)      0.216101
Name: proportion, dtype: float64
****************************************
Churn Label
No     0.73463
Yes    0.26537
Name: proportion, dtype: float64
****************************************
Churn Reason
Attitude of support person                   0.102729
Competitor offered higher download speeds    0.101124
Competitor offered more data                 0.086677
Don't know                                   0.082397
Competitor made better offer                 0.074906
Attitude of service provider                 0.072231
Competitor had better devices                0.069556
Network reliability                          0.055110
Product dissatisfaction                      0.054575
Price too high                               0.052434
Service dissatisfaction                      0.047619
Lack of self-service on Website              0.047084
Extra data charges                           0.030498
Moved                                        0.028357
Limited range of services                    0.023542
Long distance charges                        0.023542
Lack of affordable download/upload speed     0.023542
Poor expertise of phone support              0.010701
Poor expertise of online support             0.010166
Deceased                                     0.003210
Name: proportion, dtype: float64
****************************************

Visualise Numerical Columns¶

In [43]:
df[num_cols].describe().T # summary statistics for numerical columns, transpose rows to columns
df[num_cols].hist(figsize=(14, 14)) # histogram creation for numerical columns
plt.show() # show histograms
No description has been provided for this image

Bivariate and Multivariate analysis¶

In [45]:
for i in cat_cols:
    # exclude churn label
    if i!='Churn Label':
        # create normalised contingency table of columns and churn label, shown as percentage of bar, 
        (pd.crosstab(df[i],df['Churn Label'],normalize='index')*100).plot(kind='bar',figsize=(8,4),stacked=True)
        # set y axis label to %
        plt.ylabel('%')
        # display tables
        plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [47]:
# Checking the relationship between different numerical variables

# Plotting the correlation between numerical variables

plt.figure(figsize=(15,8)) # set plot size
sns.heatmap(df[num_cols].corr(), annot=True, fmt='0.2f', cmap='YlOrRd') # correlation for numerical columns, displaying 2d.p and colour themed
plt.title('Corrolation Matrix of Numerical Variables') # title heat map
plt.show() # show heatmap
No description has been provided for this image

ML Models development using LR and SVM¶

Logistic Regression¶

In [47]:
# start logistic regression with fixed random state (more reliable)
log_reg = LogisticRegression(random_state=42)
# fit feature matrix and target vector
log_reg.fit(X_train, y_train)
Out[47]:
LogisticRegression(random_state=42)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
LogisticRegression(random_state=42)

Predictions for logistic regression¶

In [49]:
# make predictions using trained logistic regression, on feature matrix and target values for test set
y_pred_lr = log_reg.predict(X_test)

Evaluation Metrics for logistic regression¶

In [51]:
# proportion of correctly predicted instances out of all instances
accuracy = accuracy_score(y_test, y_pred_lr) 
# proportion of true positive instances out of all instances predicted
precision = precision_score(y_test, y_pred_lr)
# proportion of true positive instances out of all actual positive instances
recall = recall_score(y_test, y_pred_lr)
# f1 score of all predictions
f1 = f1_score(y_test, y_pred_lr)
# ROC AUC score of predictions
roc_auc = roc_auc_score(y_test, y_pred_lr)

# print all results
print(f'Accuracy: {accuracy:.4f}')
print(f'Precision: {precision:.4f}')
print(f'Recall: {recall:.4f}')
print(f'F1 Score: {f1:.4f}')
print(f'ROC AUC: {roc_auc:.4f}')
Accuracy: 0.9879
Precision: 0.9837
Recall: 0.9706
F1 Score: 0.9771
ROC AUC: 0.9824

Confusion matrix for logistic regression¶

In [53]:
# apply confusion matrix onto the logistical regression model
conf_matrix = confusion_matrix(y_test, y_pred_lr)

# set plot size
plt.figure(figsize=(10, 6))

#create  heatmap of confusion matrix, with annotations, blue colours, and labeled axis
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues', xticklabels=['Not Churn', 'Churn'], yticklabels=['Not Churn', 'Churn'])
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')

# show confusion matrix
plt.show()
No description has been provided for this image

Support Vector Machine¶

In [55]:
# enables probability estimates which is needed for ROC AUC analysis
svm = SVC(probability=True)
# fits SVC model to training data, on feature matrix and target vector training set
svm.fit(X_train, y_train)
Out[55]:
SVC(probability=True)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SVC(probability=True)

Predictions for SVC¶

In [57]:
# Make predictions on the test set
y_pred_svm = svm.predict(X_test)

# Get predicted probabilities for the positive class (churn)
y_prob_svm = svm.predict_proba(X_test)[:, 1]

Evaluation Metrics for SVC¶

In [59]:
# Accuracy
accuracy_svm = accuracy_score(y_test, y_pred_svm)
print(f"Accuracy: {accuracy_svm:.4f}")

# Precision
precision_svm = precision_score(y_test, y_pred_svm)
print(f"Precision: {precision_svm:.4f}")

# Recall
recall_svm = recall_score(y_test, y_pred_svm)
print(f"Recall: {recall_svm:.4f}")

# F1 Score
f1_svm = f1_score(y_test, y_pred_svm)
print(f"F1 Score: {f1_svm:.4f}")

# ROC AUC (using predicted probabilities for the positive class)
roc_auc_svm = roc_auc_score(y_test, y_prob_svm)
print(f"ROC AUC: {roc_auc_svm:.4f}")
Accuracy: 0.9851
Precision: 0.9862
Recall: 0.9572
F1 Score: 0.9715
ROC AUC: 0.9989
In [61]:
# Generate confusion matrix
cm_svm = confusion_matrix(y_test, y_pred_svm)

# Visualize the confusion matrix using a heatmap
plt.figure(figsize=(6,6))
sns.heatmap(cm_svm, annot=True, fmt='d', cmap='Blues', xticklabels=['Non-Churn', 'Churn'], yticklabels=['Non-Churn', 'Churn'])
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.title('Confusion Matrix for SVM')
plt.show()
No description has been provided for this image

Evaluate the performance of the developed model using accuracy, precision, recall, F1 score and AUC-ROC¶

In [63]:
def evaluate_model(y_test, y_pred, model_name):
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    auc_roc = roc_auc_score(y_test, y_pred)
    
    print(f"{model_name} Model Performance:")
    print(f"Accuracy: {accuracy}")
    print(f"Precision: {precision}")
    print(f"Recall: {recall}")
    print(f"F1 Score: {f1}")
    print(f"AUC-ROC: {auc_roc}")
    print("\n")

evaluate_model(y_test, y_pred_lr, "Logistic Regression")
evaluate_model(y_test, y_pred_svm, "Support Vector Machine")
Logistic Regression Model Performance:
Accuracy: 0.9879347054648687
Precision: 0.983739837398374
Recall: 0.9705882352941176
F1 Score: 0.9771197846567967
AUC-ROC: 0.9823955669224211


Support Vector Machine Model Performance:
Accuracy: 0.9850958126330731
Precision: 0.9862258953168044
Recall: 0.9572192513368984
F1 Score: 0.9715061058344641
AUC-ROC: 0.9761941667312511


Hypertuning¶

In [65]:
# Initialize the Support Vector Machine classifier with probability estimation
svm = SVC(probability=True, random_state=42)

# Fit the SVM to the training data
svm.fit(X_train, y_train)

# Make predictions on the test set
y_pred_svm = svm.predict(X_test)

Define the parameter grid to tune¶

In [67]:
param_grid = {
    'C': [0.1, 1, 10, 100],  # Regularization parameter
    'kernel': ['linear', 'rbf', 'poly'],  # Kernel type
    'gamma': ['scale', 'auto'],  # Kernel coefficient
    'class_weight': [None, 'balanced']  # Handle class imbalance
}

Initialize GridSearchCV with 5-fold cross-validation¶

In [69]:
grid_search = GridSearchCV(svm, param_grid, cv=5, verbose=1, n_jobs=-1, scoring='accuracy')

Fit the grid search¶

In [71]:
grid_search.fit(X_train, y_train)
Fitting 5 folds for each of 48 candidates, totalling 240 fits
Out[71]:
GridSearchCV(cv=5, estimator=SVC(probability=True, random_state=42), n_jobs=-1,
             param_grid={'C': [0.1, 1, 10, 100],
                         'class_weight': [None, 'balanced'],
                         'gamma': ['scale', 'auto'],
                         'kernel': ['linear', 'rbf', 'poly']},
             scoring='accuracy', verbose=1)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=5, estimator=SVC(probability=True, random_state=42), n_jobs=-1,
             param_grid={'C': [0.1, 1, 10, 100],
                         'class_weight': [None, 'balanced'],
                         'gamma': ['scale', 'auto'],
                         'kernel': ['linear', 'rbf', 'poly']},
             scoring='accuracy', verbose=1)
SVC(probability=True, random_state=42)
SVC(probability=True, random_state=42)

Best hyperparameters and model¶

In [77]:
# Best hyperparameters found from GridSearchCV
print(f"Best hyperparameters found: {grid_search.best_params_}")

# Best model from grid search
best_svm = grid_search.best_estimator_
print(best_svm)
Best hyperparameters found: {'C': 10, 'class_weight': None, 'gamma': 'scale', 'kernel': 'linear'}
SVC(C=10, kernel='linear', probability=True, random_state=42)

Predictions and evaluation¶

In [79]:
# Make predictions using the best model
y_pred_best_svm = best_svm.predict(X_test)

# Evaluate performance metrics
print("SVM Model Performance (Best Model from Grid Search):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best_svm):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_best_svm):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_best_svm):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_best_svm):.4f}")
print(f"ROC AUC: {roc_auc_score(y_test, best_svm.predict_proba(X_test)[:, 1]):.4f}")
SVM Model Performance (Best Model from Grid Search):
Accuracy: 0.9922
Precision: 1.0000
Recall: 0.9706
F1 Score: 0.9851
ROC AUC: 0.9987

Confusion matrix¶

In [81]:
# Generate the confusion matrix
cm = confusion_matrix(y_test, y_pred_best_svm)

# Plot the confusion matrix
plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=["No Churn", "Churn"], yticklabels=["No Churn", "Churn"])
plt.title("Confusion Matrix")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()
No description has been provided for this image

Hyperparameter tuning for logistic regression¶

In [83]:
# Define a reduced parameter grid for Logistic Regression
# There are three dictionaries
# C gives regularisaiton strength because of smaller values
param_grid_lr = [
    # solver, Liblinear is a optimisation algorithm suitable for small datasets and both l1 and l2
    {'C': [0.1, 1, 10], 'solver': ['liblinear'], 'penalty': ['l1', 'l2']},
    # solver, ibfgs is an optimisation algorithm suitable for l2 penalty only
    {'C': [0.1, 1, 10], 'solver': ['lbfgs'], 'penalty': ['l2']},
    # solver saga is an optimisation algorithm suitable for l1 penalty only
    {'C': [0.1, 1, 10], 'solver': ['saga'], 'penalty': ['l1', 'l2']}
]

Perform Grid Search¶

In [85]:
grid_search_lr = GridSearchCV(LogisticRegression(random_state=42), param_grid_lr, cv=5, verbose=1, n_jobs=-1, scoring='accuracy')
grid_search_lr.fit(X_train, y_train)
Fitting 5 folds for each of 15 candidates, totalling 75 fits
Out[85]:
GridSearchCV(cv=5, estimator=LogisticRegression(random_state=42), n_jobs=-1,
             param_grid=[{'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
                          'solver': ['liblinear']},
                         {'C': [0.1, 1, 10], 'penalty': ['l2'],
                          'solver': ['lbfgs']},
                         {'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
                          'solver': ['saga']}],
             scoring='accuracy', verbose=1)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=5, estimator=LogisticRegression(random_state=42), n_jobs=-1,
             param_grid=[{'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
                          'solver': ['liblinear']},
                         {'C': [0.1, 1, 10], 'penalty': ['l2'],
                          'solver': ['lbfgs']},
                         {'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
                          'solver': ['saga']}],
             scoring='accuracy', verbose=1)
LogisticRegression(random_state=42)
LogisticRegression(random_state=42)

Increase the number of iterations for Logistic Regression¶

In [87]:
# Best Logistic Regression model from grid search
best_log_reg = grid_search_lr.best_estimator_
y_pred_best_log_reg = best_log_reg.predict(X_test)

Evaluate Performance Metrics for the Best Logistic Regression Model¶

In [89]:
# Evaluate performance metrics for the best Logistic Regression model
accuracy_best_log_reg = accuracy_score(y_test, y_pred_best_log_reg)
precision_best_log_reg = precision_score(y_test, y_pred_best_log_reg)
recall_best_log_reg = recall_score(y_test, y_pred_best_log_reg)
f1_best_log_reg = f1_score(y_test, y_pred_best_log_reg)
roc_auc_best_log_reg = roc_auc_score(y_test, y_pred_best_log_reg)

print(f'Logistic Regression Model Performance (Best Model from Reduced Grid Search):')
print(f'Accuracy: {accuracy_best_log_reg:.4f}')
print(f'Precision: {precision_best_log_reg:.4f}')
print(f'Recall: {recall_best_log_reg:.4f}')
print(f'F1 Score: {f1_best_log_reg:.4f}')
print(f'ROC AUC: {roc_auc_best_log_reg:.4f}')
Logistic Regression Model Performance (Best Model from Reduced Grid Search):
Accuracy: 0.9943
Precision: 0.9946
Recall: 0.9840
F1 Score: 0.9892
ROC AUC: 0.9910
In [91]:
# Ensure that the best Logistic Regression model is defined
best_log_reg = grid_search_lr.best_estimator_

# Make predictions using the best Logistic Regression model
y_pred_best_log_reg = best_log_reg.predict(X_test)
In [93]:
print(f'Logistic Regression Model Performance (Best Model from Grid Search):')
print(f'Accuracy: {accuracy_best_log_reg:.4f}')
print(f'Precision: {precision_best_log_reg:.4f}')
print(f'Recall: {recall_best_log_reg:.4f}')
print(f'F1 Score: {f1_best_log_reg:.4f}')
print(f'ROC AUC: {roc_auc_best_log_reg:.4f}')
Logistic Regression Model Performance (Best Model from Grid Search):
Accuracy: 0.9943
Precision: 0.9946
Recall: 0.9840
F1 Score: 0.9892
ROC AUC: 0.9910
In [95]:
# Cross-validation for Logistic Regression
#cv_scores_lr_after = cross_val_score(best_log_reg, X, y, cv=5, scoring='accuracy')
#print(f"Logistic Regression Cross-Validation Accuracy (After Tuning): {cv_scores_lr_after.mean():.4f}")

Logistic Regression vs. SVM Comparison after tuning¶

In [98]:
# Step 1: Evaluate Logistic Regression before and after hyperparameter tuning
log_reg_baseline = LogisticRegression(random_state=42).fit(X_train, y_train)
y_pred_log_reg_baseline = log_reg_baseline.predict(X_test)
In [100]:
# Step 2: Evaluate SVM before and after hyperparameter tuning
y_pred_svm_baseline = svm.predict(X_test)
In [102]:
# Step 3: Hyperparameter tuning for both models (as done earlier)

# Compare performance before and after tuning
print(f"Logistic Regression (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_log_reg_baseline)}")
print(f"Precision: {precision_score(y_test, y_pred_log_reg_baseline)}")
print(f"Recall: {recall_score(y_test, y_pred_log_reg_baseline)}")
print(f"F1 Score: {f1_score(y_test, y_pred_log_reg_baseline)}")

print(f"\nSVM (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_svm_baseline)}")
print(f"Precision: {precision_score(y_test, y_pred_svm_baseline)}")
print(f"Recall: {recall_score(y_test, y_pred_svm_baseline)}")
print(f"F1 Score: {f1_score(y_test, y_pred_svm_baseline)}")
Logistic Regression (Before Tuning):
Accuracy: 0.9879347054648687
Precision: 0.983739837398374
Recall: 0.9705882352941176
F1 Score: 0.9771197846567967

SVM (Before Tuning):
Accuracy: 0.9850958126330731
Precision: 0.9862258953168044
Recall: 0.9572192513368984
F1 Score: 0.9715061058344641
In [104]:
print("\nLogistic Regression (After Tuning):")
print(f"Accuracy: {accuracy_best_log_reg:.4f}")
print(f"Precision: {precision_best_log_reg:.4f}")
print(f"Recall: {recall_best_log_reg:.4f}")
print(f"F1 Score: {f1_best_log_reg:.4f}")
Logistic Regression (After Tuning):
Accuracy: 0.9943
Precision: 0.9946
Recall: 0.9840
F1 Score: 0.9892
In [106]:
print("\nSVM (After Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best_svm):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_best_svm):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_best_svm):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_best_svm):.4f}")
SVM (After Tuning):
Accuracy: 0.9922
Precision: 1.0000
Recall: 0.9706
F1 Score: 0.9851

Visualise Performance Metrics¶

In [109]:
# Performance metrics before and after tuning
metrics = ['Accuracy', 'Precision', 'Recall', 'F1 Score', 'ROC AUC']
log_reg_before = [accuracy_score(y_test, y_pred_log_reg_baseline), precision_score(y_test, y_pred_log_reg_baseline), recall_score(y_test, y_pred_log_reg_baseline), f1_score(y_test, y_pred_log_reg_baseline), roc_auc_score(y_test, y_pred_log_reg_baseline)]
log_reg_after = [accuracy_best_log_reg, precision_best_log_reg, recall_best_log_reg, f1_best_log_reg, roc_auc_best_log_reg]
svm_before = [accuracy_score(y_test, y_pred_svm_baseline), precision_score(y_test, y_pred_svm_baseline), recall_score(y_test, y_pred_svm_baseline), f1_score(y_test, y_pred_svm_baseline), roc_auc_score(y_test, y_pred_svm_baseline)]
svm_after = [accuracy_score(y_test, y_pred_best_svm), precision_score(y_test, y_pred_best_svm), recall_score(y_test, y_pred_best_svm), f1_score(y_test, y_pred_best_svm), roc_auc_score(y_test, best_svm.predict_proba(X_test)[:, 1])]

# Plotting the metrics
fig, ax = plt.subplots(1, 2, figsize=(14, 6))

ax[0].bar(metrics, log_reg_before, color='blue', alpha=0.6, label='Before Tuning')
ax[0].bar(metrics, log_reg_after, color='green', alpha=0.6, label='After Tuning')
ax[0].set_title('Logistic Regression Performance')
ax[0].legend()

ax[1].bar(metrics, svm_before, color='blue', alpha=0.6, label='Before Tuning')
ax[1].bar(metrics, svm_after, color='green', alpha=0.6, label='After Tuning')
ax[1].set_title('SVM Performance')
ax[1].legend()

plt.show()
No description has been provided for this image

Detailed Comparison¶

In [112]:
# Create a summary table
comparison_data = {
    'Metric': metrics,
    'LR (Before Tuning)': log_reg_before,
    'LR (After Tuning)': log_reg_after,
    'SVM (Before Tuning)': svm_before,
    'SVM (After Tuning)': svm_after
}

comparison_df = pd.DataFrame(comparison_data)
print(comparison_df)
      Metric  LR (Before Tuning)  LR (After Tuning)  SVM (Before Tuning)  \
0   Accuracy            0.987935           0.994322             0.985096   
1  Precision            0.983740           0.994595             0.986226   
2     Recall            0.970588           0.983957             0.957219   
3   F1 Score            0.977120           0.989247             0.971506   
4    ROC AUC            0.982396           0.991012             0.976194   

   SVM (After Tuning)  
0            0.992193  
1            1.000000  
2            0.970588  
3            0.985075  
4            0.998695  

Summary Analysis¶

In [115]:
# Summary of Findings
print("Logistic Regression (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_log_reg_baseline):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_log_reg_baseline):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_log_reg_baseline):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_log_reg_baseline):.4f}")
Logistic Regression (Before Tuning):
Accuracy: 0.9879
Precision: 0.9837
Recall: 0.9706
F1 Score: 0.9771
In [117]:
print("\nLogistic Regression (After Tuning):")
print(f"Accuracy: {accuracy_best_log_reg:.4f}")
print(f"Precision: {precision_best_log_reg:.4f}")
print(f"Recall: {recall_best_log_reg:.4f}")
print(f"F1 Score: {f1_best_log_reg:.4f}")
Logistic Regression (After Tuning):
Accuracy: 0.9943
Precision: 0.9946
Recall: 0.9840
F1 Score: 0.9892
In [119]:
print("\nSVM (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_svm_baseline):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_svm_baseline):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_svm_baseline):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_svm_baseline):.4f}")
SVM (Before Tuning):
Accuracy: 0.9851
Precision: 0.9862
Recall: 0.9572
F1 Score: 0.9715
In [121]:
print("\nSVM (After Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best_svm):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_best_svm):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_best_svm):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_best_svm):.4f}")
SVM (After Tuning):
Accuracy: 0.9922
Precision: 1.0000
Recall: 0.9706
F1 Score: 0.9851
In [123]:
# Feature Importance for Logistic Regression
feature_importance_lr = pd.Series(best_log_reg.coef_[0], index=X.columns).sort_values(ascending=False)
print("Feature Importance for Logistic Regression:")
print(feature_importance_lr)
Feature Importance for Logistic Regression:
Churn Reason_Long distance charges                        11.917486
Churn Reason_Competitor offered more data                 10.821918
Churn Reason_Competitor offered higher download speeds    10.429499
Churn Reason_Attitude of support person                   10.342916
Churn Reason_Competitor made better offer                 10.311438
Churn Reason_Don't know                                   10.231137
Churn Reason_Price too high                                9.995710
Churn Reason_Network reliability                           9.836814
Churn Reason_Moved                                         9.805171
Churn Reason_Lack of self-service on Website               9.552347
Churn Reason_Product dissatisfaction                       9.228050
Churn Reason_Service dissatisfaction                       9.027239
Churn Reason_Competitor had better devices                 8.845489
Churn Reason_Extra data charges                            8.545527
Churn Reason_Lack of affordable download/upload speed      7.706435
Churn Reason_Poor expertise of online support              7.513258
Churn Reason_Poor expertise of phone support               7.428125
Churn Reason_Limited range of services                     7.268097
Churn Reason_Deceased                                      6.422142
Churn Score                                                5.026159
Monthly Charges                                            1.100508
Partner_Yes                                                0.603408
Total Charges                                              0.583322
Paperless Billing_Yes                                      0.485778
Senior Citizen_Yes                                         0.455502
Payment Method_Electronic check                            0.261504
CLTV                                                       0.171309
Internet Service_Fiber optic                               0.054558
Contract_One year                                          0.030911
Streaming Movies_Yes                                       0.000000
Streaming Movies_No internet service                       0.000000
Streaming TV_No internet service                           0.000000
Tech Support_No internet service                           0.000000
Device Protection_Yes                                      0.000000
Device Protection_No internet service                      0.000000
Online Backup_No internet service                          0.000000
Online Security_No internet service                        0.000000
Internet Service_No                                        0.000000
Multiple Lines_Yes                                        -0.051372
Payment Method_Credit card (automatic)                    -0.117536
Payment Method_Mailed check                               -0.159349
Streaming TV_Yes                                          -0.163500
Online Security_Yes                                       -0.218493
Gender_Male                                               -0.253644
Tech Support_Yes                                          -0.399558
Contract_Two year                                         -0.691048
Online Backup_Yes                                         -0.703037
Dependents_Yes                                            -0.870152
Multiple Lines_No phone service                           -1.814781
Tenure Months                                             -1.954257
Phone Service_Yes                                         -3.591200
dtype: float64
In [125]:
# Permutation Importance for SVM
from sklearn.inspection import permutation_importance
perm_importance = permutation_importance(best_svm, X_test, y_test)
feature_importance_svm = pd.Series(perm_importance.importances_mean, index=X.columns).sort_values(ascending=False)
print("Feature Importance for SVM:")
print(feature_importance_svm)
Feature Importance for SVM:
Churn Score                                               0.162243
Churn Reason_Attitude of support person                   0.027963
Churn Reason_Don't know                                   0.022427
Churn Reason_Competitor offered more data                 0.021434
Churn Reason_Competitor offered higher download speeds    0.019730
Churn Reason_Competitor had better devices                0.017317
Churn Reason_Competitor made better offer                 0.016040
Churn Reason_Price too high                               0.015188
Churn Reason_Network reliability                          0.011923
Churn Reason_Extra data charges                           0.009226
Churn Reason_Lack of self-service on Website              0.008091
Churn Reason_Service dissatisfaction                      0.007807
Churn Reason_Product dissatisfaction                      0.007381
Churn Reason_Limited range of services                    0.006529
Churn Reason_Long distance charges                        0.005820
Churn Reason_Moved                                        0.004826
Churn Reason_Lack of affordable download/upload speed     0.004400
Churn Reason_Poor expertise of phone support              0.001845
Tenure Months                                             0.000852
Churn Reason_Poor expertise of online support             0.000710
Multiple Lines_Yes                                        0.000284
Device Protection_Yes                                     0.000142
Contract_One year                                         0.000142
Payment Method_Mailed check                               0.000142
Streaming Movies_Yes                                      0.000142
Streaming TV_Yes                                          0.000142
Monthly Charges                                           0.000000
Churn Reason_Deceased                                     0.000000
Total Charges                                             0.000000
CLTV                                                      0.000000
Gender_Male                                               0.000000
Tech Support_Yes                                          0.000000
Dependents_Yes                                            0.000000
Phone Service_Yes                                         0.000000
Multiple Lines_No phone service                           0.000000
Streaming TV_No internet service                          0.000000
Streaming Movies_No internet service                      0.000000
Internet Service_No                                       0.000000
Online Security_No internet service                       0.000000
Tech Support_No internet service                          0.000000
Online Backup_No internet service                         0.000000
Online Backup_Yes                                         0.000000
Device Protection_No internet service                     0.000000
Payment Method_Credit card (automatic)                    0.000000
Contract_Two year                                         0.000000
Online Security_Yes                                       0.000000
Internet Service_Fiber optic                             -0.000142
Partner_Yes                                              -0.000284
Payment Method_Electronic check                          -0.000426
Senior Citizen_Yes                                       -0.000568
Paperless Billing_Yes                                    -0.000568
dtype: float64
In [126]:
# Actionable Insights and Recommendations
print("\nActionable Insights:")
print("1. Customer Retention: Focus on improving services and offering loyalty programs.")
print("2. Targeted Marketing: Identify high-risk customers and target them with personalized campaigns.")
print("3. Service Improvement: Enhance services that are associated with higher churn.")
Actionable Insights:
1. Customer Retention: Focus on improving services and offering loyalty programs.
2. Targeted Marketing: Identify high-risk customers and target them with personalized campaigns.
3. Service Improvement: Enhance services that are associated with higher churn.
In [127]:
print("\nRecommendations:")
print("1. Model Deployment: Deploy the best-performing model for real-time churn prediction.")
print("2. Continuous Monitoring: Regularly monitor and retrain the model.")
print("3. Customer Feedback: Collect and address feedback from high-risk customers.")
Recommendations:
1. Model Deployment: Deploy the best-performing model for real-time churn prediction.
2. Continuous Monitoring: Regularly monitor and retrain the model.
3. Customer Feedback: Collect and address feedback from high-risk customers.
In [ ]: