1. Inserting data¶
Import libraries¶
In [1]:
import pandas as pd # data manipulation and analysis
import numpy as np # numerical
import matplotlib.pyplot as plt # visualisations
import seaborn as sns # visualisation (advanced)
from sklearn.model_selection import train_test_split, GridSearchCV # model selector/tuner
from sklearn.preprocessing import StandardScaler, LabelEncoder # data preprosessing
from sklearn.linear_model import LogisticRegression # logistic regression
from sklearn.svm import SVC # Support Vector Classifier
from sklearn.metrics import (accuracy_score, # model performance tools
precision_score,
recall_score,
f1_score,
roc_auc_score,
roc_curve)
from sklearn.metrics import confusion_matrix # confusion metrics
Insert the file using the path location¶
In [5]:
df = pd.read_excel('Telco_Customer_Churn.xlsx')
2. Data exploration¶
Check the first 10 rows of the dataset¶
In [7]:
df.head(10)
Out[7]:
| CustomerID | Count | Country | State | City | Zip Code | Lat Long | Latitude | Longitude | Gender | ... | Contract | Paperless Billing | Payment Method | Monthly Charges | Total Charges | Churn Label | Churn Value | Churn Score | CLTV | Churn Reason | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 3668-QPYBK | 1 | United States | California | Los Angeles | 90003 | 33.964131, -118.272783 | 33.964131 | -118.272783 | Male | ... | Month-to-month | Yes | Mailed check | 53.85 | 108.15 | Yes | 1 | 86 | 3239 | Competitor made better offer |
| 1 | 9237-HQITU | 1 | United States | California | Los Angeles | 90005 | 34.059281, -118.30742 | 34.059281 | -118.307420 | Female | ... | Month-to-month | Yes | Electronic check | 70.70 | 151.65 | Yes | 1 | 67 | 2701 | Moved |
| 2 | 9305-CDSKC | 1 | United States | California | Los Angeles | 90006 | 34.048013, -118.293953 | 34.048013 | -118.293953 | Female | ... | Month-to-month | Yes | Electronic check | 99.65 | 820.5 | Yes | 1 | 86 | 5372 | Moved |
| 3 | 7892-POOKP | 1 | United States | California | Los Angeles | 90010 | 34.062125, -118.315709 | 34.062125 | -118.315709 | Female | ... | Month-to-month | Yes | Electronic check | 104.80 | 3046.05 | Yes | 1 | 84 | 5003 | Moved |
| 4 | 0280-XJGEX | 1 | United States | California | Los Angeles | 90015 | 34.039224, -118.266293 | 34.039224 | -118.266293 | Male | ... | Month-to-month | Yes | Bank transfer (automatic) | 103.70 | 5036.3 | Yes | 1 | 89 | 5340 | Competitor had better devices |
| 5 | 4190-MFLUW | 1 | United States | California | Los Angeles | 90020 | 34.066367, -118.309868 | 34.066367 | -118.309868 | Female | ... | Month-to-month | No | Credit card (automatic) | 55.20 | 528.35 | Yes | 1 | 78 | 5925 | Competitor offered higher download speeds |
| 6 | 8779-QRDMV | 1 | United States | California | Los Angeles | 90022 | 34.02381, -118.156582 | 34.023810 | -118.156582 | Male | ... | Month-to-month | Yes | Electronic check | 39.65 | 39.65 | Yes | 1 | 100 | 5433 | Competitor offered more data |
| 7 | 1066-JKSGK | 1 | United States | California | Los Angeles | 90024 | 34.066303, -118.435479 | 34.066303 | -118.435479 | Male | ... | Month-to-month | No | Mailed check | 20.15 | 20.15 | Yes | 1 | 92 | 4832 | Competitor made better offer |
| 8 | 6467-CHFZW | 1 | United States | California | Los Angeles | 90028 | 34.099869, -118.326843 | 34.099869 | -118.326843 | Male | ... | Month-to-month | Yes | Electronic check | 99.35 | 4749.15 | Yes | 1 | 77 | 5789 | Competitor had better devices |
| 9 | 8665-UTDHZ | 1 | United States | California | Los Angeles | 90029 | 34.089953, -118.294824 | 34.089953 | -118.294824 | Male | ... | Month-to-month | No | Electronic check | 30.20 | 30.2 | Yes | 1 | 97 | 2915 | Competitor had better devices |
10 rows × 33 columns
Check the last 10 rows of the dataset¶
In [9]:
df.tail(10)
Out[9]:
| CustomerID | Count | Country | State | City | Zip Code | Lat Long | Latitude | Longitude | Gender | ... | Contract | Paperless Billing | Payment Method | Monthly Charges | Total Charges | Churn Label | Churn Value | Churn Score | CLTV | Churn Reason | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7033 | 0871-OPBXW | 1 | United States | California | Twentynine Palms | 92277 | 34.17211, -115.769773 | 34.172110 | -115.769773 | Female | ... | Month-to-month | Yes | Mailed check | 20.05 | 39.25 | No | 0 | 80 | 5191 | NaN |
| 7034 | 3605-JISKB | 1 | United States | California | Twentynine Palms | 92278 | 34.457829, -116.139589 | 34.457829 | -116.139589 | Male | ... | One year | No | Credit card (automatic) | 60.00 | 3316.1 | No | 0 | 71 | 4212 | NaN |
| 7035 | 9767-FFLEM | 1 | United States | California | Westmorland | 92281 | 33.03679, -115.60503 | 33.036790 | -115.605030 | Male | ... | Month-to-month | Yes | Credit card (automatic) | 69.50 | 2625.25 | No | 0 | 35 | 4591 | NaN |
| 7036 | 8456-QDAVC | 1 | United States | California | Winterhaven | 92283 | 32.852947, -114.850784 | 32.852947 | -114.850784 | Male | ... | Month-to-month | Yes | Bank transfer (automatic) | 78.70 | 1495.1 | No | 0 | 20 | 2464 | NaN |
| 7037 | 7750-EYXWZ | 1 | United States | California | Yucca Valley | 92284 | 34.159534, -116.425984 | 34.159534 | -116.425984 | Female | ... | One year | No | Electronic check | 60.65 | 743.3 | No | 0 | 24 | 3740 | NaN |
| 7038 | 2569-WGERO | 1 | United States | California | Landers | 92285 | 34.341737, -116.539416 | 34.341737 | -116.539416 | Female | ... | Two year | Yes | Bank transfer (automatic) | 21.15 | 1419.4 | No | 0 | 45 | 5306 | NaN |
| 7039 | 6840-RESVB | 1 | United States | California | Adelanto | 92301 | 34.667815, -117.536183 | 34.667815 | -117.536183 | Male | ... | One year | Yes | Mailed check | 84.80 | 1990.5 | No | 0 | 59 | 2140 | NaN |
| 7040 | 2234-XADUH | 1 | United States | California | Amboy | 92304 | 34.559882, -115.637164 | 34.559882 | -115.637164 | Female | ... | One year | Yes | Credit card (automatic) | 103.20 | 7362.9 | No | 0 | 71 | 5560 | NaN |
| 7041 | 4801-JZAZL | 1 | United States | California | Angelus Oaks | 92305 | 34.1678, -116.86433 | 34.167800 | -116.864330 | Female | ... | Month-to-month | Yes | Electronic check | 29.60 | 346.45 | No | 0 | 59 | 2793 | NaN |
| 7042 | 3186-AJIEK | 1 | United States | California | Apple Valley | 92308 | 34.424926, -117.184503 | 34.424926 | -117.184503 | Male | ... | Two year | Yes | Bank transfer (automatic) | 105.65 | 6844.5 | No | 0 | 38 | 5097 | NaN |
10 rows × 33 columns
Summary of numerical features¶
In [11]:
df.describe()
Out[11]:
| Count | Zip Code | Latitude | Longitude | Tenure Months | Monthly Charges | Churn Value | Churn Score | CLTV | |
|---|---|---|---|---|---|---|---|---|---|
| count | 7043.0 | 7043.000000 | 7043.000000 | 7043.000000 | 7043.000000 | 7043.000000 | 7043.000000 | 7043.000000 | 7043.000000 |
| mean | 1.0 | 93521.964646 | 36.282441 | -119.798880 | 32.371149 | 64.761692 | 0.265370 | 58.699418 | 4400.295755 |
| std | 0.0 | 1865.794555 | 2.455723 | 2.157889 | 24.559481 | 30.090047 | 0.441561 | 21.525131 | 1183.057152 |
| min | 1.0 | 90001.000000 | 32.555828 | -124.301372 | 0.000000 | 18.250000 | 0.000000 | 5.000000 | 2003.000000 |
| 25% | 1.0 | 92102.000000 | 34.030915 | -121.815412 | 9.000000 | 35.500000 | 0.000000 | 40.000000 | 3469.000000 |
| 50% | 1.0 | 93552.000000 | 36.391777 | -119.730885 | 29.000000 | 70.350000 | 0.000000 | 61.000000 | 4527.000000 |
| 75% | 1.0 | 95351.000000 | 38.224869 | -118.043237 | 55.000000 | 89.850000 | 1.000000 | 75.000000 | 5380.500000 |
| max | 1.0 | 96161.000000 | 41.962127 | -114.192901 | 72.000000 | 118.750000 | 1.000000 | 100.000000 | 6500.000000 |
Info about dataset (check data types and missing values)¶
In [13]:
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 7043 entries, 0 to 7042 Data columns (total 33 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 CustomerID 7043 non-null object 1 Count 7043 non-null int64 2 Country 7043 non-null object 3 State 7043 non-null object 4 City 7043 non-null object 5 Zip Code 7043 non-null int64 6 Lat Long 7043 non-null object 7 Latitude 7043 non-null float64 8 Longitude 7043 non-null float64 9 Gender 7043 non-null object 10 Senior Citizen 7043 non-null object 11 Partner 7043 non-null object 12 Dependents 7043 non-null object 13 Tenure Months 7043 non-null int64 14 Phone Service 7043 non-null object 15 Multiple Lines 7043 non-null object 16 Internet Service 7043 non-null object 17 Online Security 7043 non-null object 18 Online Backup 7043 non-null object 19 Device Protection 7043 non-null object 20 Tech Support 7043 non-null object 21 Streaming TV 7043 non-null object 22 Streaming Movies 7043 non-null object 23 Contract 7043 non-null object 24 Paperless Billing 7043 non-null object 25 Payment Method 7043 non-null object 26 Monthly Charges 7043 non-null float64 27 Total Charges 7043 non-null object 28 Churn Label 7043 non-null object 29 Churn Value 7043 non-null int64 30 Churn Score 7043 non-null int64 31 CLTV 7043 non-null int64 32 Churn Reason 1869 non-null object dtypes: float64(3), int64(6), object(24) memory usage: 1.8+ MB
Unique values¶
In [15]:
df.nunique()
Out[15]:
CustomerID 7043 Count 1 Country 1 State 1 City 1129 Zip Code 1652 Lat Long 1652 Latitude 1652 Longitude 1651 Gender 2 Senior Citizen 2 Partner 2 Dependents 2 Tenure Months 73 Phone Service 2 Multiple Lines 3 Internet Service 3 Online Security 3 Online Backup 3 Device Protection 3 Tech Support 3 Streaming TV 3 Streaming Movies 3 Contract 3 Paperless Billing 2 Payment Method 4 Monthly Charges 1585 Total Charges 6531 Churn Label 2 Churn Value 2 Churn Score 85 CLTV 3438 Churn Reason 20 dtype: int64
3. Data Cleaning¶
Drop columns¶
In [17]:
df = df.drop(['Churn Value','Lat Long','CustomerID','City','Count', 'Country', 'State', 'Zip Code', 'Latitude', 'Longitude'], axis=1)
Creating numerical columns¶
In [19]:
num_cols=['Tenure Months','Monthly Charges','Churn Score','CLTV','Total Charges']
Creating categorical variables¶
In [21]:
cat_cols=['Gender','Senior Citizen','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','Churn Label','Churn Reason']
Convert 'Total Charges' to numeric and handle errors¶
In [23]:
# coerce means that numeric values in the column are changed to NaN
df['Total Charges'] = pd.to_numeric(df['Total Charges'], errors='coerce')
# NaN values change to the column mean value
df['Total Charges'] = df['Total Charges'].fillna(df['Total Charges'].mean())
Create a copy of the DataFrame for model training¶
In [25]:
df_model = df.copy()
One-hot encoding categorical columns¶
In [31]:
# pd.get_dummies changes categorical into indicators from cat_cols
# drop_first=True drops the first category of each cat_cols variable to stop variable trap
df_encoded = pd.get_dummies(df, columns=cat_cols, drop_first=True)
Define features (X) and target (y)¶
In [33]:
# Drop churn label from independent variables
X = df_encoded.drop('Churn Label_Yes', axis=1)
# Make churn label the dependent variable
y = df_encoded['Churn Label_Yes']
Split the data into training and testing sets¶
In [35]:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, # 20% testing
random_state=42, # reproducibility with fixed seed for random splitting
stratify=y) # proportioned split for target and training
Standardize the numerical features¶
In [37]:
scaler = StandardScaler() # mean = 0 and standard deviation = 1
X_train[num_cols] = scaler.fit_transform(X_train[num_cols]) # scale training data
X_test[num_cols] = scaler.transform(X_test[num_cols]) # scale numerical columns without fitting for consistency
4. Exploratory Data Analysis (EDA)¶
Univariate analysis of numerical columns¶
Checking summary statistics¶
In [39]:
df[num_cols].describe().T
Out[39]:
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| Tenure Months | 7043.0 | 32.371149 | 24.559481 | 0.00 | 9.000 | 29.00 | 55.00 | 72.00 |
| Monthly Charges | 7043.0 | 64.761692 | 30.090047 | 18.25 | 35.500 | 70.35 | 89.85 | 118.75 |
| Churn Score | 7043.0 | 58.699418 | 21.525131 | 5.00 | 40.000 | 61.00 | 75.00 | 100.00 |
| CLTV | 7043.0 | 4400.295755 | 1183.057152 | 2003.00 | 3469.000 | 4527.00 | 5380.50 | 6500.00 |
| Total Charges | 7043.0 | 2283.300441 | 2265.000258 | 18.80 | 402.225 | 1400.55 | 3786.60 | 8684.80 |
Univariate analysis for categorical variables¶
Printing the % sub categories of each category¶
In [41]:
# printing proportions for each category
for i in cat_cols:
print(df[i].value_counts (normalize=True))
print('*'*40)
Gender Male 0.504756 Female 0.495244 Name: proportion, dtype: float64 **************************************** Senior Citizen No 0.837853 Yes 0.162147 Name: proportion, dtype: float64 **************************************** Partner No 0.516967 Yes 0.483033 Name: proportion, dtype: float64 **************************************** Dependents No 0.76899 Yes 0.23101 Name: proportion, dtype: float64 **************************************** Phone Service Yes 0.903166 No 0.096834 Name: proportion, dtype: float64 **************************************** Multiple Lines No 0.481329 Yes 0.421837 No phone service 0.096834 Name: proportion, dtype: float64 **************************************** Internet Service Fiber optic 0.439585 DSL 0.343746 No 0.216669 Name: proportion, dtype: float64 **************************************** Online Security No 0.496663 Yes 0.286668 No internet service 0.216669 Name: proportion, dtype: float64 **************************************** Online Backup No 0.438450 Yes 0.344881 No internet service 0.216669 Name: proportion, dtype: float64 **************************************** Device Protection No 0.439443 Yes 0.343888 No internet service 0.216669 Name: proportion, dtype: float64 **************************************** Tech Support No 0.493114 Yes 0.290217 No internet service 0.216669 Name: proportion, dtype: float64 **************************************** Streaming TV No 0.398978 Yes 0.384353 No internet service 0.216669 Name: proportion, dtype: float64 **************************************** Streaming Movies No 0.395428 Yes 0.387903 No internet service 0.216669 Name: proportion, dtype: float64 **************************************** Contract Month-to-month 0.550192 Two year 0.240664 One year 0.209144 Name: proportion, dtype: float64 **************************************** Paperless Billing Yes 0.592219 No 0.407781 Name: proportion, dtype: float64 **************************************** Payment Method Electronic check 0.335794 Mailed check 0.228880 Bank transfer (automatic) 0.219225 Credit card (automatic) 0.216101 Name: proportion, dtype: float64 **************************************** Churn Label No 0.73463 Yes 0.26537 Name: proportion, dtype: float64 **************************************** Churn Reason Attitude of support person 0.102729 Competitor offered higher download speeds 0.101124 Competitor offered more data 0.086677 Don't know 0.082397 Competitor made better offer 0.074906 Attitude of service provider 0.072231 Competitor had better devices 0.069556 Network reliability 0.055110 Product dissatisfaction 0.054575 Price too high 0.052434 Service dissatisfaction 0.047619 Lack of self-service on Website 0.047084 Extra data charges 0.030498 Moved 0.028357 Limited range of services 0.023542 Long distance charges 0.023542 Lack of affordable download/upload speed 0.023542 Poor expertise of phone support 0.010701 Poor expertise of online support 0.010166 Deceased 0.003210 Name: proportion, dtype: float64 ****************************************
Visualise Numerical Columns¶
In [43]:
df[num_cols].describe().T # summary statistics for numerical columns, transpose rows to columns
df[num_cols].hist(figsize=(14, 14)) # histogram creation for numerical columns
plt.show() # show histograms
Bivariate and Multivariate analysis¶
In [45]:
for i in cat_cols:
# exclude churn label
if i!='Churn Label':
# create normalised contingency table of columns and churn label, shown as percentage of bar,
(pd.crosstab(df[i],df['Churn Label'],normalize='index')*100).plot(kind='bar',figsize=(8,4),stacked=True)
# set y axis label to %
plt.ylabel('%')
# display tables
plt.show()
In [47]:
# Checking the relationship between different numerical variables
# Plotting the correlation between numerical variables
plt.figure(figsize=(15,8)) # set plot size
sns.heatmap(df[num_cols].corr(), annot=True, fmt='0.2f', cmap='YlOrRd') # correlation for numerical columns, displaying 2d.p and colour themed
plt.title('Corrolation Matrix of Numerical Variables') # title heat map
plt.show() # show heatmap
ML Models development using LR and SVM¶
Logistic Regression¶
In [47]:
# start logistic regression with fixed random state (more reliable)
log_reg = LogisticRegression(random_state=42)
# fit feature matrix and target vector
log_reg.fit(X_train, y_train)
Out[47]:
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)
Predictions for logistic regression¶
In [49]:
# make predictions using trained logistic regression, on feature matrix and target values for test set
y_pred_lr = log_reg.predict(X_test)
Evaluation Metrics for logistic regression¶
In [51]:
# proportion of correctly predicted instances out of all instances
accuracy = accuracy_score(y_test, y_pred_lr)
# proportion of true positive instances out of all instances predicted
precision = precision_score(y_test, y_pred_lr)
# proportion of true positive instances out of all actual positive instances
recall = recall_score(y_test, y_pred_lr)
# f1 score of all predictions
f1 = f1_score(y_test, y_pred_lr)
# ROC AUC score of predictions
roc_auc = roc_auc_score(y_test, y_pred_lr)
# print all results
print(f'Accuracy: {accuracy:.4f}')
print(f'Precision: {precision:.4f}')
print(f'Recall: {recall:.4f}')
print(f'F1 Score: {f1:.4f}')
print(f'ROC AUC: {roc_auc:.4f}')
Accuracy: 0.9879 Precision: 0.9837 Recall: 0.9706 F1 Score: 0.9771 ROC AUC: 0.9824
Confusion matrix for logistic regression¶
In [53]:
# apply confusion matrix onto the logistical regression model
conf_matrix = confusion_matrix(y_test, y_pred_lr)
# set plot size
plt.figure(figsize=(10, 6))
#create heatmap of confusion matrix, with annotations, blue colours, and labeled axis
sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues', xticklabels=['Not Churn', 'Churn'], yticklabels=['Not Churn', 'Churn'])
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.title('Confusion Matrix')
# show confusion matrix
plt.show()
Support Vector Machine¶
In [55]:
# enables probability estimates which is needed for ROC AUC analysis
svm = SVC(probability=True)
# fits SVC model to training data, on feature matrix and target vector training set
svm.fit(X_train, y_train)
Out[55]:
SVC(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(probability=True)
Predictions for SVC¶
In [57]:
# Make predictions on the test set
y_pred_svm = svm.predict(X_test)
# Get predicted probabilities for the positive class (churn)
y_prob_svm = svm.predict_proba(X_test)[:, 1]
Evaluation Metrics for SVC¶
In [59]:
# Accuracy
accuracy_svm = accuracy_score(y_test, y_pred_svm)
print(f"Accuracy: {accuracy_svm:.4f}")
# Precision
precision_svm = precision_score(y_test, y_pred_svm)
print(f"Precision: {precision_svm:.4f}")
# Recall
recall_svm = recall_score(y_test, y_pred_svm)
print(f"Recall: {recall_svm:.4f}")
# F1 Score
f1_svm = f1_score(y_test, y_pred_svm)
print(f"F1 Score: {f1_svm:.4f}")
# ROC AUC (using predicted probabilities for the positive class)
roc_auc_svm = roc_auc_score(y_test, y_prob_svm)
print(f"ROC AUC: {roc_auc_svm:.4f}")
Accuracy: 0.9851 Precision: 0.9862 Recall: 0.9572 F1 Score: 0.9715 ROC AUC: 0.9989
In [61]:
# Generate confusion matrix
cm_svm = confusion_matrix(y_test, y_pred_svm)
# Visualize the confusion matrix using a heatmap
plt.figure(figsize=(6,6))
sns.heatmap(cm_svm, annot=True, fmt='d', cmap='Blues', xticklabels=['Non-Churn', 'Churn'], yticklabels=['Non-Churn', 'Churn'])
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.title('Confusion Matrix for SVM')
plt.show()
Evaluate the performance of the developed model using accuracy, precision, recall, F1 score and AUC-ROC¶
In [63]:
def evaluate_model(y_test, y_pred, model_name):
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
auc_roc = roc_auc_score(y_test, y_pred)
print(f"{model_name} Model Performance:")
print(f"Accuracy: {accuracy}")
print(f"Precision: {precision}")
print(f"Recall: {recall}")
print(f"F1 Score: {f1}")
print(f"AUC-ROC: {auc_roc}")
print("\n")
evaluate_model(y_test, y_pred_lr, "Logistic Regression")
evaluate_model(y_test, y_pred_svm, "Support Vector Machine")
Logistic Regression Model Performance: Accuracy: 0.9879347054648687 Precision: 0.983739837398374 Recall: 0.9705882352941176 F1 Score: 0.9771197846567967 AUC-ROC: 0.9823955669224211 Support Vector Machine Model Performance: Accuracy: 0.9850958126330731 Precision: 0.9862258953168044 Recall: 0.9572192513368984 F1 Score: 0.9715061058344641 AUC-ROC: 0.9761941667312511
Hypertuning¶
In [65]:
# Initialize the Support Vector Machine classifier with probability estimation
svm = SVC(probability=True, random_state=42)
# Fit the SVM to the training data
svm.fit(X_train, y_train)
# Make predictions on the test set
y_pred_svm = svm.predict(X_test)
Define the parameter grid to tune¶
In [67]:
param_grid = {
'C': [0.1, 1, 10, 100], # Regularization parameter
'kernel': ['linear', 'rbf', 'poly'], # Kernel type
'gamma': ['scale', 'auto'], # Kernel coefficient
'class_weight': [None, 'balanced'] # Handle class imbalance
}
Initialize GridSearchCV with 5-fold cross-validation¶
In [69]:
grid_search = GridSearchCV(svm, param_grid, cv=5, verbose=1, n_jobs=-1, scoring='accuracy')
Fit the grid search¶
In [71]:
grid_search.fit(X_train, y_train)
Fitting 5 folds for each of 48 candidates, totalling 240 fits
Out[71]:
GridSearchCV(cv=5, estimator=SVC(probability=True, random_state=42), n_jobs=-1,
param_grid={'C': [0.1, 1, 10, 100],
'class_weight': [None, 'balanced'],
'gamma': ['scale', 'auto'],
'kernel': ['linear', 'rbf', 'poly']},
scoring='accuracy', verbose=1)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=5, estimator=SVC(probability=True, random_state=42), n_jobs=-1,
param_grid={'C': [0.1, 1, 10, 100],
'class_weight': [None, 'balanced'],
'gamma': ['scale', 'auto'],
'kernel': ['linear', 'rbf', 'poly']},
scoring='accuracy', verbose=1)SVC(probability=True, random_state=42)
SVC(probability=True, random_state=42)
Best hyperparameters and model¶
In [77]:
# Best hyperparameters found from GridSearchCV
print(f"Best hyperparameters found: {grid_search.best_params_}")
# Best model from grid search
best_svm = grid_search.best_estimator_
print(best_svm)
Best hyperparameters found: {'C': 10, 'class_weight': None, 'gamma': 'scale', 'kernel': 'linear'}
SVC(C=10, kernel='linear', probability=True, random_state=42)
Predictions and evaluation¶
In [79]:
# Make predictions using the best model
y_pred_best_svm = best_svm.predict(X_test)
# Evaluate performance metrics
print("SVM Model Performance (Best Model from Grid Search):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best_svm):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_best_svm):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_best_svm):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_best_svm):.4f}")
print(f"ROC AUC: {roc_auc_score(y_test, best_svm.predict_proba(X_test)[:, 1]):.4f}")
SVM Model Performance (Best Model from Grid Search): Accuracy: 0.9922 Precision: 1.0000 Recall: 0.9706 F1 Score: 0.9851 ROC AUC: 0.9987
Confusion matrix¶
In [81]:
# Generate the confusion matrix
cm = confusion_matrix(y_test, y_pred_best_svm)
# Plot the confusion matrix
plt.figure(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=["No Churn", "Churn"], yticklabels=["No Churn", "Churn"])
plt.title("Confusion Matrix")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()
Hyperparameter tuning for logistic regression¶
In [83]:
# Define a reduced parameter grid for Logistic Regression
# There are three dictionaries
# C gives regularisaiton strength because of smaller values
param_grid_lr = [
# solver, Liblinear is a optimisation algorithm suitable for small datasets and both l1 and l2
{'C': [0.1, 1, 10], 'solver': ['liblinear'], 'penalty': ['l1', 'l2']},
# solver, ibfgs is an optimisation algorithm suitable for l2 penalty only
{'C': [0.1, 1, 10], 'solver': ['lbfgs'], 'penalty': ['l2']},
# solver saga is an optimisation algorithm suitable for l1 penalty only
{'C': [0.1, 1, 10], 'solver': ['saga'], 'penalty': ['l1', 'l2']}
]
Perform Grid Search¶
In [85]:
grid_search_lr = GridSearchCV(LogisticRegression(random_state=42), param_grid_lr, cv=5, verbose=1, n_jobs=-1, scoring='accuracy')
grid_search_lr.fit(X_train, y_train)
Fitting 5 folds for each of 15 candidates, totalling 75 fits
Out[85]:
GridSearchCV(cv=5, estimator=LogisticRegression(random_state=42), n_jobs=-1,
param_grid=[{'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
'solver': ['liblinear']},
{'C': [0.1, 1, 10], 'penalty': ['l2'],
'solver': ['lbfgs']},
{'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
'solver': ['saga']}],
scoring='accuracy', verbose=1)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=5, estimator=LogisticRegression(random_state=42), n_jobs=-1,
param_grid=[{'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
'solver': ['liblinear']},
{'C': [0.1, 1, 10], 'penalty': ['l2'],
'solver': ['lbfgs']},
{'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'],
'solver': ['saga']}],
scoring='accuracy', verbose=1)LogisticRegression(random_state=42)
LogisticRegression(random_state=42)
Increase the number of iterations for Logistic Regression¶
In [87]:
# Best Logistic Regression model from grid search
best_log_reg = grid_search_lr.best_estimator_
y_pred_best_log_reg = best_log_reg.predict(X_test)
Evaluate Performance Metrics for the Best Logistic Regression Model¶
In [89]:
# Evaluate performance metrics for the best Logistic Regression model
accuracy_best_log_reg = accuracy_score(y_test, y_pred_best_log_reg)
precision_best_log_reg = precision_score(y_test, y_pred_best_log_reg)
recall_best_log_reg = recall_score(y_test, y_pred_best_log_reg)
f1_best_log_reg = f1_score(y_test, y_pred_best_log_reg)
roc_auc_best_log_reg = roc_auc_score(y_test, y_pred_best_log_reg)
print(f'Logistic Regression Model Performance (Best Model from Reduced Grid Search):')
print(f'Accuracy: {accuracy_best_log_reg:.4f}')
print(f'Precision: {precision_best_log_reg:.4f}')
print(f'Recall: {recall_best_log_reg:.4f}')
print(f'F1 Score: {f1_best_log_reg:.4f}')
print(f'ROC AUC: {roc_auc_best_log_reg:.4f}')
Logistic Regression Model Performance (Best Model from Reduced Grid Search): Accuracy: 0.9943 Precision: 0.9946 Recall: 0.9840 F1 Score: 0.9892 ROC AUC: 0.9910
In [91]:
# Ensure that the best Logistic Regression model is defined
best_log_reg = grid_search_lr.best_estimator_
# Make predictions using the best Logistic Regression model
y_pred_best_log_reg = best_log_reg.predict(X_test)
In [93]:
print(f'Logistic Regression Model Performance (Best Model from Grid Search):')
print(f'Accuracy: {accuracy_best_log_reg:.4f}')
print(f'Precision: {precision_best_log_reg:.4f}')
print(f'Recall: {recall_best_log_reg:.4f}')
print(f'F1 Score: {f1_best_log_reg:.4f}')
print(f'ROC AUC: {roc_auc_best_log_reg:.4f}')
Logistic Regression Model Performance (Best Model from Grid Search): Accuracy: 0.9943 Precision: 0.9946 Recall: 0.9840 F1 Score: 0.9892 ROC AUC: 0.9910
In [95]:
# Cross-validation for Logistic Regression
#cv_scores_lr_after = cross_val_score(best_log_reg, X, y, cv=5, scoring='accuracy')
#print(f"Logistic Regression Cross-Validation Accuracy (After Tuning): {cv_scores_lr_after.mean():.4f}")
Logistic Regression vs. SVM Comparison after tuning¶
In [98]:
# Step 1: Evaluate Logistic Regression before and after hyperparameter tuning
log_reg_baseline = LogisticRegression(random_state=42).fit(X_train, y_train)
y_pred_log_reg_baseline = log_reg_baseline.predict(X_test)
In [100]:
# Step 2: Evaluate SVM before and after hyperparameter tuning
y_pred_svm_baseline = svm.predict(X_test)
In [102]:
# Step 3: Hyperparameter tuning for both models (as done earlier)
# Compare performance before and after tuning
print(f"Logistic Regression (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_log_reg_baseline)}")
print(f"Precision: {precision_score(y_test, y_pred_log_reg_baseline)}")
print(f"Recall: {recall_score(y_test, y_pred_log_reg_baseline)}")
print(f"F1 Score: {f1_score(y_test, y_pred_log_reg_baseline)}")
print(f"\nSVM (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_svm_baseline)}")
print(f"Precision: {precision_score(y_test, y_pred_svm_baseline)}")
print(f"Recall: {recall_score(y_test, y_pred_svm_baseline)}")
print(f"F1 Score: {f1_score(y_test, y_pred_svm_baseline)}")
Logistic Regression (Before Tuning): Accuracy: 0.9879347054648687 Precision: 0.983739837398374 Recall: 0.9705882352941176 F1 Score: 0.9771197846567967 SVM (Before Tuning): Accuracy: 0.9850958126330731 Precision: 0.9862258953168044 Recall: 0.9572192513368984 F1 Score: 0.9715061058344641
In [104]:
print("\nLogistic Regression (After Tuning):")
print(f"Accuracy: {accuracy_best_log_reg:.4f}")
print(f"Precision: {precision_best_log_reg:.4f}")
print(f"Recall: {recall_best_log_reg:.4f}")
print(f"F1 Score: {f1_best_log_reg:.4f}")
Logistic Regression (After Tuning): Accuracy: 0.9943 Precision: 0.9946 Recall: 0.9840 F1 Score: 0.9892
In [106]:
print("\nSVM (After Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best_svm):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_best_svm):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_best_svm):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_best_svm):.4f}")
SVM (After Tuning): Accuracy: 0.9922 Precision: 1.0000 Recall: 0.9706 F1 Score: 0.9851
Visualise Performance Metrics¶
In [109]:
# Performance metrics before and after tuning
metrics = ['Accuracy', 'Precision', 'Recall', 'F1 Score', 'ROC AUC']
log_reg_before = [accuracy_score(y_test, y_pred_log_reg_baseline), precision_score(y_test, y_pred_log_reg_baseline), recall_score(y_test, y_pred_log_reg_baseline), f1_score(y_test, y_pred_log_reg_baseline), roc_auc_score(y_test, y_pred_log_reg_baseline)]
log_reg_after = [accuracy_best_log_reg, precision_best_log_reg, recall_best_log_reg, f1_best_log_reg, roc_auc_best_log_reg]
svm_before = [accuracy_score(y_test, y_pred_svm_baseline), precision_score(y_test, y_pred_svm_baseline), recall_score(y_test, y_pred_svm_baseline), f1_score(y_test, y_pred_svm_baseline), roc_auc_score(y_test, y_pred_svm_baseline)]
svm_after = [accuracy_score(y_test, y_pred_best_svm), precision_score(y_test, y_pred_best_svm), recall_score(y_test, y_pred_best_svm), f1_score(y_test, y_pred_best_svm), roc_auc_score(y_test, best_svm.predict_proba(X_test)[:, 1])]
# Plotting the metrics
fig, ax = plt.subplots(1, 2, figsize=(14, 6))
ax[0].bar(metrics, log_reg_before, color='blue', alpha=0.6, label='Before Tuning')
ax[0].bar(metrics, log_reg_after, color='green', alpha=0.6, label='After Tuning')
ax[0].set_title('Logistic Regression Performance')
ax[0].legend()
ax[1].bar(metrics, svm_before, color='blue', alpha=0.6, label='Before Tuning')
ax[1].bar(metrics, svm_after, color='green', alpha=0.6, label='After Tuning')
ax[1].set_title('SVM Performance')
ax[1].legend()
plt.show()
Detailed Comparison¶
In [112]:
# Create a summary table
comparison_data = {
'Metric': metrics,
'LR (Before Tuning)': log_reg_before,
'LR (After Tuning)': log_reg_after,
'SVM (Before Tuning)': svm_before,
'SVM (After Tuning)': svm_after
}
comparison_df = pd.DataFrame(comparison_data)
print(comparison_df)
Metric LR (Before Tuning) LR (After Tuning) SVM (Before Tuning) \ 0 Accuracy 0.987935 0.994322 0.985096 1 Precision 0.983740 0.994595 0.986226 2 Recall 0.970588 0.983957 0.957219 3 F1 Score 0.977120 0.989247 0.971506 4 ROC AUC 0.982396 0.991012 0.976194 SVM (After Tuning) 0 0.992193 1 1.000000 2 0.970588 3 0.985075 4 0.998695
Summary Analysis¶
In [115]:
# Summary of Findings
print("Logistic Regression (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_log_reg_baseline):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_log_reg_baseline):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_log_reg_baseline):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_log_reg_baseline):.4f}")
Logistic Regression (Before Tuning): Accuracy: 0.9879 Precision: 0.9837 Recall: 0.9706 F1 Score: 0.9771
In [117]:
print("\nLogistic Regression (After Tuning):")
print(f"Accuracy: {accuracy_best_log_reg:.4f}")
print(f"Precision: {precision_best_log_reg:.4f}")
print(f"Recall: {recall_best_log_reg:.4f}")
print(f"F1 Score: {f1_best_log_reg:.4f}")
Logistic Regression (After Tuning): Accuracy: 0.9943 Precision: 0.9946 Recall: 0.9840 F1 Score: 0.9892
In [119]:
print("\nSVM (Before Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_svm_baseline):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_svm_baseline):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_svm_baseline):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_svm_baseline):.4f}")
SVM (Before Tuning): Accuracy: 0.9851 Precision: 0.9862 Recall: 0.9572 F1 Score: 0.9715
In [121]:
print("\nSVM (After Tuning):")
print(f"Accuracy: {accuracy_score(y_test, y_pred_best_svm):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_best_svm):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_best_svm):.4f}")
print(f"F1 Score: {f1_score(y_test, y_pred_best_svm):.4f}")
SVM (After Tuning): Accuracy: 0.9922 Precision: 1.0000 Recall: 0.9706 F1 Score: 0.9851
In [123]:
# Feature Importance for Logistic Regression
feature_importance_lr = pd.Series(best_log_reg.coef_[0], index=X.columns).sort_values(ascending=False)
print("Feature Importance for Logistic Regression:")
print(feature_importance_lr)
Feature Importance for Logistic Regression: Churn Reason_Long distance charges 11.917486 Churn Reason_Competitor offered more data 10.821918 Churn Reason_Competitor offered higher download speeds 10.429499 Churn Reason_Attitude of support person 10.342916 Churn Reason_Competitor made better offer 10.311438 Churn Reason_Don't know 10.231137 Churn Reason_Price too high 9.995710 Churn Reason_Network reliability 9.836814 Churn Reason_Moved 9.805171 Churn Reason_Lack of self-service on Website 9.552347 Churn Reason_Product dissatisfaction 9.228050 Churn Reason_Service dissatisfaction 9.027239 Churn Reason_Competitor had better devices 8.845489 Churn Reason_Extra data charges 8.545527 Churn Reason_Lack of affordable download/upload speed 7.706435 Churn Reason_Poor expertise of online support 7.513258 Churn Reason_Poor expertise of phone support 7.428125 Churn Reason_Limited range of services 7.268097 Churn Reason_Deceased 6.422142 Churn Score 5.026159 Monthly Charges 1.100508 Partner_Yes 0.603408 Total Charges 0.583322 Paperless Billing_Yes 0.485778 Senior Citizen_Yes 0.455502 Payment Method_Electronic check 0.261504 CLTV 0.171309 Internet Service_Fiber optic 0.054558 Contract_One year 0.030911 Streaming Movies_Yes 0.000000 Streaming Movies_No internet service 0.000000 Streaming TV_No internet service 0.000000 Tech Support_No internet service 0.000000 Device Protection_Yes 0.000000 Device Protection_No internet service 0.000000 Online Backup_No internet service 0.000000 Online Security_No internet service 0.000000 Internet Service_No 0.000000 Multiple Lines_Yes -0.051372 Payment Method_Credit card (automatic) -0.117536 Payment Method_Mailed check -0.159349 Streaming TV_Yes -0.163500 Online Security_Yes -0.218493 Gender_Male -0.253644 Tech Support_Yes -0.399558 Contract_Two year -0.691048 Online Backup_Yes -0.703037 Dependents_Yes -0.870152 Multiple Lines_No phone service -1.814781 Tenure Months -1.954257 Phone Service_Yes -3.591200 dtype: float64
In [125]:
# Permutation Importance for SVM
from sklearn.inspection import permutation_importance
perm_importance = permutation_importance(best_svm, X_test, y_test)
feature_importance_svm = pd.Series(perm_importance.importances_mean, index=X.columns).sort_values(ascending=False)
print("Feature Importance for SVM:")
print(feature_importance_svm)
Feature Importance for SVM: Churn Score 0.162243 Churn Reason_Attitude of support person 0.027963 Churn Reason_Don't know 0.022427 Churn Reason_Competitor offered more data 0.021434 Churn Reason_Competitor offered higher download speeds 0.019730 Churn Reason_Competitor had better devices 0.017317 Churn Reason_Competitor made better offer 0.016040 Churn Reason_Price too high 0.015188 Churn Reason_Network reliability 0.011923 Churn Reason_Extra data charges 0.009226 Churn Reason_Lack of self-service on Website 0.008091 Churn Reason_Service dissatisfaction 0.007807 Churn Reason_Product dissatisfaction 0.007381 Churn Reason_Limited range of services 0.006529 Churn Reason_Long distance charges 0.005820 Churn Reason_Moved 0.004826 Churn Reason_Lack of affordable download/upload speed 0.004400 Churn Reason_Poor expertise of phone support 0.001845 Tenure Months 0.000852 Churn Reason_Poor expertise of online support 0.000710 Multiple Lines_Yes 0.000284 Device Protection_Yes 0.000142 Contract_One year 0.000142 Payment Method_Mailed check 0.000142 Streaming Movies_Yes 0.000142 Streaming TV_Yes 0.000142 Monthly Charges 0.000000 Churn Reason_Deceased 0.000000 Total Charges 0.000000 CLTV 0.000000 Gender_Male 0.000000 Tech Support_Yes 0.000000 Dependents_Yes 0.000000 Phone Service_Yes 0.000000 Multiple Lines_No phone service 0.000000 Streaming TV_No internet service 0.000000 Streaming Movies_No internet service 0.000000 Internet Service_No 0.000000 Online Security_No internet service 0.000000 Tech Support_No internet service 0.000000 Online Backup_No internet service 0.000000 Online Backup_Yes 0.000000 Device Protection_No internet service 0.000000 Payment Method_Credit card (automatic) 0.000000 Contract_Two year 0.000000 Online Security_Yes 0.000000 Internet Service_Fiber optic -0.000142 Partner_Yes -0.000284 Payment Method_Electronic check -0.000426 Senior Citizen_Yes -0.000568 Paperless Billing_Yes -0.000568 dtype: float64
In [126]:
# Actionable Insights and Recommendations
print("\nActionable Insights:")
print("1. Customer Retention: Focus on improving services and offering loyalty programs.")
print("2. Targeted Marketing: Identify high-risk customers and target them with personalized campaigns.")
print("3. Service Improvement: Enhance services that are associated with higher churn.")
Actionable Insights: 1. Customer Retention: Focus on improving services and offering loyalty programs. 2. Targeted Marketing: Identify high-risk customers and target them with personalized campaigns. 3. Service Improvement: Enhance services that are associated with higher churn.
In [127]:
print("\nRecommendations:")
print("1. Model Deployment: Deploy the best-performing model for real-time churn prediction.")
print("2. Continuous Monitoring: Regularly monitor and retrain the model.")
print("3. Customer Feedback: Collect and address feedback from high-risk customers.")
Recommendations: 1. Model Deployment: Deploy the best-performing model for real-time churn prediction. 2. Continuous Monitoring: Regularly monitor and retrain the model. 3. Customer Feedback: Collect and address feedback from high-risk customers.
In [ ]: