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  
No description has been provided for this image
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()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
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()
No description has been provided for this image
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

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

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

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

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

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

No description has been provided for this image