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
No description has been provided for this image
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 [ ]: