Predicting Telco Churn with Python

Explore customer churn data, build classifiers, and explain predictions.

Introduction

30 Second Summary

Every subscription service you use, from your phone plan to your streaming apps, is constantly trying to figure out one thing: who is about to cancel? Companies call this "churn," and predicting it before it happens is one of the most valuable skills in data science.

In this project, you will analyze real telecom customer data, uncover patterns behind why people leave, and build machine learning models that predict which customers are at risk of churning.

What You'll Build

A polished Google Colab notebook that tells the story of customer churn through exploratory analysis, professional visualizations, and predictive modeling.

By the end of this project, you'll have:

  • A complete Google Colab notebook with structured markdown sections, narrative storytelling, and professional visualizations that reveal why customers churn.
  • Two trained classification models (Logistic Regression and Random Forest) with evaluated performance metrics and a comparison of their strengths.
  • A feature importance analysis showing which customer attributes matter most when predicting churn.
  • Secret Mission: Level up with XGBoost and SHAP to explain exactly why your model flags individual customers as at-risk.

Are there any prerequisites?

This project assumes basic familiarity with Python, pandas, and matplotlib/seaborn. You do not need prior machine learning experience. Everything runs in Google Colab, so no local setup is required.

Set Up Your Notebook and Load Data

To investigate why customers leave, you need a workspace that combines code, visualizations, and written narrative in one place.

In this step, you will create a Google Colab notebook, import the data science libraries you need, load real customer data from IBM's public repository, and clean it up so it is ready for analysis.

In this step, get ready to:
  • Create a Google Colab notebook with a professional title.
  • Import data science libraries and load the IBM telco customer dataset.
  • Clean the data by fixing column types and converting the target variable to binary.
Open Google Colab and create your notebook

Google Colab is a free, browser-based notebook environment that comes with all the Python data science libraries pre-installed.

  • Open your browser and navigate to colab.research.google.com.
  • Sign in with your Google account if prompted.
  • Click File in the top menu bar.
  • Click New notebook.
  • Click the notebook title at the top left (it says Untitled0.ipynb by default).
  • Rename it to Customer_Churn_Analysis.ipynb and press Enter.

Now add a title cell so your notebook has a professional header.

  • Click the + Text button in the toolbar at the top of the notebook.
  • In the text cell that appears, type the following:
# Customer Churn Analysis: Why Do Customers Leave?
  • Click outside the text cell to render it. You should see a large heading appear.

What is a text cell?

Text cells in Colab use Markdown formatting. The # symbol creates a heading. These cells let you add narrative and section titles between your code, turning your notebook into a readable document.

Import libraries and load the dataset

Your notebook needs several libraries for data manipulation, visualization, and machine learning. Colab comes with all of these pre-installed, so you just need to import them.

  • Click the + Code button to add a new code cell below your title.
  • Import all the libraries and set visualization defaults by pasting this code:
# Cell 1: Setup and Imports
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.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)

What does this code do?

  • pandas and numpy handle data loading and numerical operations.
  • matplotlib and seaborn create charts and visualizations.
  • The sklearn imports bring in tools for splitting data, training models, and evaluating results (you will use these in later steps).
  • The last two lines set a clean plot style and a default figure size so every chart looks consistent.
  • Run the cell by pressing Shift+Enter.
  • The cell completes with no output. If you see no error messages, your imports loaded successfully.

Seeing an ImportError?

All of these libraries are pre-installed on Google Colab. If you see an error, make sure you are running the notebook in Colab (not a local Jupyter install). Double-check for typos in the import lines.

Help me fix my import error in Google Colab.

Now load the telco customer dataset directly from IBM's public GitHub repository.

  • Add a new code cell by pressing Shift+Enter (which runs the current cell and creates a new one below) or by clicking + Code.
  • Load the dataset and preview it by pasting this code:
# Cell 2: Load Data
url = 'https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv'
df = pd.read_csv(url)
print(f"Dataset shape: {df.shape}")
df.head()

What does this code do?

The url variable points to IBM's publicly hosted CSV file containing real customer data from a telecom company.

pd.read_csv(url) downloads and parses that file into a DataFrame (a table-like data structure). The print statement shows how many rows and columns loaded, and df.head() displays the first 5 rows so you can see what the data looks like.

  • Run the cell with Shift+Enter.
  • You should see Dataset shape: (7043, 21) followed by a table showing columns like customerID, gender, tenure, MonthlyCharges, and Churn.

Not seeing the data table?

This cell downloads data from the internet. Make sure your Colab runtime is connected (check the top right for a green checkmark). If you see a connection error, click Connect in the top right corner and run the cell again.

Help me troubleshoot my data loading error in Colab.

Clean and prepare the data

Raw datasets always need cleaning before analysis. This dataset has a known issue: the TotalCharges column contains blank strings for new customers, and the Churn column uses text (Yes/No) instead of numbers.

  • Add a new code cell and paste the following data cleaning code:
# Cell 3: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df.dropna(subset=['TotalCharges'], inplace=True)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
print(f"Churn rate: {df['Churn'].mean():.3f}")
print(f"Clean dataset shape: {df.shape}")

What does this code do?

  • pd.to_numeric(..., errors='coerce') converts TotalCharges from text to numbers. Any blank strings become NaN (missing values) instead of causing an error.
  • df.dropna(subset=['TotalCharges']) removes the 11 rows with missing values (these are brand-new customers with zero tenure).
  • The .map({'Yes': 1, 'No': 0}) line converts the Churn column into binary numbers. Machine learning models need numeric targets, not text labels.
  • The churn rate tells you what percentage of customers left. A rate around 0.265 means roughly 26.5% of customers churned.
  • Run the cell with Shift+Enter.
  • You should see output showing a churn rate around 0.265 and a clean dataset shape of approximately (7032, 21).

Seeing a KeyError or unexpected output?

Make sure you ran Cell 2 first (the data loading cell). Each cell depends on the one before it. If you see KeyError: 'TotalCharges', the DataFrame was not loaded properly. Go back and re-run Cell 2.

Help me fix my data cleaning error.

Why is the churn rate important?

A churn rate of 26.5% means about 1 in 4 customers left. This is called class imbalance because there are far more retained customers than churned ones. You will keep this in mind when building models later, since it affects how you measure model performance.

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook (Colab auto-saves to Google Drive, but you can press Ctrl+S or Cmd+S to be sure).

ⓧ I'd like to double check the full code

Here is the complete code across all three cells so far. Compare your notebook against this:

# Cell 1: Setup and Imports
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.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)

# Cell 2: Load Data
url = 'https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv'
df = pd.read_csv(url)
print(f"Dataset shape: {df.shape}")
df.head()

# Cell 3: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df.dropna(subset=['TotalCharges'], inplace=True)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
print(f"Churn rate: {df['Churn'].mean():.3f}")
print(f"Clean dataset shape: {df.shape}")

Your data is clean and ready to explore. Next up, you will create visualizations that tell the story of why customers leave.

Explore and Tell the Churn Story

Your data is loaded, cleaned, and ready for investigation. Now comes the most exciting part of any data science project: asking questions and letting the data answer them visually.

In this step, you will perform Exploratory Data Analysis (EDA) to uncover the patterns behind customer churn. Instead of just creating charts, you will practice data storytelling by adding narrative markdown cells that explain what each visualization reveals.

In this step, get ready to:
  • Visualize overall churn distribution and churn rates by contract type.
  • Investigate how tenure and add-on services relate to churn.
  • Analyze monthly charges and summarize your key insights in a narrative section.
Visualize the overall churn landscape

The first question any stakeholder would ask is: how many customers are actually leaving? A simple count plot answers this instantly.

  • Add a new text cell above your next code cell and type a markdown heading like ## Churn Distribution followed by a sentence framing the question (e.g., "How many customers churned vs. stayed?").
  • Add a new code cell and paste the following code to create a count plot of churn:
# Cell 4: Churn Distribution
fig, ax = plt.subplots()
sns.countplot(x='Churn', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Customer Churn Distribution')
plt.show()

What does this code do?

sns.countplot draws a bar for each unique value in the Churn column, showing how many customers fall into each category.

set_xticklabels replaces the raw 0/1 labels with human-readable "Retained" and "Churned" so the chart is presentation-ready.

  • Run the cell by pressing Shift+Enter.
  • You should see a bar chart with two bars. The "Retained" bar is significantly taller than "Churned", confirming the ~26.5% churn rate.

Don't see a chart?

Make sure you ran the earlier cells first (Cells 1-3). Colab cells execute in order, and df must exist before you can plot it.

If you see a NameError, click Runtime then Run all to re-run everything from the top.

Still stuck? Help me debug my churn distribution plot in Colab.

Now let's dig deeper. A count plot shows the imbalance, but it does not tell us which customer segments are most at risk. Contract type is a strong signal.

  • Add a new text cell and write a markdown annotation like: "Do customers on shorter contracts churn more often? Let's check."
  • Add a new code cell and paste this code to calculate and plot churn rate by contract type:
# Cell 5: Churn by Contract Type
contract_churn = df.groupby('Contract')['Churn'].mean().sort_values(ascending=False)
fig, ax = plt.subplots()
contract_churn.plot(kind='bar', ax=ax, color=['#e74c3c', '#f39c12', '#2ecc71'])
ax.set_title('Churn Rate by Contract Type')
ax.set_ylabel('Churn Rate')
ax.set_xlabel('Contract Type')
plt.xticks(rotation=0)
plt.show()

What does this code do?

groupby('Contract')['Churn'].mean() calculates the proportion of customers who churned within each contract type. Since Churn is 0 or 1, the mean gives you the churn rate directly.

The colors are ordered from red (highest churn) to green (lowest churn), making the risk level instantly visible.

  • Run the cell by pressing Shift+Enter.
  • You should see a bar chart where Month-to-month has the tallest bar (around 0.42), followed by One year (~0.11), and Two year (~0.03).
  • Add a text cell below the chart and write a narrative annotation, for example: "Month-to-month customers churn at 42% vs 11% for one-year contracts. Locking customers into longer contracts dramatically reduces churn."

Bars look wrong or missing?

If you see only one bar, check that the Contract column exists in your DataFrame by running df['Contract'].value_counts() in a new cell.

If you see an error about colors, make sure the color list has exactly three items to match the three contract types.

Need help? Why is my contract churn bar chart not displaying correctly?

Investigate tenure and service patterns

Contract type tells you about commitment level. Now let's look at how long customers stay before leaving, and whether having certain services protects them from churning.

  • Add a text cell with a heading like ## Tenure and Services and a sentence introducing what you are investigating.
  • Add a new code cell and paste this code to plot tenure distribution by churn status:
# Cell 6: Tenure Distribution by Churn
fig, ax = plt.subplots()
sns.histplot(data=df, x='tenure', hue='Churn', multiple='stack', bins=30, ax=ax)
ax.set_title('Customer Tenure Distribution by Churn Status')
ax.set_xlabel('Tenure (months)')
plt.show()

What does this code do?

sns.histplot with hue='Churn' colors the bars by churn status, letting you see how churned customers cluster at low tenure values.

multiple='stack' stacks the bars so you can see both the total distribution and the churn proportion at each tenure level.

  • Run the cell by pressing Shift+Enter.
  • You should see a stacked histogram. Notice that churned customers (shown in a different color) are heavily concentrated in the first 1-10 months. Customers who survive past month 20 rarely leave.
  • Add a text cell noting this insight, for example: "Most churn happens in the first year. Customers who survive past month 20 become significantly more loyal."

Histogram looks empty or has one color?

Confirm that Churn is numeric (0 and 1) by running df['Churn'].dtype in a new cell. It should return int64. If it returns object, re-run Cell 3 to convert it.

Need help? Help me fix my tenure histogram that's not showing both churn categories.

Now let's check whether customers who lack add-on services are more likely to churn. Services like online security and tech support may act as "stickiness" factors.

  • Add a new code cell and paste this code to create a 2x2 grid of service churn rates:
# Cell 7: Services and Churn
services = ['OnlineSecurity', 'TechSupport', 'OnlineBackup', 'DeviceProtection']
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for idx, service in enumerate(services):
    ax = axes[idx // 2, idx % 2]
    service_churn = df.groupby(service)['Churn'].mean()
    service_churn.plot(kind='bar', ax=ax, color='#3498db')
    ax.set_title(f'Churn Rate by {service}')
    ax.set_ylabel('Churn Rate')
    plt.sca(ax)
    plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

What does this code do?

The loop iterates through four service columns, creating a separate bar chart for each in a 2x2 grid.

axes[idx // 2, idx % 2] uses integer division and modulo to place each chart in the correct grid position (row 0 or 1, column 0 or 1).

For each service, you will see bars for "No", "Yes", and sometimes "No internet service". Customers without these services consistently show higher churn.

  • Run the cell by pressing Shift+Enter.
  • You should see a 2x2 grid of bar charts. In each chart, the "No" bar is tallest, meaning customers without that service churn at the highest rate.
  • Add a text cell noting this pattern, for example: "Customers without OnlineSecurity or TechSupport churn at roughly 2x the rate of those who have them. These services act as retention anchors."

Seeing overlapping labels or a blank grid?

If labels overlap, make sure you included plt.tight_layout() before plt.show(). This adjusts spacing between subplots automatically.

If the grid is blank, check that you spelled the column names exactly: OnlineSecurity, TechSupport, OnlineBackup, DeviceProtection. Column names are case-sensitive.

Need help? My 2x2 services subplot grid isn't rendering correctly in Colab.

Analyze the financial dimension and summarize insights

The final angle to investigate is money. Do churned customers tend to pay more each month? A box plot lets you compare the full distribution of monthly charges between the two groups.

  • Add a text cell with a heading like ## Financial Analysis and a sentence introducing the question.
  • Add a new code cell and paste this code to compare monthly charges:
# Cell 8: Monthly Charges by Churn
fig, ax = plt.subplots()
sns.boxplot(x='Churn', y='MonthlyCharges', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Monthly Charges: Churned vs Retained Customers')
plt.show()

What does this code do?

A box plot shows the median, quartiles, and outliers of MonthlyCharges for each group. You can instantly compare the central tendency and spread between retained and churned customers.

If the median line for churned customers sits higher than for retained customers, it means customers paying more per month are more likely to leave.

  • Run the cell by pressing Shift+Enter.
  • You should see two box plots side by side. The "Churned" box sits noticeably higher (median around $80) compared to "Retained" (median around $65). Churned customers tend to pay more.

Box plot not showing or looks squished?

If the y-axis looks wrong, confirm that MonthlyCharges is numeric by running df['MonthlyCharges'].dtype. It should return float64.

Need help? Help me troubleshoot my MonthlyCharges box plot in Colab.

Now it is time to synthesize everything into a narrative summary. This is what separates a pile of charts from a data story.

  • Add a new text cell below your last chart.
  • Give it a heading: ## Key Insights.
  • Write 3-4 bullet points summarizing what the data told you. Your bullets should identify contract type, tenure, lack of add-on services, and higher monthly charges as the main drivers of churn.

What should my Key Insights section look like?

Think of this as the executive summary a manager would read. Each bullet should state a finding and its implication. For example:

  • "Month-to-month contract customers churn at 42%, suggesting retention offers should target this group before renewal decisions."
  • "Most churn happens in the first 12 months. Early engagement programs could reduce this."
  • "Customers without OnlineSecurity or TechSupport churn at nearly double the rate. Bundling these services could improve retention."
  • "Churned customers pay ~$15 more per month on average. High charges without perceived value may drive dissatisfaction."

✔️ Awesome, I've got everything!

Great. Double check that you have saved your notebook by pressing Ctrl+S (or Cmd+S on macOS).

ⓧ I'd like to double check the full code

Here is all the code you should have so far, across Cells 1 through 8:

# Cell 1: Setup and Imports
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.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)

# Cell 2: Load Data
url = 'https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv'
df = pd.read_csv(url)
print(f"Dataset shape: {df.shape}")
df.head()

# Cell 3: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df.dropna(subset=['TotalCharges'], inplace=True)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
print(f"Churn rate: {df['Churn'].mean():.3f}")
print(f"Clean dataset shape: {df.shape}")

# Cell 4: Churn Distribution
fig, ax = plt.subplots()
sns.countplot(x='Churn', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Customer Churn Distribution')
plt.show()

# Cell 5: Churn by Contract Type
contract_churn = df.groupby('Contract')['Churn'].mean().sort_values(ascending=False)
fig, ax = plt.subplots()
contract_churn.plot(kind='bar', ax=ax, color=['#e74c3c', '#f39c12', '#2ecc71'])
ax.set_title('Churn Rate by Contract Type')
ax.set_ylabel('Churn Rate')
ax.set_xlabel('Contract Type')
plt.xticks(rotation=0)
plt.show()

# Cell 6: Tenure Distribution by Churn
fig, ax = plt.subplots()
sns.histplot(data=df, x='tenure', hue='Churn', multiple='stack', bins=30, ax=ax)
ax.set_title('Customer Tenure Distribution by Churn Status')
ax.set_xlabel('Tenure (months)')
plt.show()

# Cell 7: Services and Churn
services = ['OnlineSecurity', 'TechSupport', 'OnlineBackup', 'DeviceProtection']
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for idx, service in enumerate(services):
    ax = axes[idx // 2, idx % 2]
    service_churn = df.groupby(service)['Churn'].mean()
    service_churn.plot(kind='bar', ax=ax, color='#3498db')
    ax.set_title(f'Churn Rate by {service}')
    ax.set_ylabel('Churn Rate')
    plt.sca(ax)
    plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

# Cell 8: Monthly Charges by Churn
fig, ax = plt.subplots()
sns.boxplot(x='Churn', y='MonthlyCharges', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Monthly Charges: Churned vs Retained Customers')
plt.show()

You have uncovered the story hidden in the data. Your notebook now reads like a narrative, not just a code dump. Next up, you will transform these raw columns into a format that machine learning models can understand.

Engineer Features and Prepare for Modeling

Your exploratory analysis told a compelling story about why customers leave. Contract type, tenure, services, and charges all surfaced as key drivers.

Now it's time to translate those insights into a format that machine learning models can actually use. Models need numbers, not text labels like "Month-to-month" or "Yes". In this step, you'll transform the raw DataFrame into a clean numeric matrix and split it into training and testing sets.

In this step, get ready to:
  • Encode all categorical variables into numeric columns using one-hot encoding.
  • Split the data into training and testing sets while preserving the churn class balance.
Drop customerID and encode categorical variables

Machine learning algorithms work with numbers. Columns like Contract and InternetService contain text categories that need to be converted into binary (0/1) columns.

  • Create a new code cell below your last visualization cell. Add the following code to drop the non-predictive ID column and encode all categorical features:
# Cell 9: Feature Engineering
df_model = df.drop('customerID', axis=1)
df_model = pd.get_dummies(df_model, drop_first=True)
print(f"Feature matrix shape: {df_model.shape}")

What does this code do?

First, df.drop('customerID', axis=1) removes the customer ID column. It's a unique identifier with no predictive value.

Then pd.get_dummies(df_model, drop_first=True) converts every categorical column into binary indicator columns. The drop_first=True parameter drops one category per feature to avoid multicollinearity (where one column is perfectly predictable from the others).

  • Run the cell.
  • You should see output showing the feature matrix shape with approximately 30+ columns. The exact number depends on how many unique categories exist in the data.

Why drop_first=True?

Consider a column with values "Male" and "Female". One-hot encoding normally creates two columns. But if someone is not Male, they must be Female. The second column is redundant. Dropping it reduces noise and prevents mathematical issues in models like Logistic Regression.

Seeing fewer than 30 columns?

Make sure you ran the data cleaning cell (Cell 3) before this one. If the Churn column is still Yes/No instead of 1/0, the encoding will create an extra dummy column.

Also confirm you're calling pd.get_dummies() on df_model (the copy without customerID), not on the original df.

Help me debug my feature engineering output

Split data into training and testing sets

To evaluate a model fairly, you need data it has never seen during training. A train/test split reserves 20% of your data as a "holdout" set for measuring real-world performance.

  • Create a new code cell and add this code to define your features, target, and perform the split:
# Cell 10: Train/Test Split
X = df_model.drop('Churn', axis=1)
y = df_model['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"Train set: {X_train.shape[0]} samples, churn rate: {y_train.mean():.3f}")
print(f"Test set: {X_test.shape[0]} samples, churn rate: {y_test.mean():.3f}")

What does this code do?

  • X holds all feature columns (everything except Churn). y holds the target variable (Churn: 0 or 1).
  • test_size=0.2 reserves 20% of the data for testing.
  • random_state=42 ensures the split is reproducible. Anyone running this notebook gets the same split.
  • stratify=y is the key parameter. It ensures both train and test sets have the same proportion of churned customers (~26.5%). Without it, you could accidentally get a test set with very few churners, making evaluation unreliable.
  • Run the cell.
  • You should see the train set size, test set size, and churn rates for both. Both rates should be approximately 0.265, confirming that stratification preserved the class balance.

Churn rates don't match between train and test?

Double check that you included stratify=y in your train_test_split() call. Without this parameter, the split is random and class proportions can drift.

Help me fix my train/test split

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook before moving on.

ⓧ I'd like to double check the full code

Here is the full notebook code up to this point:

# Cell 1: Setup and Imports
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.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)

# Cell 2: Load Data
url = 'https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv'
df = pd.read_csv(url)
print(f"Dataset shape: {df.shape}")
df.head()

# Cell 3: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df.dropna(subset=['TotalCharges'], inplace=True)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
print(f"Churn rate: {df['Churn'].mean():.3f}")
print(f"Clean dataset shape: {df.shape}")

# Cell 4: Churn Distribution
fig, ax = plt.subplots()
sns.countplot(x='Churn', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Customer Churn Distribution')
plt.show()

# Cell 5: Churn by Contract Type
contract_churn = df.groupby('Contract')['Churn'].mean().sort_values(ascending=False)
fig, ax = plt.subplots()
contract_churn.plot(kind='bar', ax=ax, color=['#e74c3c', '#f39c12', '#2ecc71'])
ax.set_title('Churn Rate by Contract Type')
ax.set_ylabel('Churn Rate')
ax.set_xlabel('Contract Type')
plt.xticks(rotation=0)
plt.show()

# Cell 6: Tenure Distribution by Churn
fig, ax = plt.subplots()
sns.histplot(data=df, x='tenure', hue='Churn', multiple='stack', bins=30, ax=ax)
ax.set_title('Customer Tenure Distribution by Churn Status')
ax.set_xlabel('Tenure (months)')
plt.show()

# Cell 7: Services and Churn
services = ['OnlineSecurity', 'TechSupport', 'OnlineBackup', 'DeviceProtection']
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for idx, service in enumerate(services):
    ax = axes[idx // 2, idx % 2]
    service_churn = df.groupby(service)['Churn'].mean()
    service_churn.plot(kind='bar', ax=ax, color='#3498db')
    ax.set_title(f'Churn Rate by {service}')
    ax.set_ylabel('Churn Rate')
    plt.sca(ax)
    plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

# Cell 8: Monthly Charges by Churn
fig, ax = plt.subplots()
sns.boxplot(x='Churn', y='MonthlyCharges', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Monthly Charges: Churned vs Retained Customers')
plt.show()

# Cell 9: Feature Engineering
df_model = df.drop('customerID', axis=1)
df_model = pd.get_dummies(df_model, drop_first=True)
print(f"Feature matrix shape: {df_model.shape}")

# Cell 10: Train/Test Split
X = df_model.drop('Churn', axis=1)
y = df_model['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"Train set: {X_train.shape[0]} samples, churn rate: {y_train.mean():.3f}")
print(f"Test set: {X_test.shape[0]} samples, churn rate: {y_test.mean():.3f}")

Your data is now model-ready. Every column is numeric, your target variable is separated, and your train/test split preserves the real-world churn ratio. Next up, you'll train two classification models and see which one better identifies at-risk customers.

Build and Compare Classification Models

Your data is clean, encoded, and split into training and test sets. Now it's time to answer the core question: can we predict which customers will churn?

In this step, you'll train two different classification models and compare their performance. You'll also extract feature importance scores to understand which factors drive churn predictions the most.

In this step, get ready to:
  • Train a Logistic Regression model and evaluate its performance.
  • Train a Random Forest model and visualize feature importance.
  • Compare both models and write business recommendations.
Train a Logistic Regression model

Logistic Regression is a simple but effective baseline for binary classification problems. It learns a linear decision boundary between churned and retained customers, making it fast to train and easy to interpret.

  • Create a new code cell in your notebook and train the Logistic Regression model by pasting this code:
# Cell 11: Logistic Regression
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
lr_preds = lr_model.predict(X_test)
print("Logistic Regression Results:")
print(classification_report(y_test, lr_preds, target_names=['Retained', 'Churned']))

What does this code do?

LogisticRegression(max_iter=1000, random_state=42) creates the model. The max_iter=1000 ensures the solver has enough iterations to converge on this many features.

The classification_report prints precision, recall, and F1-score for each class. Pay special attention to recall for the "Churned" class. That's the percentage of actual churners the model correctly identifies.

  • Run the cell by pressing Shift+Enter.
  • You should see a formatted table showing precision, recall, and F1-score for both "Retained" and "Churned" classes.

What is recall and why does it matter here?

Recall for churners tells you: "Of all customers who actually churned, what percentage did the model catch?" A model with low recall misses many at-risk customers.

In a business context, missing a churner is expensive. You'd rather flag someone who stays than miss someone about to leave.

Seeing a convergence warning?

If you see a warning about the model not converging, double-check that you set max_iter=1000 and not the default value of 100. The higher iteration count gives the solver enough room to find the optimal weights.

Why isn't my Logistic Regression model converging?

Train a Random Forest and visualize feature importance

Random Forest takes a different approach. It builds many decision trees on random subsets of the data and averages their predictions. This often captures non-linear patterns that Logistic Regression misses.

  • Create a new code cell and train the Random Forest model by pasting this code:
# Cell 12: Random Forest
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_preds = rf_model.predict(X_test)
print("Random Forest Results:")
print(classification_report(y_test, rf_preds, target_names=['Retained', 'Churned']))

What does this code do?

RandomForestClassifier(n_estimators=100) trains 100 individual decision trees. Each tree sees a random sample of the data and a random subset of features.

The final prediction is determined by majority vote across all 100 trees. This ensemble approach reduces overfitting compared to a single decision tree.

  • Run the cell with Shift+Enter.
  • You should see another classification report table. Compare the recall for "Churned" with what Logistic Regression achieved.

Cell taking a long time to run?

Random Forest with 100 trees on 7,000 rows should complete in a few seconds on Colab. If it hangs, check that you haven't accidentally run the cell multiple times. Click Runtime then Interrupt execution and re-run the cell once.

Why is my Random Forest taking so long to train?

One of the biggest advantages of Random Forest is that it naturally ranks features by how much they contribute to predictions. Let's visualize this.

  • Create a new code cell and generate the feature importance chart by pasting this code:
# Cell 13: Feature Importance
feature_importance = pd.Series(
    rf_model.feature_importances_, index=X.columns
).sort_values(ascending=False)

fig, ax = plt.subplots(figsize=(10, 8))
feature_importance.head(10).plot(kind='barh', ax=ax, color='#2ecc71')
ax.set_title('Top 10 Most Important Features (Random Forest)')
ax.set_xlabel('Feature Importance')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

What does this code do?

rf_model.feature_importances_ returns an array of scores showing how much each feature contributed to reducing prediction error across all 100 trees.

The code wraps these scores in a pandas Series with column names, sorts them, and plots the top 10 as a horizontal bar chart. Higher bars mean that feature had more influence on the model's churn predictions.

  • Run the cell with Shift+Enter.
  • You should see a horizontal bar chart showing the top 10 features. Expect tenure, MonthlyCharges, and Contract_Month-to-month to appear near the top.

Feature names look unfamiliar?

Names like Contract_Month-to-month or InternetService_Fiber optic are normal. They come from the one-hot encoding you applied in the previous step with pd.get_dummies(). Each one represents a specific category value being True.

Compare models and write business recommendations

A model is only useful if you can translate its results into action. This final section ties everything together with a direct comparison and business-ready takeaways.

  • Create a new code cell and print the model comparison summary by pasting this code:
# Cell 14: Model Comparison Summary
print("Model Comparison Summary")
print("=" * 40)
print(f"Logistic Regression Accuracy: {(lr_preds == y_test).mean():.3f}")
print(f"Random Forest Accuracy: {(rf_preds == y_test).mean():.3f}")

What does this code do?

(lr_preds == y_test).mean() calculates accuracy by comparing each prediction to the actual label. The .mean() of a boolean array gives the proportion of correct predictions.

  • Run the cell with Shift+Enter.
  • You should see accuracy scores for both models printed in a clean summary format.

Now add markdown cells above and below your model code to tell the story of your findings. This is what transforms a collection of code cells into a portfolio-ready notebook.

  • Click the + Text button in the toolbar to add a new markdown cell below your model comparison code.
  • Write a "Model Comparison" heading using ## Model Comparison.
  • Below the heading, write 2-3 sentences comparing which model has better recall for churners and which features dominate the predictions.
  • Add a "Recommendations" subheading using ### Recommendations.
  • Write 2-3 bullet points translating your findings into business advice. For example: "Target month-to-month customers with retention offers before month 6."

Why write narrative markdown in a data science notebook?

Interviewers and hiring managers rarely read raw code. They look for your ability to communicate insights clearly. A notebook that poses questions, answers them with data, and ends with recommendations demonstrates data storytelling skills.

Use your feature importance chart

Reference specific features from your chart in the recommendations. If Contract_Month-to-month is a top driver, recommend incentives to convert those customers to annual contracts.

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook. All cells should show output below them.

ⓧ I'd like to double check the full code

Here is the complete notebook code through this step. Compare it with your cells to make sure everything matches.

# Cell 1: Setup and Imports
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.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix

sns.set_style('whitegrid')
plt.rcParams['figure.figsize'] = (10, 6)

# Cell 2: Load Data
url = 'https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv'
df = pd.read_csv(url)
print(f"Dataset shape: {df.shape}")
df.head()

# Cell 3: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df.dropna(subset=['TotalCharges'], inplace=True)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
print(f"Churn rate: {df['Churn'].mean():.3f}")
print(f"Clean dataset shape: {df.shape}")

# Cell 4: Churn Distribution
fig, ax = plt.subplots()
sns.countplot(x='Churn', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Customer Churn Distribution')
plt.show()

# Cell 5: Churn by Contract Type
contract_churn = df.groupby('Contract')['Churn'].mean().sort_values(ascending=False)
fig, ax = plt.subplots()
contract_churn.plot(kind='bar', ax=ax, color=['#e74c3c', '#f39c12', '#2ecc71'])
ax.set_title('Churn Rate by Contract Type')
ax.set_ylabel('Churn Rate')
ax.set_xlabel('Contract Type')
plt.xticks(rotation=0)
plt.show()

# Cell 6: Tenure Distribution by Churn
fig, ax = plt.subplots()
sns.histplot(data=df, x='tenure', hue='Churn', multiple='stack', bins=30, ax=ax)
ax.set_title('Customer Tenure Distribution by Churn Status')
ax.set_xlabel('Tenure (months)')
plt.show()

# Cell 7: Services and Churn
services = ['OnlineSecurity', 'TechSupport', 'OnlineBackup', 'DeviceProtection']
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for idx, service in enumerate(services):
    ax = axes[idx // 2, idx % 2]
    service_churn = df.groupby(service)['Churn'].mean()
    service_churn.plot(kind='bar', ax=ax, color='#3498db')
    ax.set_title(f'Churn Rate by {service}')
    ax.set_ylabel('Churn Rate')
    plt.sca(ax)
    plt.xticks(rotation=0)
plt.tight_layout()
plt.show()

# Cell 8: Monthly Charges by Churn
fig, ax = plt.subplots()
sns.boxplot(x='Churn', y='MonthlyCharges', data=df, ax=ax)
ax.set_xticklabels(['Retained', 'Churned'])
ax.set_title('Monthly Charges: Churned vs Retained Customers')
plt.show()

# Cell 9: Feature Engineering
df_model = df.drop('customerID', axis=1)
df_model = pd.get_dummies(df_model, drop_first=True)
print(f"Feature matrix shape: {df_model.shape}")

# Cell 10: Train/Test Split
X = df_model.drop('Churn', axis=1)
y = df_model['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"Train set: {X_train.shape[0]} samples, churn rate: {y_train.mean():.3f}")
print(f"Test set: {X_test.shape[0]} samples, churn rate: {y_test.mean():.3f}")

# Cell 11: Logistic Regression
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
lr_preds = lr_model.predict(X_test)
print("Logistic Regression Results:")
print(classification_report(y_test, lr_preds, target_names=['Retained', 'Churned']))

# Cell 12: Random Forest
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
rf_preds = rf_model.predict(X_test)
print("Random Forest Results:")
print(classification_report(y_test, rf_preds, target_names=['Retained', 'Churned']))

# Cell 13: Feature Importance
feature_importance = pd.Series(
    rf_model.feature_importances_, index=X.columns
).sort_values(ascending=False)

fig, ax = plt.subplots(figsize=(10, 8))
feature_importance.head(10).plot(kind='barh', ax=ax, color='#2ecc71')
ax.set_title('Top 10 Most Important Features (Random Forest)')
ax.set_xlabel('Feature Importance')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

# Cell 14: Model Comparison Summary
print("Model Comparison Summary")
print("=" * 40)
print(f"Logistic Regression Accuracy: {(lr_preds == y_test).mean():.3f}")
print(f"Random Forest Accuracy: {(rf_preds == y_test).mean():.3f}")

Secret mission

XGBoost with SHAP Explanations

This is what separates a good portfolio project from a great one. Interviewers love seeing that you can explain WHY a model makes specific predictions, not just what it predicts. In this secret mission, you will train an XGBoost model and use SHAP to produce both global and local explanations of your churn predictions.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely in Google Colab, so there are no ongoing costs. Decide whether to keep your notebook accessible, disconnect the runtime to free up resources, or delete everything entirely.

Resources you used:

  • Google Colab notebook (stored in your Google Drive).
  • Colab runtime session (RAM and compute while connected).
  • SHAP library installed in the runtime (Secret Mission only).

Keep everything running

No action needed. Choose this if you plan to revisit your analysis, add more models, or refine your visualizations soon.

  • Your notebook stays in Google Drive and is accessible anytime from colab.research.google.com.
  • The runtime will automatically disconnect after a period of inactivity, freeing compute resources on its own.
  • Any pip-installed libraries (like SHAP) will need to be reinstalled when you reconnect to a new runtime.

Pause - I'll come back to this later

Disconnect the runtime to free up memory and compute. Your notebook file stays safe in Google Drive.

  • In your Colab notebook, click Runtime in the top menu bar.
  • Click Disconnect and delete runtime.
  • Your notebook code and outputs remain saved. When you return, click Connect to start a fresh runtime.

Delete - I don't want to use this again

Remove your notebook from Google Drive entirely. This cannot be undone once you empty the trash.

  • First, disconnect the runtime: click Runtime in the top menu, then Disconnect and delete runtime.
  • Open Google Drive at drive.google.com.
  • Navigate to the Colab Notebooks folder.
  • Right-click your churn analysis notebook and select Move to trash.
  • To permanently delete, open Trash in the left sidebar and click Empty trash.

Nice Work!

Nice Work!

You just built a complete data science notebook that answers a real business question: why do customers leave? From raw data to trained models and actionable insights, this notebook is ready to share with hiring managers.

You've learned how to:

  • Tell a data story through exploratory data analysis, combining professional visualizations with narrative markdown to uncover churn patterns.
  • Engineer features from categorical data using one-hot encoding and prepare a modeling-ready dataset with stratified train/test splits.
  • Train and compare Logistic Regression and Random Forest classifiers, evaluating performance with precision, recall, and F1-score to identify the top drivers of customer churn.
  • (Secret Mission) Used XGBoost with SHAP to explain individual predictions, demonstrating model interpretability skills that set your portfolio apart.

Ready to quiz yourself?