Predict Customer Churn with Python
Train a Logistic Regression model to predict telecom customer churn.
Introduction
30 Second Summary
Every subscription service loses customers. The companies that thrive are the ones that can spot who is about to leave before they actually do, giving the retention team a chance to step in with the right offer at the right time.
In this project, you will build a machine learning model in Python that predicts which telecom customers are likely to cancel their service. You will clean real customer data, train a classifier, evaluate its performance, and translate the results into business recommendations.
What You'll Build
A complete customer churn prediction pipeline in a Google Colab notebook using real telecom customer data and scikit-learn.
By the end of this project, you'll have:
- A trained Logistic Regression model that predicts customer churn on real data from 7,043 telecom customers.
- A full set of classification metrics (accuracy, precision, recall, F1-score, confusion matrix) evaluated on held-out test data.
- Actionable business recommendations grounded in your data findings that connect model results to concrete retention strategies.
- Secret Mission: Use Claude to transform your technical findings into a polished 1-page executive summary with plain-English explanations and a concrete retention recommendation.
Are there any prerequisites?
No prior machine learning experience is needed. You should be comfortable reading basic Python (variables, loops, functions), but this guide walks you through every step including environment setup. All you need is a Google account and a web browser.
Before We Start
Before We Start...
Set Up Google Colab and Load the Dataset
Every prediction model starts with data. Before you can train a model to spot customers who are about to leave, you need a workspace to write code and a dataset to analyze.
In this step, you will open Google Colab (a free, browser-based coding notebook) and load a real-world customer dataset from IBM's public repository.
In this step, get ready to:
- Open Google Colab and create a new notebook.
- Load the IBM Telco Customer Churn dataset.
- Inspect the data to confirm it loaded correctly.
Open Google Colab and create a new notebook
Google Colab gives you a free Python notebook in your browser with all the data science libraries pre-installed. No downloads needed.
- Open your web browser and navigate to colab.research.google.com.
- Sign in with your Google account if prompted.
- Click New notebook to create a fresh notebook.
- Click the notebook title at the top left (it says Untitled0.ipynb by default).
- Type Churn_Prediction_Project and press Enter to rename it.
What is Google Colab?
Google Colab is a free, cloud-based notebook environment from Google. It runs Python code in your browser and comes with libraries like pandas, scikit-learn, and matplotlib already installed.
Your notebooks save automatically to Google Drive, so you can close the tab and come back anytime.
Load the dataset directly from IBM's GitHub repository
The IBM Telco Customer Churn dataset contains records for 7,043 telecom customers. Each row includes details like contract type, internet service, tenure, and whether the customer left (churned).
- Import the project libraries and load the dataset by pasting this code into the first code cell:
# Cell 1: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# Load dataset from IBM's GitHub
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
# Initial inspection
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()
What does this code do?
- The import lines bring in pandas (for data manipulation), numpy (for math), matplotlib and seaborn (for charts), and scikit-learn modules (for machine learning).
- pd.read_csv(url) downloads the CSV file from IBM's GitHub and loads it into a DataFrame called df.
- The print statements show the dataset's shape (rows and columns), all column names, and the first five rows so you can verify the data arrived correctly.
- Run the cell by clicking the play button on the left side of the cell, or by pressing Shift+Enter.
You should see output showing Dataset shape: (7043, 21) followed by a list of 21 column names and a table of the first five rows.
Seeing a connection error?
If the cell throws a network error, Colab may not be connected to a runtime yet. Look for a Connect button in the top right corner of the page and click it. Wait for the green checkmark, then re-run the cell.
help me fix a connection error when loading a CSV from a URL in Google Colab
Inspect the data for quality and structure
Before building a model, you need to understand what you are working with. A quick inspection reveals data types, missing values, and how many customers actually churned.
- Click the + Code button below your first cell to add a new code cell.
- Run the data inspection by pasting this code into the new cell:
# Cell 2: Data inspection
print(df.info())
print(f"\nMissing values:\n{df.isnull().sum().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")
print(f"\nChurn rate: {df['Churn'].value_counts(normalize=True)['Yes']:.1%}")
What does this code do?
- df.info() prints every column's data type and how many non-null values it has. This helps you spot columns that need cleaning.
- df.isnull().sum().sum() gives a single number showing total missing values across the entire dataset.
- df['Churn'].value_counts() counts how many customers stayed vs left. The normalize=True version converts that into a percentage.
- Run the cell by pressing Shift+Enter.
You should see a churn rate of approximately 26.5%. This means roughly 1 in 4 customers left the company.
Not seeing 21 columns in df.info()?
Make sure Cell 1 ran successfully first. If df is not defined, Colab will throw a NameError. Go back to Cell 1, re-run it, then re-run Cell 2.
help me fix a NameError for df in Google Colab
✔️ Awesome, I've got everything!
Great. Make sure both cells have run successfully and you can see the churn rate output before moving on.
ⓧ I'd like to double check the full code
Here is the full code across both cells. Compare it against what you have in your notebook.
# Cell 1: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# Load dataset from IBM's GitHub
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
# Initial inspection
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()
# Cell 2: Data inspection
print(df.info())
print(f"\nMissing values:\n{df.isnull().sum().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")
print(f"\nChurn rate: {df['Churn'].value_counts(normalize=True)['Yes']:.1%}")
Your workspace is ready and your data is loaded. Next up, you will explore the data visually to uncover which features drive customers to leave.
Explore and Understand the Data
Your data is loaded and you know its basic shape: 7,043 customers, 21 features, and a target column called Churn. Before building a model, you need to understand what the data is actually telling you.
In this step, you'll explore the dataset visually to uncover patterns. Which customers are leaving, and what do they have in common? These insights will guide every modeling decision you make next.
In this step, get ready to:
- Visualize the churn distribution and note the class imbalance.
- Explore key features to identify the top churn drivers.
Check the churn distribution
Exploratory Data Analysis (EDA) is the process of visualizing and summarizing a dataset before modeling. Your first visualization will show how many customers churned versus how many stayed.
- Create a new code cell below Cell 2 by clicking the + Code button in the toolbar.
- Visualize the churn distribution with a count plot by pasting this code into the new cell:
# Cell 3: Visualize churn distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(data=df, x='Churn', ax=ax)
ax.set_title('Customer Churn Distribution')
ax.set_xlabel('Churn')
ax.set_ylabel('Count')
plt.tight_layout()
plt.show()
What does this code do?
This cell creates a bar chart using seaborn's countplot function. It counts how many rows have each value in the Churn column and draws a bar for each.
The figsize=(6, 4) controls the chart dimensions. plt.tight_layout() ensures labels don't get cut off.
- Run the cell by pressing Shift+Enter.
- You should see a bar chart with two bars. The "No" bar (customers who stayed) is much taller than the "Yes" bar (customers who churned).
What is class imbalance?
Your chart shows roughly 73% of customers stayed and only 27% churned. This is called class imbalance. The model will see far more "stayed" examples than "churned" examples during training.
This matters because a model could achieve 73% accuracy by simply predicting "no churn" for everyone. You'll need metrics beyond accuracy (like recall) to judge whether your model is actually useful.
Not seeing a chart?
Make sure you ran Cells 1 and 2 first. The df variable and the sns import must exist in memory before Cell 3 can use them.
If you see a NameError, click Runtime > Run all to re-execute all cells from the top.
Help me debug my Colab chart
Explore key features vs churn
Knowing that 27% of customers churn is a starting point. The real question is: what makes churners different from loyal customers? You'll compare churn rates across three key features to find out.
- Create another new code cell below Cell 3.
- Explore churn by contract type, internet service, and tenure by pasting this code:
# Cell 4: Explore churn by contract type
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Contract type vs churn
contract_churn = df.groupby('Contract')['Churn'].value_counts(normalize=True).unstack()
contract_churn['Yes'].plot(kind='bar', ax=axes[0], color='coral')
axes[0].set_title('Churn Rate by Contract Type')
axes[0].set_ylabel('Churn Rate')
axes[0].set_xticklabels(axes[0].get_xticklabels(), rotation=45)
# Internet service vs churn
internet_churn = df.groupby('InternetService')['Churn'].value_counts(normalize=True).unstack()
internet_churn['Yes'].plot(kind='bar', ax=axes[1], color='coral')
axes[1].set_title('Churn Rate by Internet Service')
axes[1].set_ylabel('Churn Rate')
axes[1].set_xticklabels(axes[1].get_xticklabels(), rotation=45)
# Tenure distribution by churn
sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[2])
axes[2].set_title('Tenure Distribution by Churn')
plt.tight_layout()
plt.show()
print("Key findings:")
print(f" Month-to-month churn rate: {contract_churn.loc['Month-to-month', 'Yes']:.1%}")
print(f" Two year contract churn rate: {contract_churn.loc['Two year', 'Yes']:.1%}")
print(f" Fiber optic churn rate: {internet_churn.loc['Fiber optic', 'Yes']:.1%}")
What does this code do?
- Chart 1 (left): Groups customers by Contract type, calculates the churn rate for each group, and plots them as bars.
- Chart 2 (middle): Does the same for InternetService type.
- Chart 3 (right): Shows the distribution of tenure (months as a customer) split by whether the customer churned. The kde=True parameter adds a smooth curve to make the shape easier to read.
- The print statements at the bottom extract the exact churn percentages so you have concrete numbers to reference later.
- Run the cell by pressing Shift+Enter.
- You should see three charts side by side. Below the charts, you should see printed output showing the month-to-month churn rate around 42%, two-year contract churn rate around 3%, and fiber optic churn rate around 42%.
What do these findings mean?
Three patterns stand out from these charts:
- Contract type: Month-to-month customers churn at ~42%, while two-year contract customers churn at only ~3%. Commitment reduces churn dramatically.
- Internet service: Fiber optic customers churn at ~42%, roughly double the rate of DSL customers (~19%). This suggests a gap between expectations and service quality.
- Tenure: The histogram shows churned customers cluster heavily in the first 12 months. Long-tenured customers rarely leave.
Charts look squished or overlapping?
Make sure your figsize=(15, 4) is set on the plt.subplots line. If the figure is too narrow, the three subplots will overlap.
If you see a KeyError on the column name, double-check that you're using the exact column names from the dataset: Contract, InternetService, and tenure (case-sensitive).
Help me fix my subplot charts
✔️ Awesome, I've got everything!
Great. Make sure both Cell 3 and Cell 4 have run successfully and you can see the charts and printed key findings.
ⓧ I'd like to double check the full code
Here is all the code you should have in your notebook so far (Cells 1 through 4):
# Cell 1: Import libraries and load data
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# Load dataset from IBM's GitHub
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
# Initial inspection
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()
# Cell 2: Data inspection
print(df.info())
print(f"\nMissing values:\n{df.isnull().sum().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")
print(f"\nChurn rate: {df['Churn'].value_counts(normalize=True)['Yes']:.1%}")
# Cell 3: Visualize churn distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(data=df, x='Churn', ax=ax)
ax.set_title('Customer Churn Distribution')
ax.set_xlabel('Churn')
ax.set_ylabel('Count')
plt.tight_layout()
plt.show()
# Cell 4: Explore churn by contract type
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Contract type vs churn
contract_churn = df.groupby('Contract')['Churn'].value_counts(normalize=True).unstack()
contract_churn['Yes'].plot(kind='bar', ax=axes[0], color='coral')
axes[0].set_title('Churn Rate by Contract Type')
axes[0].set_ylabel('Churn Rate')
axes[0].set_xticklabels(axes[0].get_xticklabels(), rotation=45)
# Internet service vs churn
internet_churn = df.groupby('InternetService')['Churn'].value_counts(normalize=True).unstack()
internet_churn['Yes'].plot(kind='bar', ax=axes[1], color='coral')
axes[1].set_title('Churn Rate by Internet Service')
axes[1].set_ylabel('Churn Rate')
axes[1].set_xticklabels(axes[1].get_xticklabels(), rotation=45)
# Tenure distribution by churn
sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[2])
axes[2].set_title('Tenure Distribution by Churn')
plt.tight_layout()
plt.show()
print("Key findings:")
print(f" Month-to-month churn rate: {contract_churn.loc['Month-to-month', 'Yes']:.1%}")
print(f" Two year contract churn rate: {contract_churn.loc['Two year', 'Yes']:.1%}")
print(f" Fiber optic churn rate: {internet_churn.loc['Fiber optic', 'Yes']:.1%}")
You've identified the main churn drivers: short contracts, fiber optic internet, and low tenure. Next up, you'll clean and preprocess this data so a machine learning model can learn from it.
Preprocess the Data for Modeling
You now know the key patterns in your data. Month-to-month contracts, fiber optic internet, and low tenure are the biggest churn drivers.
Knowing the patterns is only half the job. Machine learning models cannot work with text like "Yes" or "Month-to-month." In this step, you will clean up data quality issues and convert every column to a number so the model can learn from it.
In this step, get ready to:
- Fix data quality issues in TotalCharges, customerID, and Churn.
- Encode all categorical features into numeric columns.
- Split the data into training and test sets.
Fix data quality issues
Before the model can learn from the data, you need to address three problems. The TotalCharges column contains blank spaces instead of numbers for some new customers. The customerID column is a random identifier that would confuse the model. The Churn column uses text ("Yes"/"No") instead of numbers.
- Click the + Code button below your last cell to create a new code cell.
- Paste this code into the new cell:
# Cell 5: Data preprocessing
# Fix TotalCharges (contains blank spaces for new customers)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'].fillna(df['TotalCharges'].median(), inplace=True)
# Drop customerID
df.drop('customerID', axis=1, inplace=True)
# Convert Churn to binary
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
What does this code do?
pd.to_numeric(df['TotalCharges'], errors='coerce') attempts to convert every value to a number. Blank spaces cannot be converted, so errors='coerce' turns them into NaN (missing) instead of crashing.
fillna() replaces those missing values with the column's median. Dropping customerID removes a random string that has no predictive value. Mapping Churn to 1/0 gives the model a numeric target to predict.
You will not run this cell yet. The next chunk completes it with the encoding step.
Encode categorical features
Columns like gender, Partner, Contract, and InternetService still contain text values. One-hot encoding converts each unique text value into its own binary (0/1) column so the model can work with it.
- Below the code you just pasted in the same cell, add these lines:
# One-hot encode categorical features
df_encoded = pd.get_dummies(df, drop_first=True)
print(f"Encoded dataset shape: {df_encoded.shape}")
print(f"All numeric: {df_encoded.dtypes.value_counts()}")
What does this code do?
pd.get_dummies(df, drop_first=True) creates a new column for each unique category value. For example, the Contract column (with values Month-to-month, One year, Two year) becomes two new binary columns.
drop_first=True removes one category per column to avoid redundancy. If a customer is not "One year" and not "Two year," they must be "Month-to-month." The model can infer the dropped category from the others.
- Run the cell by pressing Shift+Enter.
- Confirm the output shows the encoded dataset shape and that all columns are numeric (you should see only int and float types in the dtype count).
Seeing a KeyError or unexpected shape?
If you get a KeyError: 'TotalCharges', you may have run the cell twice. The first run already modified the DataFrame. Go to Runtime > Run all to re-execute from the top.
If your shape shows fewer than 7043 rows, check that you used errors='coerce' in the pd.to_numeric() call. Without it, the blank spaces cause the entire line to fail.
Still stuck? Help me debug my data preprocessing cell in Google Colab.
Split into training and test sets
A model that memorizes the training data will perform poorly on new customers. To measure real-world performance, you hold out 20% of the data as a test set that the model never sees during training.
- Create a new code cell by clicking + Code below Cell 5.
- Paste this code and run it with Shift+Enter:
# Cell 6: Train/test split
X = df_encoded.drop('Churn', axis=1)
y = df_encoded['Churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")
print(f"Churn rate in train: {y_train.mean():.1%}")
print(f"Churn rate in test: {y_test.mean():.1%}")
What does this code do?
X holds all the feature columns (everything except Churn). y holds the target column (Churn) that the model will learn to predict.
train_test_split randomly divides the data into 80% training and 20% testing. random_state=42 makes the split reproducible. stratify=y ensures both sets have the same churn ratio (~27%), so the test set is representative.
- Confirm your output shows approximately 5634 training samples and 1409 test samples.
- Verify that the churn rate is approximately 26-27% in both the training and test sets.
Numbers don't match?
If you see a NameError: name 'df_encoded' is not defined, Cell 5 did not run successfully. Click on Cell 5 and press Shift+Enter to run it first, then re-run Cell 6.
If your sample counts are different, make sure you did not accidentally drop rows earlier. Your df_encoded should have 7043 rows before the split.
Need a hand? Help me troubleshoot my train/test split in Google Colab.
✔️ Awesome, I've got everything!
Great. Double check that both Cell 5 and Cell 6 ran without errors and that your output matches the expected values.
ⓧ I'd like to double check the full code
Here is the complete code for both cells in this step.
Cell 5:
# Cell 5: Data preprocessing
# Fix TotalCharges (contains blank spaces for new customers)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'].fillna(df['TotalCharges'].median(), inplace=True)
# Drop customerID
df.drop('customerID', axis=1, inplace=True)
# Convert Churn to binary
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
# One-hot encode categorical features
df_encoded = pd.get_dummies(df, drop_first=True)
print(f"Encoded dataset shape: {df_encoded.shape}")
print(f"All numeric: {df_encoded.dtypes.value_counts()}")
Cell 6:
# Cell 6: Train/test split
X = df_encoded.drop('Churn', axis=1)
y = df_encoded['Churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Training set: {X_train.shape[0]} samples")
print(f"Test set: {X_test.shape[0]} samples")
print(f"Churn rate in train: {y_train.mean():.1%}")
print(f"Churn rate in test: {y_test.mean():.1%}")
Your data is fully numeric and split into training and test sets. Next up, you will train a Logistic Regression model on this preprocessed data and see how well it predicts churn.
Train a Logistic Regression Model
You have clean, numeric data split into training and test sets. Now it is time to train a model that learns patterns from the training data and predicts which customers will churn.
In this step, you will scale the features, train a Logistic Regression model, and evaluate its performance using standard classification metrics. The metrics will tell you how well the model catches at-risk customers.
In this step, get ready to:
- Scale numeric features and train the model.
- Make predictions and evaluate with classification metrics.
- Interpret the confusion matrix in business terms.
Scale numeric features and train the model
Features like MonthlyCharges and tenure have very different ranges. Logistic Regression performs better when all features are on the same scale, so you need to standardize them first.
- Create a new code cell (Cell 7) and scale the features, then train the model by pasting this code:
# Cell 7: Scale features and train model
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_train_scaled, y_train)
print("Model trained successfully!")
What does this code do?
The code has two main parts: scaling and training.
- fit_transform(X_train) learns the mean and standard deviation from the training data, then scales it. transform(X_test) applies those same statistics to the test data without learning anything new from it.
- max_iter=1000 gives the model enough iterations to converge on a solution. The default of 100 is sometimes not enough for datasets with many features.
- random_state=42 makes the results reproducible so you get the same output every time you run the cell.
Why fit on training data only?
This prevents data leakage. If you fit the scaler on the entire dataset (including test data), the model indirectly "sees" test data during training. This inflates performance metrics and gives you a false sense of how well the model will work on new customers.
- Run Cell 7 by pressing Shift+Enter.
- You should see Model trained successfully! printed below the cell.
Seeing a convergence warning?
If you see a warning about the solver not converging, double-check that you typed max_iter=1000 (not 100). A higher iteration limit gives the algorithm more time to find the optimal solution.
help me fix a convergence warning in my Logistic Regression model
Make predictions and evaluate
Your model is trained. Now you need to see how well it performs on data it has never seen before.
- Create a new code cell (Cell 8) and generate predictions with evaluation metrics by pasting this code:
# Cell 8: Evaluate the model
y_pred = model.predict(X_test_scaled)
print("=" * 50)
print("MODEL EVALUATION RESULTS")
print("=" * 50)
print(f"\nAccuracy: {accuracy_score(y_test, y_pred):.3f}")
print(f"\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=['Retained', 'Churned']))
What do these metrics mean?
- Accuracy: the percentage of all predictions (both retained and churned) that the model got right.
- Precision (for Churned): of all customers the model flagged as churners, what percentage actually churned. High precision means fewer false alarms.
- Recall (for Churned): of all customers who actually churned, what percentage did the model catch. High recall means fewer missed churners.
- F1-score: the balance between precision and recall. Useful when you care about both catching churners and avoiding false alarms.
- Run Cell 8 by pressing Shift+Enter.
- You should see an accuracy score and a full classification report with precision, recall, and F1-score for both the Retained and Churned classes.
Seeing a NameError?
If you see NameError: name 'X_test_scaled' is not defined, make sure you ran Cell 7 first. Each cell depends on variables created in previous cells.
help me fix a NameError in my Colab notebook
Interpret the confusion matrix
The classification report gives you numbers, but a confusion matrix shows you exactly where the model gets things right and where it makes mistakes.
- Create a new code cell (Cell 9) and visualize the confusion matrix by pasting this code:
# Cell 9: Confusion matrix visualization
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Predicted Retained', 'Predicted Churned'],
yticklabels=['Actually Retained', 'Actually Churned'],
ax=ax)
ax.set_title('Confusion Matrix')
ax.set_ylabel('True Label')
ax.set_xlabel('Predicted Label')
plt.tight_layout()
plt.show()
# Business interpretation
total_churners = cm[1][0] + cm[1][1]
caught_churners = cm[1][1]
missed_churners = cm[1][0]
print(f"\nBusiness Interpretation:")
print(f" Total actual churners in test set: {total_churners}")
print(f" Churners correctly identified: {caught_churners} ({caught_churners/total_churners:.1%})")
print(f" Churners missed: {missed_churners} ({missed_churners/total_churners:.1%})")
What does the confusion matrix show?
The matrix is a 2x2 grid. Each cell counts how many predictions fell into that category.
- Top-left (true negatives): customers who stayed and the model correctly predicted they would stay.
- Top-right (false positives): customers who stayed but the model incorrectly flagged as churners.
- Bottom-left (false negatives): customers who churned but the model missed them.
- Bottom-right (true positives): customers who churned and the model correctly caught them.
The business interpretation at the bottom translates this into actionable numbers: how many at-risk customers your model catches versus how many slip through.
- Run Cell 9 by pressing Shift+Enter.
- You should see a blue heatmap with numbers in each quadrant, followed by a business interpretation showing how many churners the model caught and missed.
Heatmap not displaying?
Make sure Cell 1 (which imports matplotlib and seaborn) was run successfully. If the chart appears blank, try adding %matplotlib inline at the top of the cell and re-running.
help me display a seaborn heatmap in Google Colab
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook, then move on to the next step.
ⓧ I'd like to double check the full code
Here are all three cells from this step. Compare them against your notebook:
# Cell 7: Scale features and train model
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = LogisticRegression(max_iter=1000, random_state=42)
model.fit(X_train_scaled, y_train)
print("Model trained successfully!")
# Cell 8: Evaluate the model
y_pred = model.predict(X_test_scaled)
print("=" * 50)
print("MODEL EVALUATION RESULTS")
print("=" * 50)
print(f"\nAccuracy: {accuracy_score(y_test, y_pred):.3f}")
print(f"\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=['Retained', 'Churned']))
# Cell 9: Confusion matrix visualization
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['Predicted Retained', 'Predicted Churned'],
yticklabels=['Actually Retained', 'Actually Churned'],
ax=ax)
ax.set_title('Confusion Matrix')
ax.set_ylabel('True Label')
ax.set_xlabel('Predicted Label')
plt.tight_layout()
plt.show()
# Business interpretation
total_churners = cm[1][0] + cm[1][1]
caught_churners = cm[1][1]
missed_churners = cm[1][0]
print(f"\nBusiness Interpretation:")
print(f" Total actual churners in test set: {total_churners}")
print(f" Churners correctly identified: {caught_churners} ({caught_churners/total_churners:.1%})")
print(f" Churners missed: {missed_churners} ({missed_churners/total_churners:.1%})")
You now have a trained model with concrete performance metrics. Next up, you will interpret these results in a business context and write recommendations that a stakeholder could act on.
Interpret Results in a Business Context
Your model is trained and evaluated. You know its accuracy, precision, recall, and where the confusion matrix shows gaps.
Now comes the part that separates a technical exercise from real analyst work: translating those numbers into language a business stakeholder can act on. In this step, you will connect your model's performance back to the churn drivers you discovered in EDA and write concrete recommendations.
In this step, get ready to:
- Summarize your model's performance alongside the top churn drivers.
- Write actionable business recommendations grounded in your data findings.
Summarize your findings
A model's value comes from what it tells the business, not from its raw metrics. Before writing code, identify the three things a stakeholder needs to hear.
- Recall the top 3 churn drivers from your EDA in Step 2: contract type (month-to-month at ~42% churn vs ~3% for two-year), internet service (fiber optic at ~42% vs DSL at ~19%), and tenure (churners cluster in the first 12 months).
- Note your model's recall for the churn class from Cell 9. This tells you what percentage of at-risk customers the model catches.
- Think about business impact: with a ~27% churn rate, how many at-risk customers out of 100 would your model successfully flag for the retention team?
Why translate metrics into business language?
A VP of Customer Success does not care about F1-scores. They care about how many customers they can save and what action to take. Your job as an analyst is to bridge technical output and business decisions.
The recall score answers their real question: "If 100 customers are about to leave, how many will this model warn us about in time?"
Write a brief business recommendation
Each recommendation should trace directly back to a specific data finding. A recommendation without evidence is just an opinion.
- Create a new code cell below Cell 9 and paste the following code to produce your business summary:
# Cell 10: Business summary and recommendations
print("=" * 50)
print("BUSINESS SUMMARY")
print("=" * 50)
print(f"""
TOP CHURN DRIVERS (from EDA):
1. Contract type: Month-to-month customers churn at ~42% vs ~3% for two-year contracts
2. Internet service: Fiber optic customers churn at ~42% vs ~19% for DSL
3. Tenure: Nearly half of churners leave within their first 12 months
MODEL PERFORMANCE:
- The model correctly identifies {caught_churners/total_churners:.0%} of at-risk customers
- Out of every 100 customers flagged as at-risk, the model is right about the majority
RECOMMENDATIONS:
1. Target month-to-month customers in their first year with contract incentives
(e.g., discounted annual plan, loyalty rewards for 12-month milestone)
2. Investigate fiber optic service quality - high churn suggests a gap between
customer expectations and service delivery
3. Implement an early warning system using this model to flag at-risk customers
for proactive outreach by the retention team
""")
What does this code do?
This cell produces a formatted business summary that brings together everything you have learned in the project.
- The TOP CHURN DRIVERS section restates the three highest-impact findings from your EDA, with specific churn rates attached to each.
- The MODEL PERFORMANCE section uses caught_churners/total_churners (computed in Cell 9) to dynamically calculate what percentage of churners the model catches.
- The RECOMMENDATIONS section ties each action directly to a data finding. Recommendation 1 targets month-to-month contracts (driver #1). Recommendation 2 addresses fiber optic quality (driver #2). Recommendation 3 operationalizes the model itself.
- Run the cell by pressing Shift+Enter.
- You should see a formatted business summary printed below the cell. The MODEL PERFORMANCE line shows a dynamic percentage pulled from your model's actual results.
Seeing a NameError?
If you see an error referencing caught_churners or total_churners, Cell 9 has not been run yet. Go back and run Cell 9 first, then re-run Cell 10.
If the percentage shows 0% or looks wrong, confirm that Cell 9 printed non-zero values for "Churners correctly identified."
Why is my business summary showing a NameError?
✔️ Awesome, I've got everything!
Great. Double check that your business summary printed with all three sections (drivers, performance, recommendations) and that the model performance percentage is not zero.
ⓧ I'd like to double check the full code
Here is the complete Cell 10 for comparison:
# Cell 10: Business summary and recommendations
print("=" * 50)
print("BUSINESS SUMMARY")
print("=" * 50)
print(f"""
TOP CHURN DRIVERS (from EDA):
1. Contract type: Month-to-month customers churn at ~42% vs ~3% for two-year contracts
2. Internet service: Fiber optic customers churn at ~42% vs ~19% for DSL
3. Tenure: Nearly half of churners leave within their first 12 months
MODEL PERFORMANCE:
- The model correctly identifies {caught_churners/total_churners:.0%} of at-risk customers
- Out of every 100 customers flagged as at-risk, the model is right about the majority
RECOMMENDATIONS:
1. Target month-to-month customers in their first year with contract incentives
(e.g., discounted annual plan, loyalty rewards for 12-month milestone)
2. Investigate fiber optic service quality - high churn suggests a gap between
customer expectations and service delivery
3. Implement an early warning system using this model to flag at-risk customers
for proactive outreach by the retention team
""")
Secret mission
Craft a Stakeholder-Ready Executive Summary with Claude
You have a trained model and metrics. But decision-makers rarely read Jupyter notebooks. In this bonus mission, you will use Claude to transform your technical findings into a polished 1-page executive summary that a VP of Customer Success could act on.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely in Google Colab, so there are no ongoing costs. Your notebook auto-saves to Google Drive for free.
Resources you used:
- Google Colab notebook Churn_Prediction_Project (saved in your Google Drive).
Keep everything running
No action needed. Choose this if you plan to revisit or extend your churn analysis.
- Your notebook stays in Google Drive indefinitely at no cost.
- Your Claude free account remains active with no charges.
- You can reopen the notebook anytime from drive.google.com.
Pause - I'll come back to this later
Shut down the Colab runtime to free up resources. Your notebook stays saved.
- Close the Colab browser tab. The runtime disconnects automatically after inactivity, but your notebook remains saved in Google Drive.
- When you return, open the notebook from Google Drive and click Runtime then Run all to re-execute your cells.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Open drive.google.com in your browser.
- Find the notebook named Churn_Prediction_Project in your Google Drive.
- Right-click the notebook and select Move to trash.
- To permanently delete, click Trash in the left sidebar, then click Empty trash.
Nice Work!
Nice Work!
You just built a complete customer churn prediction pipeline from scratch. You went from a raw CSV of 7,043 telecom customers all the way to a trained machine learning model and a set of actionable business recommendations.
You've learned how to:
- Load, explore, and preprocess a real-world dataset in Google Colab, including fixing data quality issues and encoding categorical features for modeling.
- Train a Logistic Regression classifier and evaluate it using standard classification metrics (accuracy, precision, recall, F1-score, and confusion matrix).
- Interpret model results in a business context, identifying top churn drivers and writing data-grounded retention recommendations.
- Secret Mission: Used Claude to craft a stakeholder-ready executive summary from your technical findings.
Ready to quiz yourself?