Step 1: Import Libraries¶

In [2]:
# Import essential libraries for data manipulation, preprocessing, modeling, evaluation, and visualization
import pandas as pd  # For data manipulation and analysis
import numpy as np  # For numerical operations
from sklearn.model_selection import train_test_split, GridSearchCV  # For data splitting and hyperparameter tuning
from sklearn.linear_model import LogisticRegression  # Logistic Regression model
from sklearn.preprocessing import StandardScaler, LabelEncoder  # For feature scaling and encoding categorical variables
from sklearn.pipeline import Pipeline  # To create machine learning pipelines
from sklearn.metrics import (  # For evaluating model performance
    accuracy_score, 
    classification_report, 
    confusion_matrix, 
    roc_curve, 
    auc
)
from sklearn.svm import SVC  # Support Vector Classifier
import seaborn as sns  # For visualization
import matplotlib.pyplot as plt  # For plotting graphs

Step 2: Load and Explore Data¶

In [4]:
# Load the dataset
data = pd.read_csv('data/TelcoCustomerChurn.csv')  # Replace with the path to your dataset

# Display the first few rows to understand the structure of the data
print(data.head())

# Display column data types and check for null values
print(data.info())
   customerID  gender  SeniorCitizen Partner Dependents  tenure PhoneService  \
0  7590-VHVEG  Female              0     Yes         No       1           No   
1  5575-GNVDE    Male              0      No         No      34          Yes   
2  3668-QPYBK    Male              0      No         No       2          Yes   
3  7795-CFOCW    Male              0      No         No      45           No   
4  9237-HQITU  Female              0      No         No       2          Yes   

      MultipleLines InternetService OnlineSecurity  ... DeviceProtection  \
0  No phone service             DSL             No  ...               No   
1                No             DSL            Yes  ...              Yes   
2                No             DSL            Yes  ...               No   
3  No phone service             DSL            Yes  ...              Yes   
4                No     Fiber optic             No  ...               No   

  TechSupport StreamingTV StreamingMovies        Contract PaperlessBilling  \
0          No          No              No  Month-to-month              Yes   
1          No          No              No        One year               No   
2          No          No              No  Month-to-month              Yes   
3         Yes          No              No        One year               No   
4          No          No              No  Month-to-month              Yes   

               PaymentMethod MonthlyCharges  TotalCharges Churn  
0           Electronic check          29.85         29.85    No  
1               Mailed check          56.95        1889.5    No  
2               Mailed check          53.85        108.15   Yes  
3  Bank transfer (automatic)          42.30       1840.75    No  
4           Electronic check          70.70        151.65   Yes  

[5 rows x 21 columns]
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB
None

Step 3: Data Preprocessing¶

In [6]:
# Replace empty strings with NaN and drop rows with missing values
data.replace(' ', np.nan, inplace=True)
data.dropna(inplace=True)

# Encode categorical variables to numeric using LabelEncoder
encoder = LabelEncoder()
categorical_columns = ['gender', 'Contract', 'PaymentMethod', 'InternetService']
for col in categorical_columns:
    data[col] = encoder.fit_transform(data[col])

# Replace specific categorical values with numeric equivalents
data = data.replace({'Yes': 1, 'No': 0, 'No internet service': 2, 'No phone service': 2})

# Split the dataset into features (X) and target (y)
X = data.drop(columns=['Churn', 'customerID', 'gender', 'PhoneService', 'MultipleLines', 'PaperlessBilling'])
y = data['Churn']

# Split data into training and testing sets (80% train, 20% test)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Scale features to have mean 0 and variance 1 for better model performance
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
C:\Users\rhys.davies\AppData\Local\Temp\ipykernel_33636\2669118501.py:12: FutureWarning: Downcasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
  data = data.replace({'Yes': 1, 'No': 0, 'No internet service': 2, 'No phone service': 2})

Step 4: Train Model (LR)¶

In [8]:
# Initialize and train the logistic regression model
lr_model = LogisticRegression(random_state=42)
lr_model.fit(X_train, y_train)
Out[8]:
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)

Step 5: Make Predictions (LR)¶

In [10]:
# Predict the target variable using the test set
y_pred = lr_model.predict(X_test)

# Predict probabilities for ROC analysis
y_prob = lr_model.predict_proba(X_test)[:, 1]

Step 6: Evaluate the Model (LR)¶

In [12]:
# Print classification metrics for model performance
print("Classification Report (LR):\n", classification_report(y_test, y_pred))

# Create a confusion matrix heatmap
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(6, 6))
sns.heatmap(
    cm, 
    annot=True, 
    fmt='d', 
    cmap='Blues', 
    xticklabels=['Predicted No', 'Predicted Yes'], 
    yticklabels=['Actual No', 'Actual Yes']
)
plt.title('Confusion Matrix Heatmap (LR)')
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()

# Plot the ROC curve
fpr, tpr, _ = roc_curve(y_test, y_prob)
roc_auc = auc(fpr, tpr)

plt.figure()
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', linestyle='--')  # Baseline for random guessing
plt.xlabel('False Positive Rate (1-Specificity)')
plt.ylabel('True Positive Rate (Sensitivity)')
plt.title('Receiver Operating Characteristic (LR)')
plt.legend(loc='lower right')
plt.show()
Classification Report (LR):
               precision    recall  f1-score   support

           0       0.83      0.88      0.85      1033
           1       0.60      0.50      0.55       374

    accuracy                           0.78      1407
   macro avg       0.72      0.69      0.70      1407
weighted avg       0.77      0.78      0.77      1407

No description has been provided for this image
No description has been provided for this image

Step 7: Hyperparameter Tuning (LR)¶

In [14]:
# Define a grid of hyperparameters for tuning the Logistic Regression model
param_grid = [
    {
        'solver': ['lbfgs'], 
        'penalty': ['l2'], 
        'C': [0.001, 0.01, 0.1, 1, 10, 100],
        'max_iter': [100, 200, 300]
    },
    {
        'solver': ['liblinear'], 
        'penalty': ['l1', 'l2'], 
        'C': [0.001, 0.01, 0.1, 1, 10, 100],
        'max_iter': [100, 200, 300]
    }
]

# Initialize the logistic regression model
lr_model = LogisticRegression(random_state=42)

# Set up GridSearchCV for hyperparameter tuning
grid_search = GridSearchCV(
    estimator=lr_model, 
    param_grid=param_grid, 
    cv=5, 
    scoring='accuracy', 
    verbose=1, 
    n_jobs=-1
)

# Perform grid search on the training data
grid_search.fit(X_train, y_train)

# Print the best parameters and corresponding score
print("Best Parameters:", grid_search.best_params_)
print("Best Score:", grid_search.best_score_)

# Retrain the model with the best parameters
best_lr_model = grid_search.best_estimator_
y_pred = best_lr_model.predict(X_test)
y_prob = best_lr_model.predict_proba(X_test)[:, 1]
Fitting 5 folds for each of 54 candidates, totalling 270 fits
Best Parameters: {'C': 0.1, 'max_iter': 100, 'penalty': 'l2', 'solver': 'lbfgs'}
Best Score: 0.7998222222222222

Step 8: Evaluate Hyperparameter Tuning Results (LR)¶

In [16]:
# Evaluate the model's performance with the tuned parameters
print("Classification Report (LR):\n", classification_report(y_test, y_pred))
Classification Report (LR):
               precision    recall  f1-score   support

           0       0.82      0.88      0.85      1033
           1       0.60      0.48      0.53       374

    accuracy                           0.78      1407
   macro avg       0.71      0.68      0.69      1407
weighted avg       0.76      0.78      0.77      1407

Step 9: Train Model (SVM)¶

In [18]:
# Import and initialize the Support Vector Machine (SVM) model with an RBF kernel
# The `probability=True` parameter enables probability estimates (used later for ROC curve)
# `random_state=42` ensures reproducibility
svm_model = SVC(probability=True, random_state=42, kernel='rbf')

# Train the SVM model on the training data
svm_model.fit(X_train, y_train)
Out[18]:
SVC(probability=True, 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.
SVC(probability=True, random_state=42)

Step 10: Make Predictions (SVM)¶

In [20]:
# Use the trained SVM model to make predictions on the test data
y_pred = svm_model.predict(X_test)  # Class predictions for the test set

# Generate probabilities for the positive class (Churn = 1)
# These probabilities are used for performance evaluation like the ROC curve
y_prob = svm_model.predict_proba(X_test)[:, 1]  # Get probabilities for the positive class (index 1)

Step 11: Evaluate the Model (SVM)¶

In [22]:
# Print a classification report to evaluate precision, recall, F1 score, and accuracy
print("Classification Report (SVM):\n", classification_report(y_test, y_pred))

# Generate a confusion matrix to visualize model performance
cm = confusion_matrix(y_test, y_pred)

# Plot the confusion matrix as a heatmap for better visualization
plt.figure(figsize=(6, 6))
sns.heatmap(
    cm, 
    annot=True, 
    fmt='d', 
    cmap='Reds', 
    xticklabels=['Predicted No', 'Predicted Yes'], 
    yticklabels=['Actual No', 'Actual Yes']
)
plt.title('Confusion Matrix Heatmap (SVM)')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()

# Generate an ROC curve to evaluate the trade-off between sensitivity and specificity
fpr, tpr, _ = roc_curve(y_test, y_prob)  # False Positive Rate and True Positive Rate
roc_auc = auc(fpr, tpr)  # Compute the Area Under the Curve (AUC)

# Plot the ROC curve
plt.figure()
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', linestyle='--')  # Diagonal line represents random guessing
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (SVM)')
plt.legend(loc='lower right')
plt.show()
Classification Report (SVM):
               precision    recall  f1-score   support

           0       0.82      0.92      0.86      1033
           1       0.65      0.44      0.52       374

    accuracy                           0.79      1407
   macro avg       0.73      0.68      0.69      1407
weighted avg       0.77      0.79      0.77      1407

No description has been provided for this image
No description has been provided for this image

Step 12: SVM Hyperparameter Tuning (SVM)¶

In [24]:
# Define a parameter grid for tuning hyperparameters
# C controls the regularization strength, and kernel specifies the type of SVM (RBF here)
# The probability=True parameter is required for ROC curve evaluation
param_grid = {
    'C': [0.001, 0.01, 0.1, 1, 10, 100],
    'kernel': ['linear', 'rbf', 'poly', 'sigmoid'],
    'gamma': ['scale', 'auto']  # Kernel coefficient for RBF, poly, and sigmoid
}

# Initialize the SVM model
svm_model = SVC(probability=True, random_state=42)

# Set up GridSearchCV to find the best combination of hyperparameters
grid_search = GridSearchCV(
    estimator=svm_model, 
    param_grid=param_grid, 
    cv=5,  # 5-fold cross-validation
    scoring='accuracy', 
    verbose=1,  # Displays progress during search
    n_jobs=-1  # Utilizes all available CPU cores
)

# Perform grid search on the training data
grid_search.fit(X_train, y_train)

# Display the best parameters and corresponding accuracy score
print("Best Parameters:", grid_search.best_params_)
print("Best Score:", grid_search.best_score_)

# Use the best parameters to initialize a new SVM model
best_svm_model = SVC(
    C=grid_search.best_params_['C'], 
    kernel=grid_search.best_params_['kernel'], 
    gamma=grid_search.best_params_['gamma'], 
    probability=True, 
    random_state=42
)

# Train the optimized model on the training data
best_svm_model.fit(X_train, y_train)

# Make predictions using the optimized model
y_pred = best_svm_model.predict(X_test)  # Class predictions
y_prob = best_svm_model.predict_proba(X_test)[:, 1]  # Probabilities for positive class
Fitting 5 folds for each of 48 candidates, totalling 240 fits
Best Parameters: {'C': 1, 'gamma': 'scale', 'kernel': 'rbf'}
Best Score: 0.7955555555555556

Step 13: Evaluate Hyperparameter Tuning Results (LR)¶

In [26]:
# Evaluate the optimized model's performance
print("Classification Report (SVM):\n", classification_report(y_test, y_pred))
Classification Report (SVM):
               precision    recall  f1-score   support

           0       0.82      0.92      0.86      1033
           1       0.65      0.44      0.52       374

    accuracy                           0.79      1407
   macro avg       0.73      0.68      0.69      1407
weighted avg       0.77      0.79      0.77      1407

In [ ]: