Predict Customer Churn with ML

Build a machine learning pipeline to predict telecom customer churn.

Introduction

30 Second Summary

Every subscription service you use has a team trying to predict which customers are about to leave. If they can spot the warning signs early enough, they can step in with a better deal before it's too late.

In this project, you will build a machine learning model that predicts whether a telecom customer will cancel their service. You'll load real customer data, train two different models, and discover which factors drive customers away.

What You'll Build

A complete ML pipeline in a Jupyter Notebook that takes raw customer data, preprocesses it, trains binary classification models, and outputs churn predictions with measurable accuracy.

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

  • A trained Logistic Regression and Decision Tree model that predict customer churn with evaluation metrics.
  • Visualizations showing churn distribution, model performance (ROC curves), and feature importance.
  • A reusable prediction function that takes new customer data and returns a churn probability.
  • Secret Mission: Build a Random Forest ensemble and compare all three models in a visual performance dashboard.

Are there any prerequisites?

You'll need basic Python knowledge (variables, functions, loops). No prior machine learning experience is required.

Before We Start

Before We Start...

Set Up Your ML Environment

Machine learning projects depend on a stack of specialized libraries for data manipulation, model training, and visualization. Before writing any ML code, you need an isolated environment where these libraries can live without conflicting with other Python projects on your machine.

In this step, you'll install the Jupyter extension for VS Code, create a dedicated project folder with a virtual environment, and install the four libraries that power your ML pipeline.

In this step, get ready to:
  • Install the Jupyter extension for VS Code.
  • Create a project folder with a Python virtual environment.
  • Install scikit-learn, pandas, matplotlib, and seaborn.
Install the Jupyter extension

Jupyter Notebooks let you write and run code in small, interactive cells. This makes them perfect for ML work because you can see the output of each step (a chart, a table, a metric) before moving on to the next.

  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Jupyter in the search bar at the top of the panel.
  • Find the result published by Microsoft with the ID ms-toolsai.jupyter.
  • Click Install.

What is Jupyter?

A Jupyter Notebook is a document that mixes code cells with their output (text, tables, charts). Data scientists use notebooks to experiment with data interactively, because you can tweak one cell and re-run it without restarting the whole script.

You should see the Install button change to a gear icon, confirming the extension is active.

Create your project folder and virtual environment

A virtual environment gives your project its own isolated copy of Python packages. This prevents version conflicts between different projects on your machine.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir churn-prediction
cd churn-prediction
  • Confirm the folder exists by running:
ls

You should see an empty directory (no output from ls since the folder is brand new).

Now open this folder as your VS Code workspace.

  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the churn-prediction folder, then click Open.

VS Code reloads with churn-prediction as the root folder in the Explorer sidebar.

Next, create the virtual environment using the Command Palette.

  • Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
  • Type Python: Create Environment and select it from the dropdown.
  • Select Venv when prompted to choose the environment type.
  • Select your Python 3.13 interpreter from the list.

VS Code creates a .venv folder inside your project and automatically activates it. You should see (.venv) appear in your terminal prompt.

What does a virtual environment do?

Without a virtual environment, every Python project on your machine shares the same global set of libraries. If project A needs version 1 of a library and project B needs version 2, they collide.

A virtual environment creates a self-contained .venv folder that holds its own copies of packages. When activated, pip install puts libraries here instead of the global Python folder.

Don't see (.venv) in your terminal?

If the Command Palette didn't show a Python interpreter, make sure you have the Python extension installed in VS Code (search for ms-python.python in the Extensions panel).

If the terminal still shows no (.venv) prefix, close the terminal and open a new one. VS Code activates the environment in newly opened terminals after creating it.

Help me activate my virtual environment in VS Code.

Install your ML libraries

Your virtual environment comes with pip, the Python package installer. You'll use it to download the four libraries that power your ML pipeline.

  • Verify pip is working by running this command:
pip --version

✔️ I see a version number

You should see something like pip 25.x.x from .venv/.... The path includes .venv, confirming pip is running inside your virtual environment.

ⓧ Command not found

If pip is not recognized, your virtual environment may not be activated.

  • On macOS/Linux, run:
source .venv/bin/activate
  • On Windows, run:
.venv\Scripts\activate

After activation, try pip --version again.

  • Install all four libraries by running this command:
pip install scikit-learn pandas matplotlib seaborn

What are these libraries?

  • pandas loads and manipulates tabular data (think spreadsheets in Python).
  • scikit-learn provides the ML algorithms you'll train (Logistic Regression, Decision Tree) and evaluation tools.
  • matplotlib draws charts and plots.
  • seaborn builds on matplotlib to create polished statistical visualizations with less code.

The install takes a minute or two. When it finishes, you should see Successfully installed followed by a list of all the packages that were downloaded.

Seeing a permissions error or timeout?

If you see a permissions error, confirm your terminal prompt starts with (.venv). Installing without an active virtual environment can require admin permissions.

If the install times out, check your internet connection and try running the command again.

Help me fix my pip install error.

Your environment is ready. Next up, you'll download a real customer dataset and start exploring it with pandas and seaborn.

Load and Explore the Data

Your ML environment is set up and ready to go. Now it's time to get your hands on some real data.

In this step, you'll download a real-world customer dataset, load it into a Jupyter notebook, and perform exploratory data analysis to understand what you're working with before building any models.

In this step, get ready to:
  • Download the Telco Customer Churn dataset and load it into a pandas DataFrame.
  • Examine the data structure including shape, columns, data types, and missing values.
  • Visualize the churn distribution and key feature relationships.
Download the dataset and create your notebook

Before writing any code, you need data to work with. You'll use the Telco Customer Churn dataset from Kaggle, a platform for sharing datasets and ML projects.

  • Open your browser and go to the Telco Customer Churn dataset page:
  • https://www.kaggle.com/datasets/blastchar/telco-customer-churn
  • Sign in to your Kaggle account (or create a free one if you don't have one yet).
  • Click the Download button near the top of the page.
  • Unzip the downloaded file.
  • Move the file WA_Fn-UseC_-Telco-Customer-Churn.csv into your project folder (the same folder open in VS Code).

Why Kaggle for datasets?

Kaggle hosts thousands of real-world datasets that data scientists use for practice and competition. The Telco Customer Churn dataset contains 7,043 rows of real customer data with 21 columns covering demographics, services, and account information.

Now you'll create the notebook where all your ML code will live.

  • In VS Code, open the Command Palette by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows).
  • Type Create: New Jupyter Notebook and select it from the list.
  • A new notebook opens with one empty cell.
  • Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows).
  • Name the file churn_prediction.ipynb and save it in your project folder.
  • Click the kernel picker button in the top right of the notebook.
  • Select the Python interpreter from your virtual environment (it should show your project's .venv path).

Don't see your virtual environment in the kernel list?

Make sure your virtual environment is active in the VS Code terminal. If the environment was just created, try reloading VS Code with Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) and selecting Developer: Reload Window.

help me find my virtual environment kernel in VS Code Jupyter

With your notebook ready, it's time to import the libraries you'll use throughout this project.

  • Import all the libraries for this project by pasting the following code into the first cell of your notebook:
# Cell 1: Import Libraries
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.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score, roc_curve, classification_report, confusion_matrix

What does this code do?

  • pandas (pd) loads and manipulates tabular data in DataFrames.
  • NumPy (np) provides efficient numerical operations on arrays.
  • matplotlib (plt) and seaborn (sns) create charts and statistical visualizations.
  • scikit-learn provides the ML tools: splitting data, training models, and measuring performance.
  • Run the cell by pressing Shift+Enter.

The cell runs silently with no output (imports don't print anything). A new empty cell appears below.

Next, load the CSV dataset into a DataFrame so you can start exploring it.

  • Load the dataset and display its basic shape by pasting this code into the next cell:
# Cell 2: Load the Data
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
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?

  • pd.read_csv() reads the CSV file and converts it into a pandas DataFrame called df.
  • df.shape tells you how many rows (customers) and columns (features) the dataset has.
  • df.columns.tolist() lists every column name so you can see what data is available.
  • df.head() shows the first 5 rows as a preview table.
  • Run the cell by pressing Shift+Enter.

You should see output showing the dataset has 7043 rows and 21 columns, followed by a list of column names and a table preview of the first 5 rows.

Seeing a FileNotFoundError?

This means Python cannot find the CSV file. Make sure WA_Fn-UseC_-Telco-Customer-Churn.csv is saved in the same folder as your churn_prediction.ipynb notebook.

help me fix a FileNotFoundError when loading my CSV in Jupyter

Examine the data structure

You know the dataset has 7,043 customers and 21 columns. Now you need to understand the data types and check for any missing values before feeding it into a model.

  • Explore the data types, missing values, and churn distribution by pasting this code into the next cell:
# Cell 3: Explore the Data
print(f"Data types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")

What does this code do?

  • df.dtypes shows each column's data type (numbers vs text). This matters because ML models need numeric inputs.
  • df.isnull().sum() counts missing values per column. Missing data can break your model or skew results.
  • df['Churn'].value_counts() shows how many customers stayed vs left. This is your target variable (what you're predicting).
  • Run the cell by pressing Shift+Enter.

You should see a list of data types (mostly object for text columns and int64/float64 for numbers), zero missing values reported by isnull(), and a churn count showing more customers stayed (No) than left (Yes).

Why does the churn split matter?

The churn column is imbalanced. More customers stayed than left. This is important to know upfront because an imbalanced dataset can trick a model into always predicting the majority class and still appearing accurate.

Visualize the churn distribution and tenure relationship

Numbers in tables only tell part of the story. Visualizations help you spot patterns that raw numbers can hide.

  • Create a churn distribution chart and a tenure histogram by pasting this code into the next cell:
# Cell 4: Visualize Churn Distribution
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sns.countplot(x='Churn', data=df, ax=axes[0])
axes[0].set_title('Churn Distribution')

sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[1])
axes[1].set_title('Tenure Distribution by Churn')

plt.tight_layout()
plt.show()

What does this code do?

  • plt.subplots(1, 2) creates a figure with two side-by-side chart areas.
  • sns.countplot() draws a bar chart showing how many customers are in each churn category (Yes/No).
  • sns.histplot() with hue='Churn' draws overlapping histograms so you can see how tenure (months with the company) differs between churned and retained customers.
  • kde=True adds a smooth curve over the histogram to make the distribution shape clearer.
  • Run the cell by pressing Shift+Enter.
  • Save your notebook with Cmd+S (macOS) or Ctrl+S (Windows).

You should see two charts appear below the cell. The left chart shows a bar chart with more "No" than "Yes" churn. The right chart shows that customers who churn tend to have very short tenures (the red/orange distribution peaks early).

Charts not appearing?

Make sure you selected the correct kernel (your virtual environment's Python) at the top right of the notebook. If the kernel shows a different Python, click it and switch to the one in your .venv folder.

help me display matplotlib charts in VS Code Jupyter

✔️ Awesome, I've got everything!

Great. Make sure your notebook is saved before moving on.

ⓧ I'd like to double check the full code

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

# Cell 1: Import Libraries
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.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score, roc_curve, classification_report, confusion_matrix

# Cell 2: Load the Data
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Explore the Data
print(f"Data types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")

# Cell 4: Visualize Churn Distribution
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sns.countplot(x='Churn', data=df, ax=axes[0])
axes[0].set_title('Churn Distribution')

sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[1])
axes[1].set_title('Tenure Distribution by Churn')

plt.tight_layout()
plt.show()

You've loaded real customer data and uncovered your first insight: customers with short tenures are far more likely to churn. Next up, you'll clean and preprocess this data so it's ready for machine learning models to understand.

Preprocess the Data for Machine Learning

In the previous step, you loaded the Telco Customer Churn dataset and explored its structure. You saw that churn is imbalanced (more customers stay than leave) and that tenure has a clear relationship with churn.

Raw data is messy. Machine learning models need numbers, not text labels like "Yes" or "Fiber optic". In this step, you'll clean the data, convert categories into numbers, and split everything into training and testing sets.

In this step, get ready to:
  • Handle missing values and fix data type issues.
  • Encode categorical variables into numbers using one-hot encoding.
  • Split the data into training and testing sets.
Handle missing values and data type conversions

The TotalCharges column has a hidden problem. Some rows contain empty strings instead of numbers, which pandas loaded as text. You also need to drop the customerID column (it's just an identifier, not a useful feature) and convert the Churn column from "Yes"/"No" text into 1/0 numbers.

  • Create a new cell below Cell 4 and add the following preprocessing code:
# Cell 5: Preprocess the Data
# Fix TotalCharges column (has empty strings)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'].fillna(df['TotalCharges'].median(), inplace=True)

# Drop customerID (not a feature)
df = df.drop('customerID', axis=1)

# Encode target variable
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})

print(f"After preprocessing - shape: {df.shape}")
print(f"Churn values: {df['Churn'].value_counts().to_dict()}")

What does this code do?

This cell tackles three data quality issues at once:

  • pd.to_numeric(errors='coerce') converts TotalCharges to numbers. Any empty strings become NaN ("not a number") instead of crashing.
  • fillna() replaces those NaN values with the column's median. This is called median imputation.
  • .map({'Yes': 1, 'No': 0}) turns the text labels into binary numbers that a model can work with.
  • Run the cell by pressing Shift+Enter.
  • You should see the shape printed as (7043, 20) (one column fewer after dropping customerID) and the Churn values showing counts for 0 and 1.

Seeing a KeyError?

If you get a KeyError: 'customerID' or KeyError: 'Churn', you may have run this cell twice. The first run already dropped customerID and mapped Churn. Restart the kernel and re-run all cells from the top.

help me fix a KeyError when preprocessing my churn data

Encode categorical variables using one-hot encoding

Your dataset still has text columns like InternetService ("DSL", "Fiber optic", "No") and Contract ("Month-to-month", "One year", "Two year"). One-hot encoding converts each category into its own column with a 1 or 0 value.

  • Create a new cell below Cell 5 and add this encoding code:
# Cell 6: One-Hot Encode Categorical Features
# Identify categorical columns (exclude target)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
print(f"Categorical columns to encode: {categorical_cols}")

# One-hot encode
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=True)
print(f"Shape after encoding: {df_encoded.shape}")

What does this code do?

  • select_dtypes(include=['object']) automatically finds all remaining text columns in the DataFrame.
  • pd.get_dummies() creates a new binary column for each category. For example, Contract becomes Contract_One year and Contract_Two year.
  • drop_first=True removes one category per column to avoid redundancy. If it's not "One year" or "Two year", the model knows it's "Month-to-month".
  • Run the cell.
  • You should see a list of categorical columns and the new shape with many more columns (around 30+) as each category got its own column.

Shape looks wrong?

If your shape still shows 20 columns, make sure Cell 5 ran successfully first. The Churn column must already be numeric (0/1) before this cell runs, otherwise get_dummies would try to encode it as a category.

help me debug my one-hot encoding output

Split data into features and target, then into train and test sets

A train-test split divides your data into two groups. The model learns patterns from the training set and gets evaluated on the test set (data it has never seen). This tells you how well the model generalizes to new customers.

  • Create a new cell below Cell 6 and add this split code:
# Cell 7: Split into Train and Test Sets
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():.2%}")
print(f"Churn rate in test: {y_test.mean():.2%}")

What does this code do?

  • X holds all the feature columns (everything except Churn). y holds just the target column (Churn).
  • test_size=0.2 reserves 20% of data for testing. The remaining 80% is used for training.
  • random_state=42 makes the split reproducible. You'll get the same split every time you run it.
  • stratify=y ensures both sets have the same proportion of churners. Without this, the test set might accidentally have too many or too few.
  • Run the cell.
  • You should see the training and test set sizes printed, along with matching churn rates (both around 26-27%) confirming that stratify worked correctly.

Seeing a ValueError about stratify?

If you get an error mentioning stratify, make sure your Churn column contains only 0 and 1 values (not "Yes"/"No" strings). Re-run Cell 5 to fix this.

help me fix a stratify error in train_test_split

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook.

ⓧ I'd like to double check the full code

Here is the complete notebook code up to this point:

# Cell 1: Import Libraries
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.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score, roc_curve, classification_report, confusion_matrix

# Cell 2: Load the Data
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Explore the Data
print(f"Data types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")

# Cell 4: Visualize Churn Distribution
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sns.countplot(x='Churn', data=df, ax=axes[0])
axes[0].set_title('Churn Distribution')

sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[1])
axes[1].set_title('Tenure Distribution by Churn')

plt.tight_layout()
plt.show()

# Cell 5: Preprocess the Data
# Fix TotalCharges column (has empty strings)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'].fillna(df['TotalCharges'].median(), inplace=True)

# Drop customerID (not a feature)
df = df.drop('customerID', axis=1)

# Encode target variable
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})

print(f"After preprocessing - shape: {df.shape}")
print(f"Churn values: {df['Churn'].value_counts().to_dict()}")

# Cell 6: One-Hot Encode Categorical Features
# Identify categorical columns (exclude target)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
print(f"Categorical columns to encode: {categorical_cols}")

# One-hot encode
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=True)
print(f"Shape after encoding: {df_encoded.shape}")

# Cell 7: Split into Train and Test Sets
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():.2%}")
print(f"Churn rate in test: {y_test.mean():.2%}")

Your data is clean, encoded, and split. Next up, you'll train your first machine learning models and see how well they can predict churn.

Train and Evaluate Your Models

Your data is clean, encoded, and split into training and test sets. Now comes the exciting part: teaching a machine learning model to predict which customers will churn.

You'll train two different algorithms on the same data and compare how well each performs. This is standard practice in ML because no single algorithm is best for every dataset.

In this step, get ready to:
  • Train a Logistic Regression model and measure its accuracy, precision, and recall.
  • Train a Decision Tree classifier and compare its performance.
  • Plot ROC curves to visually compare both models.
Train a Logistic Regression model

Logistic Regression is one of the simplest and most interpretable classification algorithms. Despite its name, it predicts categories (churn or no churn) rather than continuous values.

  • Create a new cell below Cell 7 and add this code to train your first model:
# Cell 8: Train Logistic Regression
log_reg = LogisticRegression(max_iter=1000, random_state=42)
log_reg.fit(X_train, y_train)

y_pred_lr = log_reg.predict(X_test)
y_prob_lr = log_reg.predict_proba(X_test)[:, 1]

print("=== Logistic Regression Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_lr):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_lr):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_lr):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_lr)}")

What does this code do?

  • LogisticRegression(max_iter=1000) creates the model with enough iterations for the solver to converge on our ~46 encoded features.
  • .fit(X_train, y_train) trains the model by finding the best weights for each feature.
  • .predict() gives a hard prediction (0 or 1), while .predict_proba()[:, 1] gives the probability of churn (useful for ROC curves later).
  • The four metrics (accuracy, precision, recall, ROC-AUC) each measure a different aspect of model quality. Accuracy is the overall correctness. Precision measures how many predicted churners actually churned. Recall measures how many actual churners were caught.
  • Run the cell by clicking the play button or pressing Shift+Enter.
  • You should see a results table with accuracy, precision, recall, and ROC-AUC scores, followed by a full classification report.

Seeing a convergence warning?

If you see a warning about the solver not converging, double check that max_iter=1000 is set in the LogisticRegression() call. The default of 100 is not enough for this dataset's ~46 features.

Help me fix a convergence warning in my Logistic Regression model.

Train a Decision Tree classifier

A Decision Tree takes a completely different approach. It learns a series of yes/no questions about the features, splitting the data at each step to separate churners from non-churners.

  • Create a new cell below Cell 8 and add this code to train your second model:
# Cell 9: Train Decision Tree
dt_clf = DecisionTreeClassifier(max_depth=5, random_state=42)
dt_clf.fit(X_train, y_train)

y_pred_dt = dt_clf.predict(X_test)
y_prob_dt = dt_clf.predict_proba(X_test)[:, 1]

print("=== Decision Tree Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_dt):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_dt):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_dt):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_dt):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_dt)}")

What does this code do?

  • DecisionTreeClassifier(max_depth=5) limits the tree to 5 levels of questions. Without this limit, the tree would memorize the training data (overfitting) instead of learning general patterns.
  • random_state=42 ensures you get the same results every time you run the cell. This makes your work reproducible.
  • The same four metrics are printed so you can directly compare this model against Logistic Regression.
  • Run the cell with Shift+Enter.
  • You should see the Decision Tree Results with accuracy, precision, recall, and ROC-AUC scores. Compare these numbers to your Logistic Regression results above.

Getting a NameError?

If you see NameError: name 'X_train' is not defined, it means Cell 7 (the train/test split) was not run. Go back and run all cells in order from Cell 1 through Cell 8 first.

Help me fix a NameError in my Decision Tree cell.

Plot ROC curves for both models

Numbers are useful, but a visual comparison makes the difference between models instantly clear. An ROC curve plots how well each model separates churners from non-churners at every possible threshold.

  • Create a new cell below Cell 9 and add this code to visualize the comparison:
# Cell 10: Plot ROC Curves
fpr_lr, tpr_lr, _ = roc_curve(y_test, y_prob_lr)
fpr_dt, tpr_dt, _ = roc_curve(y_test, y_prob_dt)

plt.figure(figsize=(8, 6))
plt.plot(fpr_lr, tpr_lr, label=f'Logistic Regression (AUC = {roc_auc_score(y_test, y_prob_lr):.3f})')
plt.plot(fpr_dt, tpr_dt, label=f'Decision Tree (AUC = {roc_auc_score(y_test, y_prob_dt):.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

How do I read this chart?

The dashed diagonal line represents a random coin-flip classifier. Any model worth using must curve above this line. The further a model's curve bows toward the top-left corner, the better it is at separating churners from non-churners.

The AUC (Area Under the Curve) number in the legend summarizes each curve into a single score. An AUC of 1.0 would mean a perfect model. An AUC of 0.5 means random guessing.

  • Run the cell with Shift+Enter.
  • You should see a chart with two colored curves (one per model) and a dashed diagonal line. Both model curves should bow above the diagonal.

Chart not appearing?

Make sure you ran Cell 8 and Cell 9 before this cell. The variables y_prob_lr and y_prob_dt must exist before plotting. If the chart appears blank, try restarting the kernel and running all cells from the top.

Help me fix my ROC curve plot.

✔️ 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 up to this point. Compare each cell against yours:

# Cell 1: Import Libraries
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.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score, roc_curve, classification_report, confusion_matrix

# Cell 2: Load the Data
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Explore the Data
print(f"Data types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")

# Cell 4: Visualize Churn Distribution
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sns.countplot(x='Churn', data=df, ax=axes[0])
axes[0].set_title('Churn Distribution')

sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[1])
axes[1].set_title('Tenure Distribution by Churn')

plt.tight_layout()
plt.show()

# Cell 5: Preprocess the Data
# Fix TotalCharges column (has empty strings)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'].fillna(df['TotalCharges'].median(), inplace=True)

# Drop customerID (not a feature)
df = df.drop('customerID', axis=1)

# Encode target variable
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})

print(f"After preprocessing - shape: {df.shape}")
print(f"Churn values: {df['Churn'].value_counts().to_dict()}")

# Cell 6: One-Hot Encode Categorical Features
# Identify categorical columns (exclude target)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
print(f"Categorical columns to encode: {categorical_cols}")

# One-hot encode
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=True)
print(f"Shape after encoding: {df_encoded.shape}")

# Cell 7: Split into Train and Test Sets
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():.2%}")
print(f"Churn rate in test: {y_test.mean():.2%}")

# Cell 8: Train Logistic Regression
log_reg = LogisticRegression(max_iter=1000, random_state=42)
log_reg.fit(X_train, y_train)

y_pred_lr = log_reg.predict(X_test)
y_prob_lr = log_reg.predict_proba(X_test)[:, 1]

print("=== Logistic Regression Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_lr):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_lr):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_lr):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_lr)}")

# Cell 9: Train Decision Tree
dt_clf = DecisionTreeClassifier(max_depth=5, random_state=42)
dt_clf.fit(X_train, y_train)

y_pred_dt = dt_clf.predict(X_test)
y_prob_dt = dt_clf.predict_proba(X_test)[:, 1]

print("=== Decision Tree Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_dt):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_dt):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_dt):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_dt):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_dt)}")

# Cell 10: Plot ROC Curves
fpr_lr, tpr_lr, _ = roc_curve(y_test, y_prob_lr)
fpr_dt, tpr_dt, _ = roc_curve(y_test, y_prob_dt)

plt.figure(figsize=(8, 6))
plt.plot(fpr_lr, tpr_lr, label=f'Logistic Regression (AUC = {roc_auc_score(y_test, y_prob_lr):.3f})')
plt.plot(fpr_dt, tpr_dt, label=f'Decision Tree (AUC = {roc_auc_score(y_test, y_prob_dt):.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

You now have two trained models with measurable performance scores. Next up, you'll dig into which features actually drive churn predictions and build a reusable function that predicts churn for any new customer.

Interpret Your Model and Make Predictions

Your models are trained and the ROC curves show how well they separate churners from loyal customers. Accuracy scores and AUC numbers are useful for comparing models, but they don't tell you which factors actually drive churn.

In this step, you'll dig into your Decision Tree model to extract the features it relies on most. You'll also build a reusable function that takes new customer data and predicts whether they'll churn.

In this step, get ready to:
  • Extract and visualize feature importance from the Decision Tree.
  • Build a prediction function for new customer data.
  • Identify top churn factors and connect them to business value.
Extract and visualize feature importance

Decision Trees make predictions by splitting data on whichever feature reduces uncertainty the most at each step. The model tracks how much each feature contributes to those splits, giving you a built-in importance score.

  • Create a new cell below Cell 10 in your notebook and add the following code to extract and plot feature importance:
# Cell 11: Feature Importance
feature_importance = pd.Series(
    dt_clf.feature_importances_, index=X_train.columns
).sort_values(ascending=False)

plt.figure(figsize=(10, 6))
feature_importance.head(15).plot(kind='barh')
plt.title('Top 15 Features - Decision Tree Importance')
plt.xlabel('Importance')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

print("Top 10 churn predictors:")
for i, (feature, importance) in enumerate(feature_importance.head(10).items(), 1):
    print(f"  {i}. {feature}: {importance:.4f}")

What does this code do?

dt_clf.feature_importances_ pulls an array of importance scores from your trained Decision Tree. Each score represents how much that feature contributed to reducing prediction error across all tree splits.

The code wraps these scores in a pandas Series with column names as the index, sorts them, and plots the top 15 as a horizontal bar chart. It also prints a numbered list of the top 10 predictors with their exact scores.

  • Run the cell by clicking the play button or pressing Shift+Enter.
  • You should see a horizontal bar chart showing which features your model considers most important, plus a printed list of the top 10 predictors with their scores.

Don't see the chart?

Make sure you ran all previous cells first (Cells 1-10). The dt_clf variable must exist from Cell 9.

If you see a NameError, click Run All at the top of the notebook to execute all cells from the beginning.

Help me fix my feature importance chart

Build a prediction function

A trained model is only useful if you can feed it new customer data and get a prediction back. The challenge is that your model expects a DataFrame with the exact same columns it was trained on (all 40+ one-hot encoded features).

  • Create a new cell below Cell 11 and add this prediction function:
# Cell 12: Prediction Function
def predict_churn(customer_data, model=log_reg, feature_columns=X_train.columns):
    """
    Predict churn probability for a new customer.

    Parameters:
        customer_data: dict with customer features
        model: trained model (default: Logistic Regression)
        feature_columns: expected feature column names

    Returns:
        dict with prediction and probability
    """
    # Create DataFrame with all expected columns set to 0
    input_df = pd.DataFrame(0, index=[0], columns=feature_columns)

    # Fill in provided values
    for key, value in customer_data.items():
        if key in input_df.columns:
            input_df[key] = value

    # Make prediction
    probability = model.predict_proba(input_df)[0][1]
    prediction = "Will Churn" if probability > 0.5 else "Will Stay"

    return {
        'prediction': prediction,
        'churn_probability': f"{probability:.2%}",
        'confidence': f"{max(probability, 1-probability):.2%}"
    }


# Example prediction
example_customer = {
    'tenure': 2,
    'MonthlyCharges': 80.0,
    'TotalCharges': 160.0,
    'Contract_Two year': 0,
    'Contract_One year': 0,
    'InternetService_Fiber optic': 1,
}

result = predict_churn(example_customer)
print(f"Prediction: {result['prediction']}")
print(f"Churn Probability: {result['churn_probability']}")
print(f"Confidence: {result['confidence']}")

What does this code do?

The predict_churn function solves the column-mismatch problem. It creates a blank DataFrame with all expected feature columns set to 0, then fills in only the values you provide. This means you can pass just a few key features without worrying about the 40+ one-hot encoded columns.

The example customer represents a high-risk profile: very short tenure (2 months), high monthly charges ($80), no long-term contract, and fiber optic internet. These are all factors commonly associated with churn.

  • Run the cell.
  • You should see output showing the prediction (Will Churn or Will Stay), the churn probability as a percentage, and a confidence score.

Seeing a NameError for log_reg?

The function uses log_reg as its default model and X_train.columns for feature names. Both of these variables must exist from earlier cells. Run all cells from the top if you restarted your kernel.

Help me debug my prediction function

✔️ Awesome, I've got everything!

Great. Double check you've saved your notebook before continuing.

ⓧ I'd like to double check the full code

Here is the full notebook code from Cell 1 through Cell 12. Compare against your notebook to catch any differences.

# Cell 1: Import Libraries
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.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score, roc_curve, classification_report, confusion_matrix

# Cell 2: Load the Data
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
print(f"Dataset shape: {df.shape}")
print(f"\nColumn names:\n{df.columns.tolist()}")
print(f"\nFirst 5 rows:")
df.head()

# Cell 3: Explore the Data
print(f"Data types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nChurn distribution:\n{df['Churn'].value_counts()}")

# Cell 4: Visualize Churn Distribution
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

sns.countplot(x='Churn', data=df, ax=axes[0])
axes[0].set_title('Churn Distribution')

sns.histplot(data=df, x='tenure', hue='Churn', kde=True, ax=axes[1])
axes[1].set_title('Tenure Distribution by Churn')

plt.tight_layout()
plt.show()

# Cell 5: Preprocess the Data
# Fix TotalCharges column (has empty strings)
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'].fillna(df['TotalCharges'].median(), inplace=True)

# Drop customerID (not a feature)
df = df.drop('customerID', axis=1)

# Encode target variable
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})

print(f"After preprocessing - shape: {df.shape}")
print(f"Churn values: {df['Churn'].value_counts().to_dict()}")

# Cell 6: One-Hot Encode Categorical Features
# Identify categorical columns (exclude target)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
print(f"Categorical columns to encode: {categorical_cols}")

# One-hot encode
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=True)
print(f"Shape after encoding: {df_encoded.shape}")

# Cell 7: Split into Train and Test Sets
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():.2%}")
print(f"Churn rate in test: {y_test.mean():.2%}")

# Cell 8: Train Logistic Regression
log_reg = LogisticRegression(max_iter=1000, random_state=42)
log_reg.fit(X_train, y_train)

y_pred_lr = log_reg.predict(X_test)
y_prob_lr = log_reg.predict_proba(X_test)[:, 1]

print("=== Logistic Regression Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_lr):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_lr):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_lr):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_lr)}")

# Cell 9: Train Decision Tree
dt_clf = DecisionTreeClassifier(max_depth=5, random_state=42)
dt_clf.fit(X_train, y_train)

y_pred_dt = dt_clf.predict(X_test)
y_prob_dt = dt_clf.predict_proba(X_test)[:, 1]

print("=== Decision Tree Results ===")
print(f"Accuracy: {accuracy_score(y_test, y_pred_dt):.4f}")
print(f"Precision: {precision_score(y_test, y_pred_dt):.4f}")
print(f"Recall: {recall_score(y_test, y_pred_dt):.4f}")
print(f"ROC-AUC: {roc_auc_score(y_test, y_prob_dt):.4f}")
print(f"\nClassification Report:\n{classification_report(y_test, y_pred_dt)}")

# Cell 10: Plot ROC Curves
fpr_lr, tpr_lr, _ = roc_curve(y_test, y_prob_lr)
fpr_dt, tpr_dt, _ = roc_curve(y_test, y_prob_dt)

plt.figure(figsize=(8, 6))
plt.plot(fpr_lr, tpr_lr, label=f'Logistic Regression (AUC = {roc_auc_score(y_test, y_prob_lr):.3f})')
plt.plot(fpr_dt, tpr_dt, label=f'Decision Tree (AUC = {roc_auc_score(y_test, y_prob_dt):.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve Comparison')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# Cell 11: Feature Importance
feature_importance = pd.Series(
    dt_clf.feature_importances_, index=X_train.columns
).sort_values(ascending=False)

plt.figure(figsize=(10, 6))
feature_importance.head(15).plot(kind='barh')
plt.title('Top 15 Features - Decision Tree Importance')
plt.xlabel('Importance')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

print("Top 10 churn predictors:")
for i, (feature, importance) in enumerate(feature_importance.head(10).items(), 1):
    print(f"  {i}. {feature}: {importance:.4f}")

# Cell 12: Prediction Function
def predict_churn(customer_data, model=log_reg, feature_columns=X_train.columns):
    """
    Predict churn probability for a new customer.

    Parameters:
        customer_data: dict with customer features
        model: trained model (default: Logistic Regression)
        feature_columns: expected feature column names

    Returns:
        dict with prediction and probability
    """
    # Create DataFrame with all expected columns set to 0
    input_df = pd.DataFrame(0, index=[0], columns=feature_columns)

    # Fill in provided values
    for key, value in customer_data.items():
        if key in input_df.columns:
            input_df[key] = value

    # Make prediction
    probability = model.predict_proba(input_df)[0][1]
    prediction = "Will Churn" if probability > 0.5 else "Will Stay"

    return {
        'prediction': prediction,
        'churn_probability': f"{probability:.2%}",
        'confidence': f"{max(probability, 1-probability):.2%}"
    }


# Example prediction
example_customer = {
    'tenure': 2,
    'MonthlyCharges': 80.0,
    'TotalCharges': 160.0,
    'Contract_Two year': 0,
    'Contract_One year': 0,
    'InternetService_Fiber optic': 1,
}

result = predict_churn(example_customer)
print(f"Prediction: {result['prediction']}")
print(f"Churn Probability: {result['churn_probability']}")
print(f"Confidence: {result['confidence']}")
Connect your model to business value

Look at the top 10 churn predictors printed by Cell 11. These are the features your model weighted most heavily when deciding whether a customer will leave.

What do these features mean for the business?

Feature importance tells a telecom company where to focus retention efforts. For example, if Contract_Two year ranks high, it means customers without a two-year contract are much more likely to churn. The business response: offer contract incentives to month-to-month customers.

If tenure is near the top, it confirms that new customers are the highest risk. The business response: invest in onboarding programs for the first few months of a customer's lifecycle.

This is where machine learning becomes actionable. The model doesn't just predict who will leave. It tells you why they leave, which is what the business needs to act on.

  • Review your top 10 list and identify 2-3 features that a business could directly act on (e.g., contract type, monthly charges, internet service type).
  • Try modifying the values in your example_customer dict (like changing tenure to 24 or setting Contract_Two year to 1) and re-running Cell 12 to see how the prediction changes.

Why feature importance matters for your portfolio

In job interviews, interviewers rarely ask "what was your model's accuracy?" They ask "what did you learn from the data?" and "what would you recommend?" Being able to translate feature importance into business recommendations is what separates a data scientist from someone who just runs code.

Secret mission

Build a Random Forest Ensemble

You've trained two models, but what if you could combine the strengths of many decision trees into one powerful predictor? In this secret mission, you'll build a Random Forest ensemble and create a performance dashboard that compares all three models side-by-side.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder on your Desktop (contains virtual environment, notebook, and dataset).
  • Jupyter extension in VS Code.
  • Kaggle account (used to download the dataset).

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with your models further.

  • Your project folder stays on your Desktop with the virtual environment, notebook, and dataset intact.
  • The Jupyter extension remains installed in VS Code for future notebooks.
  • Your Kaggle account stays active for downloading other datasets.

Pause - I'll come back to this later

Close VS Code to free up memory. Your files stay on disk so you can pick up where you left off.

  • Close the VS Code window.
  • Your project folder, virtual environment, and notebook remain on your Desktop.
  • When you return, open the project folder in VS Code and select the virtual environment kernel to resume.

Delete - I don't want to use this again

Remove all project resources and start fresh. This deletes your notebook, trained models, dataset, and virtual environment.

  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/churn-prediction

Windows

rmdir /s /q %USERPROFILE%\Desktop\churn-prediction
  • Uninstall the Jupyter extension in VS Code by opening the Extensions panel, finding Jupyter, and clicking Uninstall.

Nice Work!

Nice Work!

Nice work! You just built a complete machine learning pipeline that predicts customer churn using real-world telecom data.

You've learned how to:

  • Preprocess raw data for machine learning by handling missing values, encoding categorical features, and splitting into train/test sets.
  • Train and evaluate Logistic Regression and Decision Tree classifiers using industry-standard metrics like accuracy, precision, recall, and ROC-AUC.
  • Visualize feature importance to extract business insights and build a reusable prediction function that scores new customers.
  • Built a Random Forest ensemble and created a model comparison dashboard to identify the best performer.

Ready to quiz yourself?