Import required modules¶

In [1]:
# Import required libraries for data manipulation, machine learning, visualization and utility functions
import pandas as pd
from sklearn.model_selection import train_test_split, GridSearchCV, learning_curve
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, classification_report, confusion_matrix, roc_curve
import matplotlib.pyplot as plt
import seaborn as sns
import time
import numpy as np

Import dataset¶

In [2]:
# Load the telco customer churn dataset from CSV file
data = pd.read_csv('Simple_Telco_Customer_Churn.csv')

Explore the dataset¶

Get numerical and categorical data¶

In [3]:
# Clean TotalCharges column by converting string values to float
# Handle empty or space values by setting them to None
newTotalCharges = []
for i in data['TotalCharges']:
    try:
        newValue = float(i)
    except:
        newValue = None
    finally:
        newTotalCharges.append(newValue)
    
data['TotalCharges'] = newTotalCharges
data = data.dropna(subset=['TotalCharges'])
In [4]:
# Separate numerical and categorical columns for preprocessing
all_cols = data.columns
num_cols = list(data._get_numeric_data().columns)
cat_cols = list(set(all_cols) - set(num_cols))

# Remove ID and target columns from categorical columns list
cat_cols.remove('CustomerID')
cat_cols.remove('Churn')

print(cat_cols)
print(num_cols)
['StreamingTV', 'gender', 'Contract', 'OnlineBackup', 'PaperlessBilling', 'PaymentMethod', 'Partner', 'StreamingMovies', 'MultipleLines', 'Dependents', 'TechSupport', 'InternetService', 'PhoneService', 'OnlineSecurity', 'DeviceProtection']
['SeniorCitizen', 'tenure', 'MonthlyCharges', 'TotalCharges']

Display dateset information¶

In [5]:
# Display dataset information and basic statistics
print("Dataset Info:")
print(data.info())
print("\nBasic Statistics:")
print(data.describe())
print("\nClass Distribution:")
print(data['Churn'].value_counts(normalize=True))

# Create correlation heatmap for numerical features
plt.figure(figsize=(15,8))
sns.heatmap(data[num_cols].corr(), annot=True, fmt='0.2f', cmap='YlOrRd')
plt.title('Feature Correlations')
Dataset Info:
<class 'pandas.core.frame.DataFrame'>
Index: 7032 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   CustomerID        7032 non-null   object 
 1   gender            7032 non-null   object 
 2   SeniorCitizen     7032 non-null   int64  
 3   Partner           7032 non-null   object 
 4   Dependents        7032 non-null   object 
 5   tenure            7032 non-null   int64  
 6   PhoneService      7032 non-null   object 
 7   MultipleLines     7032 non-null   object 
 8   InternetService   7032 non-null   object 
 9   OnlineSecurity    7032 non-null   object 
 10  OnlineBackup      7032 non-null   object 
 11  DeviceProtection  7032 non-null   object 
 12  TechSupport       7032 non-null   object 
 13  StreamingTV       7032 non-null   object 
 14  StreamingMovies   7032 non-null   object 
 15  Contract          7032 non-null   object 
 16  PaperlessBilling  7032 non-null   object 
 17  PaymentMethod     7032 non-null   object 
 18  MonthlyCharges    7032 non-null   float64
 19  TotalCharges      7032 non-null   float64
 20  Churn             7032 non-null   object 
dtypes: float64(2), int64(2), object(17)
memory usage: 1.2+ MB
None

Basic Statistics:
       SeniorCitizen       tenure  MonthlyCharges  TotalCharges
count    7032.000000  7032.000000     7032.000000   7032.000000
mean        0.162400    32.421786       64.798208   2283.300441
std         0.368844    24.545260       30.085974   2266.771362
min         0.000000     1.000000       18.250000     18.800000
25%         0.000000     9.000000       35.587500    401.450000
50%         0.000000    29.000000       70.350000   1397.475000
75%         0.000000    55.000000       89.862500   3794.737500
max         1.000000    72.000000      118.750000   8684.800000

Class Distribution:
Churn
No     0.734215
Yes    0.265785
Name: proportion, dtype: float64
Out[5]:
Text(0.5, 1.0, 'Feature Correlations')
No description has been provided for this image

Processing the data¶

Drop rows with missing values¶

In [6]:
# Remove CustomerID as it's unique for each row and not useful for prediction
data = data.drop('CustomerID', axis=1)

Encode categorical data¶

In [7]:
# Convert categorical variables to dummy variables
encodedData = pd.get_dummies(data[cat_cols])
data = pd.concat([data[num_cols], data['Churn'], encodedData], axis=1)

# Convert Churn to binary values (0/1)
data['Churn'] = (data['Churn'] == 'Yes').astype(int)

Split features and target¶

In [8]:
# Separate features (X) and target variable (Y)
X = data.drop('Churn', axis=1)  # Features
Y = data['Churn']  # Target

Split the data¶

In [9]:
# Split data into training (70%) and testing (30%) sets
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.3, random_state=42)

Scale features¶

In [10]:
# Standardize features to have zero mean and unit variance
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

Train data using logistic regression and SVM¶

In [11]:
# Train Logistic Regression model and measure training time
startTime = time.time()
lr_model = LogisticRegression(max_iter=100000)
lr_model.fit(X_train, Y_train)
lr_predictions = lr_model.predict(X_test)
endTime = time.time()
print(f"Linear Regression took {round(endTime-startTime,2)} seconds to finish")
Linear Regression took 0.02 seconds to finish
In [12]:
# Train Support Vector Machine model and measure training time
startTime = time.time()
svm_model = SVC(probability=True, max_iter=-1)
svm_model.fit(X_train, Y_train)
svm_predictions = svm_model.predict(X_test)
endTime = time.time()
print(f"Support Vector Machine took {round(endTime-startTime,2)} seconds to finish")
Support Vector Machine took 2.29 seconds to finish

Model evaluation¶

In [13]:
# Function to evaluate model performance using various metrics
def evaluate_model(y_test, y_pred, y_proba, model_name):
    averageMode = "macro"
    posLabelMode = "Pos"
    multiClassMode = "ovo"

    # Calculate performance metrics
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred, average=averageMode)
    recall = recall_score(y_test, y_pred, average=averageMode)
    f1 = f1_score(y_test, y_pred, average=averageMode)
    auc = roc_auc_score(y_test, y_proba, multi_class=multiClassMode)

    # Create and plot confusion matrix
    plt.figure(figsize=(6, 6))
    cm = confusion_matrix(y_test, y_pred)
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
    plt.title(f'Confusion Matrix - {model_name}')
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    
    # Create and plot ROC curve
    plt.figure(figsize=(6, 6))
    fpr, tpr, _ = roc_curve(y_test, y_proba, pos_label=1)
    plt.plot(fpr, tpr)
    plt.plot([0, 1], [0, 1], 'k--')
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title(f'ROC Curve - {model_name}')

    return accuracy, precision, recall, f1, auc

Logistic regression¶

Confusion metrics and ROC curve¶

In [14]:
# Evaluate Logistic Regression model
lr_proba = lr_model.predict_proba(X_test)[:,1]
lr_metrics = evaluate_model(Y_test, lr_predictions, lr_proba, "Logistic Regression")
No description has been provided for this image
No description has been provided for this image

SVM¶

Confusion metrics and ROC curve¶

In [15]:
# Evaluate SVM model
svm_proba = svm_model.predict_proba(X_test)[:,1]
svm_metrics = evaluate_model(Y_test, svm_predictions, svm_proba, "SVM")
No description has been provided for this image
No description has been provided for this image

Metrics information¶

In [16]:
# Function to display model metrics in formatted output
def displayResults(metrics):
    decimalPlaces = 4
    print(f'''
    Accuracy:\t{round(metrics[0], decimalPlaces)},
    Precision:\t{round(metrics[1], decimalPlaces)},
    Recall:\t{round(metrics[2], decimalPlaces)},
    F1 Score:\t{round(metrics[3], decimalPlaces)},
    AUC:\t{round(metrics[4], decimalPlaces)}
    ''')
In [17]:
# Display metrics for both models
print('Logistic Regression Metrics:')
displayResults(lr_metrics)
print('SVM Metrics: Accuracy:')
displayResults(svm_metrics)
Logistic Regression Metrics:

    Accuracy:	0.7953,
    Precision:	0.7388,
    Recall:	0.7128,
    F1 Score:	0.7233,
    AUC:	0.8392
    
SVM Metrics: Accuracy:

    Accuracy:	0.7938,
    Precision:	0.7395,
    Recall:	0.697,
    F1 Score:	0.712,
    AUC:	0.8013
    

Optimisations and hyperprameter tuning¶

Optimisations for SVM and LR¶

In [18]:
# Perform grid search for SVM hyperparameter optimization
startTime = time.time()
param_grid = {'C': [0.1, 1, 10], 'gamma': ['scale', 'auto', 0.1], 'kernel': ['rbf','linear']}
SVMgrid = GridSearchCV(svm_model, param_grid, refit=True, verbose=1)
SVMgrid.fit(X_train, Y_train)
endTime = time.time()
print(f"SVM grid search optimisation took {round(endTime-startTime,2)} seconds to finish\n")

# Perform grid search for Logistic Regression hyperparameter optimization
startTime = time.time()
param_grid = {'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'], 'solver': ['liblinear']}
LRgrid = GridSearchCV(lr_model, param_grid, refit=True, verbose=1)
LRgrid.fit(X_train, Y_train)
endTime = time.time()
print(f"LR grid search optimisation took {round(endTime-startTime,2)} seconds to finish")
Fitting 5 folds for each of 18 candidates, totalling 90 fits
SVM grid search optimisation took 223.48 seconds to finish

Fitting 5 folds for each of 6 candidates, totalling 30 fits
LR grid search optimisation took 2.67 seconds to finish

Tuned SVM model results¶

In [20]:
# Evaluate optimized SVM model
print("Best Parameters for SVM:", SVMgrid.best_params_)
best_svm = SVMgrid.best_estimator_
y_pred_best_svm = best_svm.predict(X_test)

print("Tuned SVM Performance:")
bestSVM = best_svm.predict_proba(X_test)[:, 1]
tuned_svm_metrics = evaluate_model(Y_test, y_pred_best_svm, bestSVM, "Tuned SVM Performance")

print('Tuned SVM Metrics:')
displayResults(tuned_svm_metrics)
Best Parameters for SVM: {'C': 10, 'gamma': 'scale', 'kernel': 'linear'}
Tuned SVM Performance:
Tuned SVM Metrics:

    Accuracy:	0.7905,
    Precision:	0.7319,
    Recall:	0.7072,
    F1 Score:	0.7173,
    AUC:	0.8237
    
No description has been provided for this image
No description has been provided for this image

Tuned LR model results¶

In [21]:
# Evaluate optimized Logistic Regression model
print("Best Parameters for LR:", LRgrid.best_params_)
best_lr = LRgrid.best_estimator_
y_pred_best_lr = best_lr.predict(X_test)

print("Tuned LR Performance:")
bestLR = best_lr.predict_proba(X_test)[:, 1]
tuned_lr_metrics = evaluate_model(Y_test, y_pred_best_lr, bestLR, "Tuned LR Performance")

print('Tuned LR Metrics:')
displayResults(tuned_lr_metrics)
Best Parameters for LR: {'C': 1, 'penalty': 'l1', 'solver': 'liblinear'}
Tuned LR Performance:
Tuned LR Metrics:

    Accuracy:	0.7962,
    Precision:	0.7404,
    Recall:	0.7123,
    F1 Score:	0.7235,
    AUC:	0.8391
    
No description has been provided for this image
No description has been provided for this image

Feature importance¶

In [22]:
# Function to analyze and visualize feature importance
def analyze_feature_importance(model, feature_names, model_name):
    # Extract feature importance based on model type
    if model_name == 'Logistic Regression':
        importance = pd.DataFrame({
            'Feature': feature_names,
            'Importance': abs(model.best_estimator_.coef_[0])
        })
    else:  # SVM
        importance = pd.DataFrame({
            'Feature': feature_names,
            'Importance': abs(model.best_estimator_.coef_[0]) if model.best_estimator_.kernel == 'linear'
                         else [np.nan] * len(feature_names)
        })
    
    importance = importance.sort_values('Importance', ascending=False)
    
    # Plot top 10 most important features
    plt.figure(figsize=(9, 6))
    sns.barplot(data=importance.head(10), x='Importance', y='Feature')
    plt.title(f'Top 10 Feature Importance - {model_name}')
    
    return importance
In [23]:
# Analyze feature importance for both models
lr_importance = analyze_feature_importance(SVMgrid, X.columns, "SVM")
lr_importance = analyze_feature_importance(LRgrid, X.columns, "Logistic Regression")
No description has been provided for this image
No description has been provided for this image

Model comparison¶

In [24]:
# Function to compare and visualize model performances
def compare_models_performance(models_metrics, model_names, fit=None):
    metrics_df = pd.DataFrame(models_metrics, 
                            columns=['Accuracy', 'Precision', 'Recall', 'F1', 'AUC'],
                            index=model_names)

    # Create bar plot comparing model performances
    plt.figure(figsize=(12, 6))
    metrics_df.plot(kind='bar', width=0.8)
    plt.xlabel('Models')
    plt.ylabel('Score')
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    plt.tight_layout()
    if fit:
        plt.title('Zoomed in Model Performance Comparison')
        min = np.min(models_metrics)
        max = np.max(models_metrics)
        plt.ylim([min*0.995, max*1.005])
    else:
        plt.title('Model Performance Comparison')
        plt.ylim([0.5, 1])
In [25]:
# Compare performance of all models
models_metrics = [lr_metrics, tuned_lr_metrics, svm_metrics, tuned_svm_metrics]
model_names = ['Base LR', 'Tuned LR', 'Base SVM', 'Tuned SVM']
compare_models_performance(models_metrics, model_names)
compare_models_performance(models_metrics, model_names, True)
<Figure size 1200x600 with 0 Axes>
No description has been provided for this image
<Figure size 1200x600 with 0 Axes>
No description has been provided for this image
In [ ]: