Install required libararies¶

Use Jupyter Notebook magic command %pip to run pip install commands within notebook¶
In [ ]:
%pip install seaborn
%pip install pandas
%pip install numpy
%pip install scikit-learn
%pip install matplotlib
%pip install openpyxl
%pip install statsmodels

Import relevant libraries¶

Suppress warnings to prevent any warnings cluttering workbook.
Use warnings when developing to de-bug code

In [ ]:
import os
import matplotlib.pyplot as plt
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn import metrics
from sklearn.metrics import confusion_matrix, classification_report, precision_recall_curve, recall_score
from sklearn.metrics import roc_curve,roc_auc_score, RocCurveDisplay, recall_score, confusion_matrix, precision_score, f1_score, accuracy_score, classification_report
import seaborn as sns
import numpy as np
import pandas as pd
from scipy.stats import zscore, chi2_contingency
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant


import warnings
warnings.filterwarnings("ignore")
Get Dataset¶

Using the pandas library to read the csv file into a dataframe

The .head(), .tail() functions show the top and bottom 5 rows of the dataset

.info() and .describe() functions summarise the dataframe
- .info() shows the data types for each column
- .describe() will generate descriptive analysis for columns that contain integer values

'Customer ID' field is removed from the dataframe as it does not give any useful information

In [ ]:
# Declare variable for filename
FileName = 'Telco-Customer-Churn.csv'

# Use pandas library to read csv file
df = pd.read_csv(FileName)

# Display the top 5 rows of the dataset (.head() function defaults to 5 rows - to increase row count use .head(n))
df.head()
# Display last 5 rows
df.tail()

# Using .info() to give a brief summary of the dataset
df.info()
# Using the .describe() function to display the statistical summary from the dataframe 
#   - only applies to the column that are classed as int64
#      - 'T' transposes the data into a more readable format
df.describe().T

# Customer ID field is not needed for the analysis and this can be dropped from the dataframe
df = df.drop('customerID', axis=1)

# Display the columns from the dataframe
print(df.columns)

Pre-process Data¶

'TotalCharges' column is shown as an 'object' type. This data needs to be converted to integars and any null values handled

Use pandas to convert 'TotalCharges' to numerical values
.isnull().sum() summarises the dataframe to show any null values present in any column

When analysing the NaN values in 'TotalCharges' is was shown that the tenure column is 0.
This suggest that they are new customers and haven't had any total charges. These can be removed as the total charges can't be calculated yet, thus unable to be used in modelling

In [ ]:
# Convert 'TotalCharges' column to numberic values
# Any values that cannot be converted will be converted to NaN (Not a Number) using error='coerce'
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')

# Summarise the df to show null values in columns
print(df.isnull().sum())

# 'TotalCharges' shows 11 null values
# Display the rows where 'TotalCharges' is NaN
df[np.isnan(df['TotalCharges'])]

# Drop rows where tenure is eaqual to 0
df.drop(labels=df[df['tenure'] == 0].index, axis=0, inplace=True)

Data Analysis¶

Create visualisations that compare different features to the churn column.

Churn By Gender

In [ ]:
# Create a new dataframe for churn counts by gender
churn_by_gender = df.groupby(['gender', 'Churn']).size().unstack()

# Calculate the proportion of churn by gender
churn_proportion = churn_by_gender.div(churn_by_gender.sum(axis=1), axis=0)

# Plot the proportions
churn_proportion.plot(kind='bar', stacked=True, color=['#1f77b4', '#ff7f0e'], figsize=(10, 6))
plt.title('Proportion of Customer Churn by Gender')
plt.xlabel('Gender')
plt.ylabel('Proportion')
plt.legend(['No Churn', 'Churn'], title='Churn')
plt.xticks(rotation=0)
plt.show()

Churn by contract Type

In [ ]:
# Create a new dataframe for churn counts by contract type
churn_by_contract = df.groupby(['Contract', 'Churn']).size().unstack()

# Calculate the proportion of churn by contract type
churn_proportion = churn_by_contract.div(churn_by_contract.sum(axis=1), axis=0)

# Plot the proportions
churn_proportion.plot(kind='bar', stacked=True, color=['#1f77b4', '#ff7f0e'], figsize=(10, 6))
plt.title('Proportion of Customer Churn by Contract Type')
plt.xlabel('Contract Type')
plt.ylabel('Proportion')
plt.legend(['No Churn', 'Churn'], title='Churn')
plt.xticks(rotation=0)
plt.show()

Distibution of monthly charges

In [ ]:
# Plot the distribution of MonthlyCharges using a histogram
plt.figure(figsize=(10, 6))
plt.hist(df['MonthlyCharges'], bins=30, color='skyblue', edgecolor='black')
plt.title('Distribution of Monthly Charges')
plt.xlabel('Monthly Charges ($)')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()

Distribution of monthly charges for churn customers

In [ ]:
# Filter the dataframe for customers who have churned
churned_customers = df[df['Churn'] == 'Yes']

# Plot the distribution of MonthlyCharges for customers who have churned using a histogram
plt.figure(figsize=(10, 6))
plt.hist(churned_customers['MonthlyCharges'], bins=30, color='red', edgecolor='black')
plt.title('Distribution of Monthly Charges for Customers Who Churned')
plt.xlabel('Monthly Charges ($)')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()

Outliers¶

Using Z-scores helps in understanding how individual data points relate to the mean of the dataset, making it easier to detect outliers and compare values across different datasets.

Z-scores beyond ±3 are typically considered outliers.

In [ ]:
# Define numerical columns
numericalColumns = df.select_dtypes(include=['number']).columns
# Drop 'SeniorCitizen'
numericalColumns = numericalColumns.drop('SeniorCitizen')

# Calculate Z-scores
z_scores = df[numericalColumns].apply(zscore).abs()

# Calcluate Z-scores that are +/- 3
outliers = (z_scores > 3).any(axis=1)
print(z_scores)

# Show any outliers present within the data
if outliers.any():
    print(outliers.sum())
else:
    print('No Outliers')

Chi-Square Test¶

Apply Chi-Square test to the data

Use ['Contract', 'MultipleLines', 'InternetService', 'PaymentMethod'] columns to give the best insights

Show the significance of association at the confidence level of 0.05

In [ ]:
# Function to return the values from the chi suqared test
def chiSquareTest(CategoryOne,CategoryTwo):
    contingency_table = pd.crosstab(CategoryOne,CategoryTwo)
    chi2, p, dof, expected = chi2_contingency(contingency_table)
    return chi2,dof,p

# Dataframe for the results to be inserted into
chiSquareTestResults = pd.DataFrame(columns=['Column','ChiSquare Stat','Degrees of Freedom','p-value','Association'])

# Loop through columns of interest and send churn and column to function
for column in ['Contract', 'MultipleLines', 'InternetService', 'PaymentMethod']:
    chi2,dof,p = chiSquareTest(df['Churn'], df[column])
    # Test the P-Value for confidence level
    if p < 0.05:
        association = f'significant association between churn and {column}'
    else:
        association = f'no significant association between churn and {column}'
    # Create row showing results of the test for each column
    chiSquare = pd.DataFrame([{'Column': column,
                'ChiSquare Stat': f'{chi2:.2f}', 
                'Degrees of Freedom': f'{dof}', 
                'p-value': f'{p:.4f}',
                'Association':association}])
    # Add row to dataframe
    chiSquareTestResults = pd.concat([chiSquareTestResults,chiSquare])

# Print the results   
print(chiSquareTestResults)

Enconding¶

Categorical columns with significance to be encoded using on-hot encoding technique

Use sparse_output = False to make data easier in a dataframe
Use drop = if binary to avoid multicollinearity to avoid the dummy variable trap, where one feature can be perfectly predicted from the others

Create dataframe of the encoded values

In [ ]:
# Define the columns to encode
columns = ['Contract', 'MultipleLines', 'InternetService', 'PaymentMethod']

# Initiate the OneHotEncoder with specified parameters - drop if_binary to avoid multicollinearity
encoder = OneHotEncoder(sparse_output=False, drop='if_binary')

# Fit and transform the data
CatDF = encoder.fit_transform(df[columns])
EncodedCatColumns = encoder.get_feature_names_out(columns)

# Create dataframe of categorical columns encoded
CategoricalEncodedColumns = pd.DataFrame(CatDF, columns=EncodedCatColumns, index=df.index)

print(CategoricalEncodedColumns)

Remaining columns that have a datatype of object - encode using LabelEncoder()
Concatenate categorical nd numerical encoded dataframes to create one dataframe

In [ ]:
# Function to encode columns that have datatype of object
def ConvertObjectToBinary(data):
    if data.dtype=='object':
        data = LabelEncoder().fit_transform(data)
    return data

# Apply function to dataframe
NumericalEncodedDF = df.apply(lambda x: ConvertObjectToBinary(x))

# Join categorical and numerical encoded columns into one dataframe
dfEncoded = pd.concat([CategoricalEncodedColumns, NumericalEncodedDF], axis=1)

print(dfEncoded)

Variance Inflation Factor¶

use categorical encoded columns that have a postive correlation to the churn value to calculate the VIF value

In [ ]:
# Find categorical columns to use for VIF analysis 
print(dfEncoded.corr()['Churn'].sort_values(ascending = False))

# Defining the columns from the correlation 
predictors = dfEncoded[['Contract_Month-to-month', 'InternetService_Fiber optic', 'MultipleLines_Yes',
                         'PaymentMethod_Electronic check', 'MonthlyCharges', 'PaperlessBilling']]

# Add constant for VIF calculation
X = add_constant(predictors)

# Calculate VIF for each variable
vif_data = pd.DataFrame()
vif_data['Feature'] = X.columns
vif_data['VIF'] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
print(vif_data)

Train Test dataset split¶

using the numerical columns fit the dataseet using the standard scaler
split the dataset 70/30

Isolate churn column and drop from dataset

In [ ]:
# Metric score function for class report and conf matrix
def metrics_score(actual, predicted):
    cm = confusion_matrix(actual,predicted)
    print(classification_report(actual,predicted))
    plt.figure(figsize=(8,5))
    sns.heatmap(cm,annot=True,fmt='.2f',xticklabels=['No Churn','Churn'],yticklabels=['No Churn','Churn'])
    plt.ylabel('Actual')
    plt.xlabel('Predicted')
    plt.show()

#numerical columns
numerical_columns = ['tenure', 'MonthlyCharges', 'TotalCharges']
sc = StandardScaler()
# fit the data for the model
dfEncoded[numerical_columns] = sc.fit_transform(dfEncoded[numerical_columns])

# create the x and y columns
X = dfEncoded.drop(columns = ['Churn'])
y = dfEncoded['Churn'].values

#train test split
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.3, random_state=45)

# train dataset into dataframe
X_train_scaled = sc.fit_transform(X_train)
X_train_scaled = pd.DataFrame(X_train_scaled,columns = X.columns)

#test dataset into dataframe
X_test_scaled = sc.transform(X_test)
X_test_scaled = pd.DataFrame(X_test_scaled,columns = X.columns)

Logistic Regression Model¶

instigate the lg model and fit with the training subset

send the data to the metric score function

In [ ]:
# LR model function
lg = LogisticRegression()
#fit data to model
lg.fit(X_train_scaled,y_train)
#predict training dataset
YPredTrainLG = lg.predict(X_train_scaled)
#predict testing dataset
YPredTestLG = lg.predict(X_test_scaled)

#send values to metric score function
metrics_score(y_train,YPredTrainLG)
metrics_score(y_test,YPredTestLG)

SVM model¶

instigate the svm model and fit with the training subset

send the data to the metric score function

In [ ]:
#possible kernels to use
#kernel = ['linear', 'poly', 'rbf', 'sigmoid']

#SVM model function
clf=SVC(kernel='linear')
#fit the data
clf.fit(X_train_scaled,y_train)
#predict the training dataset
YPredTrainSVC = clf.predict(X_train_scaled)
#predict testing dataset
YPredTestSVC = clf.predict(X_test_scaled)

#send values to metric score function
metrics_score(y_train,YPredTrainSVC)
metrics_score(y_test,YPredTestSVC)

hyperparameter tuning¶

use param grid to specify with parameters to test

find the best params and esitmators

In [ ]:
#define the param grid
param_grid = { 
    'C': [0.01, 0.1, 1, 10, 100],
    'penalty': ['l1', 'l2', 'elasticnet', 'none'], 
    'solver': ['newton-cg', 'lbfgs', 'liblinear', 'sag', 'saga']
}

#use gridsearchcv for the paramters
grid_search = GridSearchCV(estimator=lg, param_grid=param_grid, cv=5, scoring='accuracy')
#fit the data
grid_search.fit(X_train_scaled, y_train)

#declare the best params and model
best_params = grid_search.best_params_
best_model = grid_search.best_estimator_

print("Best Hyperparameters:", best_params)

show the accuracy and class report for LG hyperparamter tuning

In [ ]:
#predict the values
y_pred = best_model.predict(X_test_scaled)

# Evaluate the model
accuracy = accuracy_score(y_test, y_pred)
report = classification_report(y_test, y_pred)

print(f'Accuracy: {accuracy}')
print('Classification Report:')
print(report)

SVM hyperparamter tuning

In [ ]:
# Define the model
svc_model = SVC()

# Define the hyperparameters to tune
param_grid = {
    'C': [0.1, 1, 10],
    'kernel': ['linear', 'poly', 'rbf', 'sigmoid'],
    'gamma': ['scale', 'auto']
}

# Use GridSearchCV for hyperparameter tuning
grid_search_svc = GridSearchCV(estimator=svc_model, param_grid=param_grid, cv=5, scoring='accuracy')
grid_search_svc.fit(X_train_scaled,y_train)

# Print the best parameters and the best score
print("Best Hyperparameters:", grid_search_svc.best_params_)
print("Best Score:", grid_search_svc.best_score_)

# Evaluate the tuned model on the test set
best_svc_model = grid_search_svc.best_estimator_
y_pred_svc = best_svc_model.predict(X_test_scaled)
print("Classification Report:\n", classification_report(y_test, y_pred_svc))
metrics_score(y_test, y_pred_svc)

AUC-ROC evaluation¶

predict the auc-roc value for the models

In [ ]:
# Predict probabilities for the positive class
y_pred_prob = lg.predict_proba(X_test_scaled)[:, 1]


# Calculate the AUC-ROC score
auc_score = roc_auc_score(y_test, y_pred_prob)
print(f"AUC-ROC Score: {auc_score:.2f}")

# Calculate the ROC curve
fpr, tpr, thresholds = roc_curve(y_test, y_pred_prob)


# Plot the ROC curve
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'AUC = {auc_score:.2f}')
plt.plot([0, 1], [0, 1], 'k--')  # Diagonal line
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc='lower right')
plt.show()