%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
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
# 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
# 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
# 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
# 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
# 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
# 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.
# 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
# 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
# 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
# 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
# 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
# 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
# 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
#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
#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
#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
# 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
# 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()