In [1]:
#Import necessary libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.metrics import classification_report, confusion_matrix
import warnings
warnings.filterwarnings("ignore")
In [2]:
#Load the dataset to the program
df = pd.read_excel(r'C:\Users\chuck\Downloads\Telco_customer_churn.xlsx\Telco_customer_churn.xlsx')
In [3]:
#View the first few rows of the dataset
print(df.head())
CustomerID Count Country State City Zip Code \
0 3668-QPYBK 1 United States California Los Angeles 90003
1 9237-HQITU 1 United States California Los Angeles 90005
2 9305-CDSKC 1 United States California Los Angeles 90006
3 7892-POOKP 1 United States California Los Angeles 90010
4 0280-XJGEX 1 United States California Los Angeles 90015
Lat Long Latitude Longitude Gender ... Contract \
0 33.964131, -118.272783 33.964131 -118.272783 Male ... Month-to-month
1 34.059281, -118.30742 34.059281 -118.307420 Female ... Month-to-month
2 34.048013, -118.293953 34.048013 -118.293953 Female ... Month-to-month
3 34.062125, -118.315709 34.062125 -118.315709 Female ... Month-to-month
4 34.039224, -118.266293 34.039224 -118.266293 Male ... Month-to-month
Paperless Billing Payment Method Monthly Charges Total Charges \
0 Yes Mailed check 53.85 108.15
1 Yes Electronic check 70.70 151.65
2 Yes Electronic check 99.65 820.5
3 Yes Electronic check 104.80 3046.05
4 Yes Bank transfer (automatic) 103.70 5036.3
Churn Label Churn Value Churn Score CLTV Churn Reason
0 Yes 1 86 3239 Competitor made better offer
1 Yes 1 67 2701 Moved
2 Yes 1 86 5372 Moved
3 Yes 1 84 5003 Moved
4 Yes 1 89 5340 Competitor had better devices
[5 rows x 33 columns]
In [4]:
#Dropping any irrelevant columns
df = df.drop(['CustomerID', 'Count', 'Country', 'State', 'City', 'Zip Code',
'Lat Long', 'Latitude', 'Longitude'], axis=1)
In [5]:
#Convert "Total Charges" to numeric and handle any missing values
df['Total Charges'] = pd.to_numeric(df['Total Charges'], errors='coerce')
df['Total Charges'] = df['Total Charges'].fillna(df['Total Charges'].mean())
In [6]:
#Separate numerical and categorical columns
num_cols = ['Tenure Months', 'Monthly Charges', 'Total Charges']
cat_cols = ['Gender', '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']
In [7]:
#Check the summary statistics for numerical columns
print(df[num_cols].describe().T)
# Visualise numerical columns
df[num_cols].hist(figsize=(12, 12))
plt.show()
count mean std min 25% 50% \
Tenure Months 7043.0 32.371149 24.559481 0.00 9.000 29.00
Monthly Charges 7043.0 64.761692 30.090047 18.25 35.500 70.35
Total Charges 7043.0 2283.300441 2265.000258 18.80 402.225 1400.55
75% max
Tenure Months 55.00 72.00
Monthly Charges 89.85 118.75
Total Charges 3786.60 8684.80
In [8]:
#Visualise the categorical variables with respect to churn
for col in cat_cols:
if col != 'Churn Value': # Skip the target variable
(pd.crosstab(df[col], df['Churn Value'], normalize='index') * 100).plot(kind='bar', stacked=True)
plt.title(f'{col} vs Churn')
plt.ylabel('Percentage of Churn')
plt.show()
In [9]:
#Plot correlation heatmap for numerical columns
plt.figure(figsize=(12, 8))
sns.heatmap(df[num_cols].corr(), annot=True, cmap='YlOrRd')
plt.title('Correlation Heatmap')
plt.show()
In [10]:
#Encode the categorical variables
df = pd.get_dummies(df, columns=cat_cols, drop_first=True)
In [11]:
#Define the target and features
y = df['Churn Value']
X = df.drop(['Churn Label', 'Churn Reason', 'Churn Value'], axis=1)
In [12]:
#Scale numerical features
scaler = StandardScaler()
X[num_cols] = scaler.fit_transform(X[num_cols])
In [13]:
#Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
In [14]:
#Function to evaluate the model's performance
def metrics_score(actual, predicted):
print(classification_report(actual, predicted))
cm = confusion_matrix(actual, predicted)
plt.figure(figsize=(8, 5))
sns.heatmap(cm, annot=True, fmt='d', xticklabels=['No Churn', 'Churn'], yticklabels=['No Churn', 'Churn'])
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.title('Confusion Matrix')
plt.show()
In [15]:
#Ensure all columns are numeric
binary_mapping = {'Yes': 1, 'No': 0}
X_train = X_train.applymap(lambda x: binary_mapping.get(x, x))
X_test = X_test.applymap(lambda x: binary_mapping.get(x, x))
In [16]:
#Handle multi-class categorical columns if any
X_train = pd.get_dummies(X_train, drop_first=True)
X_test = pd.get_dummies(X_test, drop_first=True)
In [17]:
#Ensure column consistency between train and test sets
X_test = X_test.reindex(columns=X_train.columns, fill_value=0)
In [18]:
#Handle missing values for categorical columns using the mode
categorical_cols = X_train.select_dtypes(include=['object']).columns
In [19]:
#Handle missing values in categorical columns by filling with the mode
for col in categorical_cols:
if not X_train[col].mode().empty: # Check if mode is available (not empty)
mode_value = X_train[col].mode().iloc[0] # Get the mode value
X_train[col] = X_train[col].fillna(mode_value) # Fill missing values with mode
X_test[col] = X_test[col].fillna(mode_value) # Fill missing values with mode in test set
else:
# Handle case where mode is not available (e.g., the column is empty or all NaN)
print(f"Warning: No mode found for column {col}. Using a placeholder value.")
X_train[col] = X_train[col].fillna("Unknown") # Fill with placeholder value
X_test[col] = X_test[col].fillna("Unknown") # Fill with placeholder value
In [20]:
#Convert all data to numeric (handling potential non-numeric values)
X_train = X_train.apply(pd.to_numeric, errors='coerce')
X_test = X_test.apply(pd.to_numeric, errors='coerce')
In [21]:
#Verify no missing values remain
print("Missing values in X_train after imputation:", X_train.isnull().sum().sum())
print("Missing values in X_test after imputation:", X_test.isnull().sum().sum())
Missing values in X_train after imputation: 0 Missing values in X_test after imputation: 0
In [22]:
#Convert to numpy arrays after preprocessing
X_train = np.array(X_train).astype(np.float64)
X_test = np.array(X_test).astype(np.float64)
In [23]:
#Train Logistic Regression
lg = LogisticRegression()
lg.fit(X_train, y_train)
Out[23]:
LogisticRegression()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()
In [24]:
#Evaluate Logistic Regression on training data
print("Logistic Regression - Training Data")
y_pred_train = lg.predict(X_train)
metrics_score(y_train, y_pred_train)
Logistic Regression - Training Data
precision recall f1-score support
0 0.94 0.94 0.94 3622
1 0.84 0.83 0.83 1308
accuracy 0.91 4930
macro avg 0.89 0.89 0.89 4930
weighted avg 0.91 0.91 0.91 4930
In [25]:
#Evaluate Logistic Regression on testing data
print("Logistic Regression - Testing Data")
y_pred_test = lg.predict(X_test)
metrics_score(y_test, y_pred_test)
Logistic Regression - Testing Data
precision recall f1-score support
0 0.94 0.93 0.94 1552
1 0.81 0.85 0.83 561
accuracy 0.91 2113
macro avg 0.88 0.89 0.88 2113
weighted avg 0.91 0.91 0.91 2113
In [26]:
#Train SVM with default parameters
default_svm = SVC(probability=True, kernel='rbf', C=1, gamma='scale')
default_svm.fit(X_train, y_train)
Out[26]:
SVC(C=1, 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(C=1, probability=True)
In [27]:
#Evaluate default SVM on training data
print("Default SVM - Training Data")
y_pred_train = default_svm.predict(X_train)
metrics_score(y_train, y_pred_train)
Default SVM - Training Data
precision recall f1-score support
0 0.73 1.00 0.85 3622
1 0.00 0.00 0.00 1308
accuracy 0.73 4930
macro avg 0.37 0.50 0.42 4930
weighted avg 0.54 0.73 0.62 4930
In [28]:
#Evaluate defualt SVM on testing data
print("Default SVM - Testing Data")
y_pred_test = default_svm.predict(X_test)
metrics_score(y_test, y_pred_test)
Default SVM - Testing Data
precision recall f1-score support
0 0.73 1.00 0.85 1552
1 0.00 0.00 0.00 561
accuracy 0.73 2113
macro avg 0.37 0.50 0.42 2113
weighted avg 0.54 0.73 0.62 2113
In [29]:
#Hyperparameter tuning for SVM
param_grid = {
'C': [1, 10], #Reduced range of C values
'gamma': ['scale', 0.1], #Reduced gamma options
'kernel': ['linear', 'rbf'], #Keep both kernel options
'class_weight': ['balanced'] #Retain balanced class weighting
}
#Convert X_train and y_train back to DataFrame and Series for sampling
X_train_df = pd.DataFrame(X_train, columns=X.columns)
y_train_df = pd.Series(y_train.values, index=X_train_df.index, name='Churn Value') #Ensure indices align
#Create a smaller subset for quicker testing
X_train_small = X_train_df.sample(n=1000, random_state=42) #Use only 1000 rows
y_train_small = y_train_df.loc[X_train_small.index] #Align y_train_small with X_train_small
#Perform grid search with the subset
svm = SVC(probability=True)
grid_search = GridSearchCV(estimator=svm, param_grid=param_grid, cv=3, scoring='accuracy', verbose=2, n_jobs=-1)
grid_search.fit(X_train_small, y_train_small)
Fitting 3 folds for each of 8 candidates, totalling 24 fits
Out[29]:
GridSearchCV(cv=3, estimator=SVC(probability=True), n_jobs=-1,
param_grid={'C': [1, 10], 'class_weight': ['balanced'],
'gamma': ['scale', 0.1], 'kernel': ['linear', 'rbf']},
scoring='accuracy', verbose=2)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=3, estimator=SVC(probability=True), n_jobs=-1,
param_grid={'C': [1, 10], 'class_weight': ['balanced'],
'gamma': ['scale', 0.1], 'kernel': ['linear', 'rbf']},
scoring='accuracy', verbose=2)SVC(C=10, class_weight='balanced', kernel='linear', probability=True)
SVC(C=10, class_weight='balanced', kernel='linear', probability=True)
In [30]:
#Display best parameters and cross-validation score
print("Hyperparameter Tuning Results")
print("Best Hyperparameters:", grid_search.best_params_)
print("Best Cross-Validation Score:", grid_search.best_score_)
Hyperparameter Tuning Results
Best Hyperparameters: {'C': 10, 'class_weight': 'balanced', 'gamma': 'scale', 'kernel': 'linear'}
Best Cross-Validation Score: 0.8859817901734068
In [31]:
#Use the best estimator for evaluation
best_svm = grid_search.best_estimator_
In [32]:
#Evaluate tuned SVM on training data
print("Tuned SVM - Training Data")
y_pred_train = best_svm.predict(X_train)
metrics_score(y_train, y_pred_train)
Tuned SVM - Training Data
precision recall f1-score support
0 0.97 0.89 0.93 3622
1 0.75 0.91 0.83 1308
accuracy 0.90 4930
macro avg 0.86 0.90 0.88 4930
weighted avg 0.91 0.90 0.90 4930
In [33]:
#Evaluate tuned SVM on testing data
print("Tuned SVM - Testing Data")
y_pred_test = best_svm.predict(X_test)
metrics_score(y_test, y_pred_test)
Tuned SVM - Testing Data
precision recall f1-score support
0 0.97 0.88 0.93 1552
1 0.74 0.93 0.82 561
accuracy 0.89 2113
macro avg 0.86 0.91 0.87 2113
weighted avg 0.91 0.89 0.90 2113