In [31]:
# Importing necessary libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# To scale the data using z-score
from sklearn.preprocessing import StandardScaler
# Validation
from sklearn.model_selection import train_test_split, GridSearchCV
# Algorithms to use
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
# Metrics to evaluate the model
from sklearn import metrics
from sklearn.metrics import confusion_matrix, classification_report, precision_score, recall_score, f1_score, accuracy_score, roc_auc_score
# To ignore warnings
import warnings
warnings.filterwarnings("ignore")
In [33]:
# Helper function to print performance metrics
def metrics_score(y_true, y_pred):
print("Accuracy:", accuracy_score(y_true, y_pred))
print("Precision:", precision_score(y_true, y_pred))
print("Recall:", recall_score(y_true, y_pred))
print("F1-Score:", f1_score(y_true, y_pred))
In [35]:
# Load and inspect dataset
df = pd.read_excel('Telco_customer_churn.xlsx')
print("Initial rows:")
print(df.head())
print("\nData info:")
df.info()
# Map Churn label to Churn value if needed
if 'Churn Value' not in df.columns and 'Churn Label' in df.columns:
df['Churn Value'] = df['Churn Label'].map({'Yes': 1, 'No': 0})
target_col = 'Churn Value'
if target_col not in df.columns:
raise ValueError("Target column 'Churn Value' not found. Ensure churn indicator is available.")
# Drop unnecessary columns
drop_cols = ['CustomerID', 'Lat Long', 'Churn Reason', 'Churn Label']
for col in drop_cols:
if col in df.columns:
df.drop(col, axis=1, inplace=True)
# Check for missing values
print("\nMissing values:\n", df.isnull().sum())
df.dropna(inplace=True)
# Check class distribution
print("\nChurn value distribution:")
print(df[target_col].value_counts())
if df[target_col].nunique() < 2:
raise ValueError("Only one class present in the data. Need both classes for classification.")
Initial rows:
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]
Data 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
Missing values:
Count 0
Country 0
State 0
City 0
Zip Code 0
Latitude 0
Longitude 0
Gender 0
Senior Citizen 0
Partner 0
Dependents 0
Tenure Months 0
Phone Service 0
Multiple Lines 0
Internet Service 0
Online Security 0
Online Backup 0
Device Protection 0
Tech Support 0
Streaming TV 0
Streaming Movies 0
Contract 0
Paperless Billing 0
Payment Method 0
Monthly Charges 0
Total Charges 0
Churn Value 0
Churn Score 0
CLTV 0
dtype: int64
Churn value distribution:
Churn Value
0 5174
1 1869
Name: count, dtype: int64
In [37]:
# Identify numeric and categorical columns
numeric_columns = df.select_dtypes(include=np.number).columns.tolist()
categorical_columns = df.select_dtypes(exclude=np.number).columns.tolist()
if target_col in categorical_columns:
categorical_columns.remove(target_col)
print("\nNumeric columns:", numeric_columns)
print("Categorical columns:", categorical_columns)
Numeric columns: ['Count', 'Zip Code', 'Latitude', 'Longitude', 'Tenure Months', 'Monthly Charges', 'Churn Value', 'Churn Score', 'CLTV'] Categorical columns: ['Country', 'State', 'City', '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', 'Total Charges']
In [39]:
# Print summary statistics for numeric columns
print("\nSummary statistics for numeric columns:")
print(df[numeric_columns].describe().T)
Summary statistics for numeric columns:
count mean std min \
Count 7043.0 1.000000 0.000000 1.000000
Zip Code 7043.0 93521.964646 1865.794555 90001.000000
Latitude 7043.0 36.282441 2.455723 32.555828
Longitude 7043.0 -119.798880 2.157889 -124.301372
Tenure Months 7043.0 32.371149 24.559481 0.000000
Monthly Charges 7043.0 64.761692 30.090047 18.250000
Churn Value 7043.0 0.265370 0.441561 0.000000
Churn Score 7043.0 58.699418 21.525131 5.000000
CLTV 7043.0 4400.295755 1183.057152 2003.000000
25% 50% 75% max
Count 1.000000 1.000000 1.000000 1.000000
Zip Code 92102.000000 93552.000000 95351.000000 96161.000000
Latitude 34.030915 36.391777 38.224869 41.962127
Longitude -121.815412 -119.730885 -118.043237 -114.192901
Tenure Months 9.000000 29.000000 55.000000 72.000000
Monthly Charges 35.500000 70.350000 89.850000 118.750000
Churn Value 0.000000 0.000000 1.000000 1.000000
Churn Score 40.000000 61.000000 75.000000 100.000000
CLTV 3469.000000 4527.000000 5380.500000 6500.000000
In [41]:
# Data preparation
# One-hot encode categorical variables
categorical_columns = [col for col in categorical_columns if col in df.columns]
df = pd.get_dummies(df, columns=categorical_columns, drop_first=True)
# Separate features and target
y = df[target_col]
X = df.drop(columns=[target_col])
# Train-test split with stratification
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)
print("\nClass distribution in y_train:")
print(y_train.value_counts())
if y_train.nunique() < 2:
raise ValueError("After splitting, training set has only one class. Check data stratification or data.")
# Scale numeric features
sc = StandardScaler()
final_numeric_cols = [col for col in X_train.columns if np.issubdtype(X_train[col].dtype, np.number)]
X_train_scaled = X_train.copy()
X_train_scaled[final_numeric_cols] = sc.fit_transform(X_train[final_numeric_cols])
X_test_scaled = X_test.copy()
X_test_scaled[final_numeric_cols] = sc.transform(X_test[final_numeric_cols])
Class distribution in y_train: Churn Value 0 3622 1 1308 Name: count, dtype: int64
In [43]:
# Logistic Regression
lr = LogisticRegression(max_iter=200)
lr.fit(X_train_scaled, y_train)
y_pred_lr = lr.predict(X_test_scaled)
y_prob_lr = lr.predict_proba(X_test_scaled)[:,1]
print("\n*** Logistic Regression ***")
print("Classification report:")
print(classification_report(y_test, y_pred_lr))
print("Metrics:")
metrics_score(y_test, y_pred_lr)
print("AUC-ROC:", roc_auc_score(y_test, y_prob_lr))
*** Logistic Regression ***
Classification report:
precision recall f1-score support
0 0.94 0.95 0.94 1552
1 0.85 0.83 0.84 561
accuracy 0.92 2113
macro avg 0.90 0.89 0.89 2113
weighted avg 0.92 0.92 0.92 2113
Metrics:
Accuracy: 0.9176526265972551
Precision: 0.8524590163934426
Recall: 0.8342245989304813
F1-Score: 0.8432432432432433
AUC-ROC: 0.9704756785563335
In [45]:
# Reducing dataset size for SVM to speed up training
X_train_small = X_train_scaled[:200]
y_train_small = y_train[:200]
# SVM
svm = SVC(probability=True, kernel='linear')
svm.fit(X_train_small, y_train_small) # using small to faster testing can be changed to scaled
y_pred_svm = svm.predict(X_test_scaled)
y_prob_svm = svm.predict_proba(X_test_scaled)[:,1]
print("\n*** SVM (with smaller data) ***")
print("Classification report:")
print(classification_report(y_test, y_pred_svm))
print("Metrics:")
metrics_score(y_test, y_pred_svm)
print("AUC-ROC:", roc_auc_score(y_test, y_prob_svm))
*** SVM (with smaller data) ***
Classification report:
precision recall f1-score support
0 0.94 0.92 0.93 1552
1 0.80 0.83 0.81 561
accuracy 0.90 2113
macro avg 0.87 0.88 0.87 2113
weighted avg 0.90 0.90 0.90 2113
Metrics:
Accuracy: 0.898248935163275
Precision: 0.7962328767123288
Recall: 0.8288770053475936
F1-Score: 0.8122270742358079
AUC-ROC: 0.9606838166381829
In [47]:
# Logistic Regression tuning
param_grid_lr = {
'C': [0.01, 0.1, 1, 10, 100],
'penalty': ['l2']
}
grid_lr = GridSearchCV(LogisticRegression(max_iter=200), param_grid_lr, cv=5, scoring='f1', n_jobs=-1)
grid_lr.fit(X_train_scaled, y_train)
best_lr = grid_lr.best_estimator_
y_pred_best_lr = best_lr.predict(X_test_scaled)
y_prob_best_lr = best_lr.predict_proba(X_test_scaled)[:,1]
print("\n*** Tuned Logistic Regression ***")
print("Best params:", grid_lr.best_params_)
print("Classification report:")
print(classification_report(y_test, y_pred_best_lr))
print("Metrics:")
metrics_score(y_test, y_pred_best_lr)
print("AUC-ROC:", roc_auc_score(y_test, y_prob_best_lr))
*** Tuned Logistic Regression ***
Best params: {'C': 0.1, 'penalty': 'l2'}
Classification report:
precision recall f1-score support
0 0.94 0.95 0.95 1552
1 0.86 0.83 0.85 561
accuracy 0.92 2113
macro avg 0.90 0.89 0.90 2113
weighted avg 0.92 0.92 0.92 2113
Metrics:
Accuracy: 0.9200189304306673
Precision: 0.8643122676579925
Recall: 0.8288770053475936
F1-Score: 0.8462238398544131
AUC-ROC: 0.9709017862065162
In [49]:
# SVM tuning
param_grid_svm = {
'C': [0.1, 1, 10],
'kernel': ['linear', 'rbf'],
'gamma': ['scale', 'auto']
}
grid_svm = GridSearchCV(SVC(probability=True), param_grid_svm, cv=5, scoring='f1', n_jobs=-1)
grid_svm.fit(X_train_small, y_train_small) # using small to faster testing can be used with scaled
best_svm = grid_svm.best_estimator_
y_pred_best_svm = best_svm.predict(X_test_scaled)
y_prob_best_svm = best_svm.predict_proba(X_test_scaled)[:,1]
print("\n*** Tuned SVM ***")
print("Best params:", grid_svm.best_params_)
print("Classification report:")
print(classification_report(y_test, y_pred_best_svm))
print("Metrics:")
metrics_score(y_test, y_pred_best_svm)
print("AUC-ROC:", roc_auc_score(y_test, y_prob_best_svm))
*** Tuned SVM ***
Best params: {'C': 0.1, 'gamma': 'scale', 'kernel': 'linear'}
Classification report:
precision recall f1-score support
0 0.95 0.92 0.94 1552
1 0.80 0.85 0.83 561
accuracy 0.91 2113
macro avg 0.87 0.89 0.88 2113
weighted avg 0.91 0.91 0.91 2113
Metrics:
Accuracy: 0.9058211074301941
Precision: 0.8036912751677853
Recall: 0.8538324420677362
F1-Score: 0.8280034572169404
AUC-ROC: 0.9643717726078249
In [51]:
# Analyze Impact of Hyperparameter Tuning
print("\n*** Impact of Hyperparameter tuning ***")
print("LR F1 (Basic vs Tuned):",
f1_score(y_test, y_pred_lr), "->", f1_score(y_test, y_pred_best_lr))
print("SVM F1 (Basic vs Tuned):",
f1_score(y_test, y_pred_svm), "->", f1_score(y_test, y_pred_best_svm))
*** Impact of Hyperparameter tuning *** LR F1 (Basic vs Tuned): 0.8432432432432433 -> 0.8462238398544131 SVM F1 (Basic vs Tuned): 0.8122270742358079 -> 0.8280034572169404
In [53]:
# Check feature importance for LR
cols = X.columns
coef_lg = best_lr.coef_
imp_features = pd.DataFrame(coef_lg, columns=cols).T.sort_values(by=0, ascending=False)
print("\nTop 10 important features from tuned LR:")
print(imp_features.head(10))
# Actionable Insights:
# Features with the highest positive coefficients are most strongly associated with churn (1).
# If "Contract_Month-to-month" is near the top, it suggests month-to-month customers are more likely to churn.
# Business can consider offering discounts or longer-term incentives to these customers.
# If "InternetService_Fiber optic" is highly positive, customers on fiber might be leaving due to cost or competition.
# Consider loyalty programs, improved customer support, or bundle discounts to reduce churn.
# Visualize confusion matrices for tuned models
fig, axes = plt.subplots(1, 2, figsize=(12,5))
cm_lr = confusion_matrix(y_test, y_pred_best_lr)
cm_svm = confusion_matrix(y_test, y_pred_best_svm)
sns.heatmap(cm_lr, annot=True, fmt='d', cmap='Blues', ax=axes[0])
axes[0].set_title('Tuned LR confusion Matrix')
axes[0].set_xlabel('Predicted')
axes[0].set_ylabel('Actual')
sns.heatmap(cm_svm, annot=True, fmt='d', cmap='Greens', ax=axes[1])
axes[1].set_title('Tuned SVM confusion Matrix')
axes[1].set_xlabel('Predicted')
axes[1].set_ylabel('Actual')
plt.tight_layout()
plt.show()
print("\nRecommendations:")
print("Target customers in segments with high churn probability (identified via feature importance) with retention strategies.")
print("Adjust pricing or offer bundles for services that correlate with higher churn.")
print("Encourage customers on month-to-month contracts to switch to longer-term plans.")
print("Further analyze why certain service types cause churn and enhance quality or pricing.")
print("**************************************************************************************************************** 1802408 Osvaldas Stanelis")
Top 10 important features from tuned LR:
0
Churn Score 3.485971
Internet Service_Fiber optic 0.493426
Payment Method_Electronic check 0.469557
Senior Citizen_Yes 0.332098
Paperless Billing_Yes 0.306132
Monthly Charges 0.282684
City_San Diego 0.274664
City_Glendale 0.238260
Streaming TV_Yes 0.220997
City_Sacramento 0.196563
Recommendations: Target customers in segments with high churn probability (identified via feature importance) with retention strategies. Adjust pricing or offer bundles for services that correlate with higher churn. Encourage customers on month-to-month contracts to switch to longer-term plans. Further analyze why certain service types cause churn and enhance quality or pricing. **************************************************************************************************************** 1802408 Osvaldas Stanelis
In [ ]: