Predict Customer Churn with ML
Train two ML models on IBM Telco data to predict which customers will leave.
Introduction
30 Second Summary
Think about the last time you cancelled a subscription. For telecom companies, every cancelled account is revenue that could have been saved with the right intervention at the right time.
In this project, you will build a customer churn prediction system using Python and scikit-learn. You'll train two machine learning models on real telecom data from IBM to discover what makes customers leave.
What You'll Build
When you run your finished script, it loads over 7,000 real customer records, trains both models, prints a head-to-head accuracy comparison, and saves a feature importance chart showing what actually drives churn.
By the end of this project, you'll have:
- A cleaned dataset of 7,000+ real telecom customer records loaded with pandas, with a hidden data quality issue you discovered and fixed yourself.
- A trained Logistic Regression model that looks 80% accurate, until a confusion matrix reveals it's missing many of the actual churners.
- A Random Forest model that outperforms the baseline, plus a feature importance chart showing the top 10 attributes that predict whether a customer will leave.
- Secret Mission: Generate a Churn Risk Report that ranks every test customer by their probability of leaving.
Are there any prerequisites?
This project assumes you're comfortable with basic Python (variables, loops, functions) and basic pandas (loading CSVs, working with DataFrames). No prior machine learning experience is needed.
Before We Start
Before diving into any code, let's lock in what you're about to build and why it matters.
Set Up Your Environment
To predict customer churn, you need a few key tools. scikit-learn handles the machine learning, pandas handles the data, and matplotlib handles the charts.
In this step, you'll install these libraries, download IBM's Telco Customer Churn dataset, and create your first Python script to confirm everything connects.
In this step, get ready to:
- Install scikit-learn, pandas, and matplotlib.
- Download the IBM Telco Customer Churn dataset.
- Create your Python script and verify it loads the data.
Install the required Python libraries
You need three libraries that don't ship with Python by default. pip, Python's package manager, will fetch and install all three in one command.
- In VS Code, click Terminal in the top menu bar.
- Click New Terminal.
- Install all three libraries by running this command:
pip install scikit-learn pandas matplotlib
You should see Successfully installed messages as each library finishes downloading.
- Confirm each library is accessible by running these commands:
python -c "import sklearn; print('scikit-learn', sklearn.__version__)"
python -c "import pandas; print('pandas', pandas.__version__)"
python -c "import matplotlib; print('matplotlib', matplotlib.__version__)"
Each command prints the library name followed by a version number. For example, scikit-learn should print something like scikit-learn 1.9.0.
Seeing 'command not found' for pip?
Try replacing pip with pip3 in the install command. On some systems, pip3 is the correct command for Python 3.
If that doesn't work, try running python -m pip install scikit-learn pandas matplotlib instead.
Still stuck? Help me fix my pip installation so I can install scikit-learn, pandas, and matplotlib.
Download the IBM Telco dataset
IBM publishes a public dataset of 7,043 telecom customer records. You'll download the raw CSV and store it in a dedicated project folder.
- 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
- Open your web browser.
- Copy and paste this URL into the address bar:
https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv
- Press Enter to load the page. You'll see raw CSV data displayed as text.
- Press Cmd+S (macOS) or Ctrl+S (Windows) to open the Save dialog.
- Navigate to the churn-prediction folder on your Desktop.
- Make sure the filename is Telco-Customer-Churn.csv and click Save.
- Back in your VS Code terminal, confirm the file landed in the right folder by running:
ls
You should see Telco-Customer-Churn.csv listed in the output.
Don't see the CSV file?
Check your Downloads folder. Your browser may have saved the file there instead of in churn-prediction. Move it into the churn-prediction folder on your Desktop.
Also make sure the filename is exactly Telco-Customer-Churn.csv. Some browsers add a .txt extension or change the name. Rename it if needed.
Need a hand? Help me find and move my downloaded CSV file into my project folder.
Create your prediction script
Now you'll open the project folder in VS Code and create the Python script that will hold all your churn prediction code.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the churn-prediction folder, and click Open.
Trust the workspace
VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.
You should see Telco-Customer-Churn.csv in the Explorer sidebar on the left.
- Click the New File icon next to the folder name in the Explorer sidebar.
- Type churn_prediction.py as the filename.
- Press Enter to create the file.
- Add the following code to churn_prediction.py:
import pandas as pd
# Load the Telco Customer Churn dataset
df = pd.read_csv("Telco-Customer-Churn.csv")
print("Dataset loaded:", df.shape)
What does this code do?
- import pandas as pd loads the pandas library and gives it the shorthand name pd so you can call its functions more easily.
- pd.read_csv() reads the CSV file into a DataFrame, which is a table-like structure you can filter, sort, and analyze.
- df.shape returns the number of rows and columns in the dataset, confirming the file loaded correctly.
- Save churn_prediction.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open a new terminal by clicking Terminal in the menu bar, then clicking New Terminal.
Before you run this script, what do you expect the output will tell you about the dataset?
- Run your script by entering this command:
python churn_prediction.py
You should see Dataset loaded: (7043, 21). That means 7,043 customer records across 21 columns loaded successfully.
Seeing a FileNotFoundError?
This means Python cannot find Telco-Customer-Churn.csv in the same folder as your script. Make sure both files are inside the churn-prediction folder on your Desktop.
Also check that your terminal is in the right folder. Run pwd (macOS) or cd (Windows) to print the current directory. It should end with churn-prediction.
Still stuck? Help me figure out why Python can't find my CSV file.
✔️ Awesome, I've got everything!
Great. Make sure you've saved churn_prediction.py and you're ready to move on.
ⓧ I'd like to double check the full code
Here's the full churn_prediction.py so far. Make sure yours matches exactly:
import pandas as pd
df = pd.read_csv("Telco-Customer-Churn.csv")
print("Dataset loaded:", df.shape)
Your environment is ready and the dataset is loaded. Next up, you'll explore the data and uncover a hidden problem lurking in one of the columns.
Explore the Data and Discover a Hidden Problem
Your environment is ready and the dataset loads without errors. Before training any machine learning model, you need to understand the shape and quality of the data you're working with.
In this step, you'll inspect the dataset's structure, discover a hidden data quality issue lurking inside one of the columns, and visualize how the churn labels are distributed. Catching problems like this early prevents mysterious crashes later.
In this step, get ready to:
- Explore the dataset's shape, column types, and churn distribution.
- Discover and fix a hidden data quality issue in the TotalCharges column.
- Visualize the churn split to reveal class imbalance.
Inspect the dataset and discover a hidden problem
The IBM Telco dataset has 21 columns covering customer demographics, services, billing, and whether the customer churned. Printing the column types and churn counts gives you a quick health check of the data before you feed it to a model.
- Replace the entire contents of churn_prediction.py with the following exploration code:
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# ============================================================
# STEP 2: Load, explore, and clean the data
# ============================================================
df = pd.read_csv("Telco-Customer-Churn.csv")
print("Dataset shape:", df.shape)
print("\nColumn data types:")
print(df.dtypes)
print("\nChurn distribution:")
print(df["Churn"].value_counts())
print(df["Churn"].value_counts(normalize=True))
# Discover TotalCharges is stored as object (string)
print("\nTotalCharges dtype:", df["TotalCharges"].dtype)
What does this code do?
- matplotlib.use("Agg") sets matplotlib to a non-interactive backend. This prevents the script from hanging when you save charts from the terminal instead of a notebook.
- df.dtypes prints the data type of every column, which is how you spot columns that aren't stored the way you'd expect.
- value_counts(normalize=True) shows the churn split as percentages instead of raw counts, making it easy to see how imbalanced the classes are.
Before you run this, think about the TotalCharges column. It holds dollar amounts. What data type would you expect it to be?
- Save churn_prediction.py and run it with this command:
python churn_prediction.py
Scroll through the output. You'll see the dataset has 7043 rows and 21 columns. The churn distribution shows roughly 73% "No" and 27% "Yes." At the very bottom, the TotalCharges dtype prints as object instead of float64. That's the hidden problem.
Why is object a problem?
In pandas, object dtype means the column is stored as text. Some rows in TotalCharges contain empty strings instead of numbers. These are brand-new customers with zero tenure.
If you tried to train a model on this column as-is, it would crash because machine learning models expect numbers, not text. You need to convert it first.
Seeing a ModuleNotFoundError?
- If the error mentions matplotlib, confirm you installed it in the previous step by running pip install matplotlib.
- Make sure Telco-Customer-Churn.csv is in the same folder as churn_prediction.py. A mismatched location causes a FileNotFoundError.
- Still stuck? Help me debug this error when running churn_prediction.py.
Clean the data by fixing TotalCharges
Now that you've spotted the problem, you need to convert TotalCharges to a proper number and handle the rows that can't be converted. You'll also drop the customerID column because an identifier doesn't help predict churn.
- Add the following cleaning code at the bottom of churn_prediction.py, below the TotalCharges dtype print line:
# Fix TotalCharges: convert to numeric, coerce bad values to NaN
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
print("\nMissing values after conversion:")
print(df.isnull().sum()[df.isnull().sum() > 0])
# Drop rows with missing TotalCharges (11 rows)
df = df.dropna(subset=["TotalCharges"])
# Drop customerID - not a predictive feature
df = df.drop(columns=["customerID"])
print("\nDataset shape after cleaning:", df.shape)
What does this code do?
- pd.to_numeric(errors="coerce") tries to convert every value in TotalCharges to a number. Values that can't be converted (the empty strings) become NaN instead of crashing the script.
- dropna(subset=["TotalCharges"]) removes only the 11 rows where TotalCharges is now NaN. The rest of the dataset stays intact.
- Dropping customerID prevents the model from treating a unique identifier as a meaningful feature. A random string of characters doesn't predict churn.
- Save churn_prediction.py and run it again:
python churn_prediction.py
The output now shows 11 missing values in TotalCharges after conversion. After dropping those rows and the customerID column, the cleaned dataset shape prints as (7032, 20).
Still seeing 7043 rows?
- Make sure the cleaning code is placed after the exploration prints, not before them. The order matters because dropna modifies the DataFrame in place.
- Check that you used errors="coerce" in the pd.to_numeric() call. Without it, the conversion raises an error instead of creating NaN values.
- Help me figure out why my cleaned dataset shape doesn't show (7032, 20).
Visualize the churn distribution
Numbers tell you the split is roughly 73/27. A bar chart makes the imbalance impossible to ignore.
- Add the following visualization code at the bottom of churn_prediction.py:
# Visualize churn distribution
df["Churn"].value_counts().plot(kind="bar", color=["steelblue", "salmon"])
plt.title("Customer Churn Distribution")
plt.xlabel("Churn")
plt.ylabel("Count")
plt.xticks(rotation=0)
plt.tight_layout()
plt.savefig("churn_distribution.png", bbox_inches="tight")
plt.close()
print("Saved churn_distribution.png")
What does this code do?
- value_counts().plot(kind="bar") counts how many customers fall into "No" vs "Yes" for churn and draws a bar for each.
- plt.savefig() writes the chart to a PNG file instead of opening a window. plt.close() frees the memory used by that figure so it doesn't build up across multiple charts.
Before you run the script, predict what the bar chart will look like. Which bar do you expect to be taller, "No" or "Yes"?
- Save churn_prediction.py and run it:
python churn_prediction.py
The last line of output reads Saved churn_distribution.png.
- Confirm the chart was created by running:
ls *.png
You should see churn_distribution.png in the output. Open it from the VS Code Explorer sidebar or your file browser to see the chart.
Why does the imbalance matter?
The chart shows roughly 3x more "No" bars than "Yes." This is called class imbalance. Only about 27% of customers in this dataset actually churned.
Keep this in mind. A model that simply guesses "No Churn" for every customer would be right 73% of the time. That means raw accuracy can be dangerously misleading. You'll see exactly how in a later step.
No PNG file created?
- Check that you have matplotlib.use("Agg") near the top of your file, before import matplotlib.pyplot as plt. Without it, the script may hang waiting for a display window.
- Make sure your terminal is in the churn-prediction folder. The PNG saves to whatever directory the script runs from.
- Help me figure out why my churn distribution chart isn't saving.
✔️ Awesome, I've got everything!
Great. Double check you've saved churn_prediction.py.
ⓧ I'd like to double check the full code
The full file is built from three sections you added above. Scroll up and confirm each one is present in this order:
- Imports and exploration code (imports, CSV load, shape/dtypes/churn distribution prints, TotalCharges dtype print).
- Cleaning code (pd.to_numeric conversion, dropna, drop customerID, cleaned shape print).
- Visualization code (bar chart creation, save to churn_distribution.png, plt.close()).
Your data is clean and you can see the churn imbalance staring back at you from the chart. Next up, you'll transform this cleaned data into something a machine learning model can actually learn from, and train your first classifier.
Build a Baseline Churn Predictor
Your dataset is clean, with 7,032 customer records and 20 features ready for analysis. Now it's time to build your first machine learning model.
Your data is full of text values like 'Yes', 'No', and 'Month-to-month'. Machine learning models only understand numbers, so you'll need to convert everything first. Then you'll train a simple Logistic Regression model to establish a performance baseline.
In this step, get ready to:
- Encode categorical text features into numbers that scikit-learn can process.
- Split the data into training and test sets and scale the numerical features.
- Train a Logistic Regression model and check its accuracy.
Encode the categorical features into numbers
scikit-learn can't work with text columns like 'Yes' or 'Month-to-month'. You need to convert every text feature into a number before training any model.
- In churn_prediction.py, find the imports at the top of your file. Add these new imports directly below import matplotlib.pyplot as plt:
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay
Why import everything now?
Some of these imports (like RandomForestClassifier and ConfusionMatrixDisplay) aren't used until later steps. Adding them all now keeps your imports organized in one place at the top of the file.
- Now scroll to the bottom of your file, below the line that says print("Saved churn_distribution.png"). Add this code to encode the binary Yes/No columns:
# ============================================================
# STEP 3: Preprocess and build baseline model
# ============================================================
# Encode binary categorical columns with LabelEncoder
label_encoder = LabelEncoder()
binary_columns = ["gender", "Partner", "Dependents", "PhoneService",
"PaperlessBilling", "Churn"]
for col in binary_columns:
df[col] = label_encoder.fit_transform(df[col])
What does this code do?
LabelEncoder converts text values like 'Male'/'Female' and 'Yes'/'No' into numbers (0 and 1). This works well for columns that have exactly two categories.
The loop applies this conversion to all six binary columns at once. After this runs, every 'Yes' becomes 1 and every 'No' becomes 0.
- Save churn_prediction.py and run your script to confirm the new imports and encoding work:
python churn_prediction.py
You should see the same Step 2 output as before, with no import errors or crashes at the end.
Seeing a ModuleNotFoundError?
- Make sure you installed scikit-learn with pip install scikit-learn in Step 1. The import uses sklearn, not scikit-learn.
- Check that you're running the script in the same terminal where you installed the packages.
Why can't Python find my scikit-learn imports?
Binary columns are handled. But columns like InternetService have three or more categories (like 'DSL', 'Fiber optic', 'No'). Assigning them numbers 0, 1, 2 would imply an order that doesn't exist.
- Add this code below the LabelEncoder loop to one-hot encode the multi-category columns:
# One-hot encode multi-category columns
multi_columns = ["MultipleLines", "InternetService", "OnlineSecurity",
"OnlineBackup", "DeviceProtection", "TechSupport",
"StreamingTV", "StreamingMovies", "Contract", "PaymentMethod"]
df = pd.get_dummies(df, columns=multi_columns, drop_first=True)
What is one-hot encoding?
One-hot encoding creates a new column for each category. For example, Contract (which has values 'Month-to-month', 'One year', 'Two year') becomes two new columns: Contract_One year and Contract_Two year. Each column holds a 1 or 0.
The drop_first=True parameter drops the first category to avoid redundancy. If both 'One year' and 'Two year' are 0, the model knows the customer is on 'Month-to-month'.
- Save churn_prediction.py and run your script again:
python churn_prediction.py
The script should complete without errors. All your text features are now numeric and ready for a model.
Split the data and scale numerical features
Before training a model, you need to separate your data into two groups. One group is for training. The other is for testing whether the model can predict churn on data it has never seen.
- Add the following code below your pd.get_dummies line to split the data and scale the numerical features:
# Split features and target
X = df.drop(columns=["Churn"])
y = df["Churn"]
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Scale numerical features
scaler = StandardScaler()
X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()
numerical_cols = ["tenure", "MonthlyCharges", "TotalCharges", "SeniorCitizen"]
X_train_scaled[numerical_cols] = scaler.fit_transform(X_train[numerical_cols])
X_test_scaled[numerical_cols] = scaler.transform(X_test[numerical_cols])
What does this code do?
- X holds all the feature columns (everything except Churn). y holds the target column (Churn) that the model will learn to predict.
- train_test_split randomly sets aside 20% of the data for testing. The random_state=42 ensures the split is reproducible every time you run the script.
- StandardScaler centers the numerical columns so they all have similar ranges. Without scaling, a column like TotalCharges (values in the thousands) would dominate smaller columns like tenure (values 0 to 72).
Why fit on training data only?
Notice that fit_transform is called on the training set but only transform is called on the test set. The scaler learns the mean and standard deviation from training data only. Fitting on the test set would leak information about data the model isn't supposed to see yet.
- Save churn_prediction.py and run your script to confirm the split and scaling run without errors:
python churn_prediction.py
You should see the same output as before, ending with the churn distribution chart confirmation. Your data is now split into training and test sets, scaled and ready for a model.
Seeing a KeyError or ValueError?
- A KeyError on a column name means that column doesn't exist in your DataFrame. Double check that the column names in numerical_cols match exactly: tenure, MonthlyCharges, TotalCharges, SeniorCitizen.
- Make sure the encoding code from the previous chunk runs before this code. The split relies on the encoded DataFrame.
Why am I getting a KeyError when splitting or scaling my data?
Train a Logistic Regression model and check its accuracy
Logistic Regression is one of the simplest classification models. It draws a line between 'will churn' and 'won't churn' based on the features. It's the perfect starting point because it trains fast and gives you a clear baseline to compare against.
- Add this code below your scaling code to train the Logistic Regression model and print its accuracy:
# Train Logistic Regression baseline
lr_model = LogisticRegression(random_state=42, max_iter=1000)
lr_model.fit(X_train_scaled, y_train)
lr_predictions = lr_model.predict(X_test_scaled)
lr_accuracy = accuracy_score(y_test, lr_predictions)
print("\n=== LOGISTIC REGRESSION RESULTS ===")
print(f"Accuracy: {lr_accuracy:.4f}")
What does this code do?
- LogisticRegression(random_state=42, max_iter=1000) creates the model. The max_iter=1000 gives the solver enough iterations to converge on a solution.
- fit() trains the model on your scaled training data. predict() then uses the trained model to guess churn labels for the test set.
- accuracy_score() compares the model's predictions to the actual test labels and returns the percentage it got right.
- Save churn_prediction.py.
Before you run this, take a guess. Roughly what percentage of customers does the model predict correctly? Remember that about 73% of customers don't churn at all.
- Run your script to see the baseline accuracy:
python churn_prediction.py
At the end of the output, you should see new lines that look like this:
=== LOGISTIC REGRESSION RESULTS ===
Accuracy: 0.8029
Around 80% accuracy. That sounds pretty good for a first model.
Not seeing the accuracy output?
- If the script finishes without printing the accuracy, make sure your Logistic Regression code is at the very bottom of the file, after all the encoding and scaling code.
- If you see a ConvergenceWarning, check that max_iter=1000 is set in the LogisticRegression() call. The default of 100 iterations may not be enough.
Why isn't my Logistic Regression model printing results?
✔️ Awesome, I've got everything!
Great. Double check that you've saved churn_prediction.py and confirm your script prints the accuracy line at the end.
ⓧ I'd like to double check the full code
Your file is getting long, so scroll through and confirm each section is present in this order:
- Imports at the top: pandas, matplotlib, and all five sklearn import lines.
- Step 2 section: Loading the CSV, printing shape and dtypes, fixing TotalCharges, dropping rows and customerID, and saving the churn distribution chart.
- Step 3 section: LabelEncoder on binary columns, pd.get_dummies on multi-category columns, train/test split, StandardScaler on numerical columns, and Logistic Regression training with accuracy print.
Your baseline is set at around 80% accuracy. That sounds decent. But is the model actually doing its job? In the next step, you'll dig into the confusion matrix and discover what accuracy is really hiding.
Uncover the Accuracy Trap
In the previous step, your Logistic Regression model scored around 80% accuracy on the test data. That looks like a promising first result.
But think about it this way. Only about 27% of customers in this dataset actually churn. A model that blindly predicted "No Churn" for every single customer would still be right 73% of the time, while being completely useless. So is your model really learning to spot churners, or is it just riding the class imbalance?
In this step, get ready to:
- Print the confusion matrix and classification report to see what accuracy hides.
- Visualize the confusion matrix as a chart to make the model's blind spots obvious.
Print the confusion matrix and classification report
A confusion matrix breaks down your model's predictions into four buckets: correct "No Churn" predictions, correct "Churn" predictions, customers it wrongly flagged as churners, and customers who actually churned but were missed. The classification report adds precision, recall, and F1 scores for each class.
- In churn_prediction.py, scroll to the bottom of your file and add the following code below the accuracy print statement:
# ============================================================
# STEP 4: Uncover the accuracy trap
# ============================================================
print("\nConfusion Matrix:")
print(confusion_matrix(y_test, lr_predictions))
print("\nClassification Report:")
print(classification_report(y_test, lr_predictions,
target_names=["No Churn", "Churn"]))
What does this code do?
- confusion_matrix(y_test, lr_predictions) compares the true labels against your model's predictions, producing a 2x2 grid of counts.
- classification_report calculates precision (of the customers flagged as churners, how many actually churned), recall (of the customers who actually churned, how many did the model catch), and F1 score for each class.
- target_names=["No Churn", "Churn"] labels the rows so the report is readable instead of showing raw 0s and 1s.
- Save churn_prediction.py.
Before you run your script, make a prediction. Your model got 80% accuracy. Do you think it is catching most of the customers who actually churn, or is it missing a lot of them?
- Run your script to see what the confusion matrix reveals:
python churn_prediction.py
Look at the confusion matrix output. The bottom-left cell shows how many actual churners were predicted as "No Churn." These are false negatives. Each one is a customer you could have saved but your model missed entirely.
Now look at the classification report. The Churn class has noticeably lower recall than the No Churn class. That means the model is much better at confirming loyal customers than it is at catching the ones about to leave.
How do I read a confusion matrix?
The 2x2 grid reads like this. The top-left cell is customers correctly predicted as No Churn (true negatives). The top-right cell is loyal customers wrongly flagged as churners (false positives).
The bottom-left cell is actual churners the model missed (false negatives). The bottom-right cell is churners the model correctly caught (true positives). In a churn prediction scenario, the bottom-left cell is the most dangerous. Every missed churner is lost revenue.
Seeing a NameError or KeyError?
- Make sure you did not accidentally delete or overwrite the lr_predictions or y_test variables from the previous step.
- Confirm that your new code is placed below the accuracy print statement, not above it. The variables need to be created before your code tries to use them.
Still stuck? Help me debug my confusion matrix code in churn_prediction.py.
Visualize the confusion matrix
Numbers in a grid are useful, but a visual chart makes the imbalance between true positives and false negatives impossible to miss. scikit-learn provides a built-in ConfusionMatrixDisplay that turns your confusion matrix into a labeled heatmap.
- Add the following code at the bottom of churn_prediction.py, below the classification report code you just added:
# Visualize confusion matrix
ConfusionMatrixDisplay.from_estimator(
lr_model, X_test_scaled, y_test,
display_labels=["No Churn", "Churn"]
)
plt.title("Logistic Regression - Confusion Matrix")
plt.savefig("lr_confusion_matrix.png", bbox_inches="tight")
plt.close()
print("Saved lr_confusion_matrix.png")
What does this code do?
- ConfusionMatrixDisplay.from_estimator() takes the trained model, test features, and true labels, then generates a color-coded confusion matrix plot.
- display_labels=["No Churn", "Churn"] replaces the raw 0/1 axis labels with readable names.
- plt.close() frees the figure from memory after saving. Without it, plots can stack up and slow down your script.
- Save churn_prediction.py and run your script again:
python churn_prediction.py
- You should see Saved lr_confusion_matrix.png in your terminal output.
- Open lr_confusion_matrix.png from your churn-prediction folder. The heatmap makes the imbalance obvious: the model is great at predicting "No Churn" but weaker at catching actual churners.
Why is this called the accuracy trap?
Your model's 80% accuracy sounds good, but the confusion matrix tells the real story. The model earns most of its "correct" predictions by getting the majority class (No Churn) right, while missing a significant chunk of actual churners.
In a business context, every missed churner is a customer who leaves without anyone on the retention team knowing they were at risk. Accuracy alone hides this cost. That is why metrics like recall and the confusion matrix are essential for imbalanced datasets.
Don't see the saved chart?
- Make sure matplotlib.use("Agg") is still near the top of your file, right after import matplotlib. Without the Agg backend, saving can fail silently on some systems.
- Check that plt.savefig("lr_confusion_matrix.png", bbox_inches="tight") appears before plt.close(). If they are swapped, the figure is cleared before saving.
Still having trouble? Help me debug why my confusion matrix chart is not saving.
✔️ Awesome, I've got everything!
Great. Double check you have saved churn_prediction.py and that lr_confusion_matrix.png exists in your project folder.
ⓧ I'd like to double check the full code
Your churn_prediction.py file is now over 30 lines long. Scroll through it from top to bottom and confirm these sections appear in order:
- Imports at the top (pandas, matplotlib, sklearn modules).
- STEP 2 section: loading, exploring, and cleaning the data, ending with the churn distribution bar chart.
- STEP 3 section: encoding features, train/test split, scaling, training Logistic Regression, and printing accuracy.
- STEP 4 section: printing the confusion matrix and classification report, then the ConfusionMatrixDisplay visualization saved as lr_confusion_matrix.png.
Now you can see what 80% accuracy was really hiding. Your baseline model misses too many actual churners. In the next step, you will train a Random Forest classifier to close that gap and extract the features that drive churn the most.
Improve with Random Forest and Find Churn Drivers
The confusion matrix just exposed the Logistic Regression model's blind spot. It looks accurate on the surface, but it misses a significant number of actual churners.
Random Forest is an ensemble method that combines hundreds of decision trees to make better predictions. In this step, you'll train a Random Forest classifier, compare it head-to-head with your baseline, and extract the features that actually drive churn.
In this step, get ready to:
- Train a Random Forest classifier and compare its performance against the Logistic Regression baseline.
- Extract and visualize the top 10 features that predict customer churn.
Train a Random Forest model and compare it to the baseline
A single decision tree can overfit to quirks in the training data. Random Forest solves this by training many trees on random subsets of the data and combining their votes.
Why Random Forest instead of just a bigger Logistic Regression?
Logistic Regression draws a single straight boundary between "churn" and "no churn." It works well when the relationship between features and the outcome is roughly linear.
Random Forest captures complex, non-linear patterns by letting each tree split the data in different ways. It also gives you feature importance scores, revealing which customer attributes matter most for predicting churn.
- Add the following code at the bottom of churn_prediction.py to train a Random Forest model and compare it with the baseline:
# ============================================================
# STEP 5: Improve with Random Forest
# ============================================================
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train_scaled, y_train)
rf_predictions = rf_model.predict(X_test_scaled)
rf_accuracy = accuracy_score(y_test, rf_predictions)
print("\n=== RANDOM FOREST RESULTS ===")
print(f"Accuracy: {rf_accuracy:.4f}")
print("\nConfusion Matrix:")
print(confusion_matrix(y_test, rf_predictions))
print("\nClassification Report:")
print(classification_report(y_test, rf_predictions,
target_names=["No Churn", "Churn"]))
# Model comparison
print("\n=== MODEL COMPARISON ===")
print(f"Logistic Regression Accuracy: {lr_accuracy:.4f}")
print(f"Random Forest Accuracy: {rf_accuracy:.4f}")
What does this code do?
- RandomForestClassifier(n_estimators=100) creates an ensemble of 100 decision trees. Each tree learns slightly different patterns from the data, and they vote together on each prediction.
- The confusion matrix and classification report let you compare churn recall directly with the Logistic Regression results from the previous step.
- The model comparison section prints both accuracies side by side so you can see the overall difference at a glance.
Before you run this, do you think the Random Forest will catch more actual churners than the Logistic Regression? Why or why not?
- Save churn_prediction.py and run your script with this command:
python churn_prediction.py
Scroll to the bottom of the output. You should see the Random Forest results followed by a model comparison.
Check the classification report for the Churn class. The recall value should be noticeably higher than what you saw with Logistic Regression in the previous step. The Random Forest is catching more of the actual churners that the baseline missed.
Seeing an import error or name error?
- Make sure your new code is placed after the Step 4 code (below the print("Saved lr_confusion_matrix.png") line), not inside or above it.
- Check that RandomForestClassifier is already in your import block at the top of the file. It should have been added in Step 1.
- Confirm that the variables X_train_scaled, X_test_scaled, y_train, and y_test are defined in your Step 3 code above.
Still stuck? Ask for help.
Extract and visualize feature importance
One major advantage of Random Forest is that it tracks how much each feature contributed to its predictions. This lets you answer the business question: what actually makes customers leave?
- Add the following code below the model comparison section in churn_prediction.py to extract and print the top 10 most important features:
# Feature importance
feature_importance = pd.Series(
rf_model.feature_importances_, index=X.columns
)
top_features = feature_importance.sort_values(ascending=False).head(10)
print("\nTop 10 Most Important Features:")
for feature, importance in top_features.items():
print(f" {feature}: {importance:.4f}")
What does this code do?
- rf_model.feature_importances_ returns an array of scores showing how much each feature contributed to the model's predictions. Higher scores mean more influence on the churn decision.
- Wrapping the scores in a pd.Series with X.columns as the index pairs each score with its feature name, so you can read the results easily.
- Save churn_prediction.py and run your script:
python churn_prediction.py
At the bottom of the output, you should see a ranked list of the 10 most important features with their importance scores.
- Now add the following code below the feature importance print block to create a horizontal bar chart of the top 10 features:
# Plot feature importance
top_features.sort_values().plot(kind="barh", figsize=(10, 6), color="steelblue")
plt.title("Top 10 Features Driving Customer Churn")
plt.xlabel("Feature Importance")
plt.tight_layout()
plt.savefig("feature_importance.png", bbox_inches="tight")
plt.close()
print("\nSaved feature_importance.png")
What does this code do?
- top_features.sort_values() sorts the features from lowest to highest so the most important feature appears at the top of the horizontal bar chart.
- The chart is saved as feature_importance.png so you can view it outside the terminal.
Which customer attributes do you think will rank as the strongest churn predictors? Make a guess before you run it.
- Save churn_prediction.py and run your script one more time:
python churn_prediction.py
- Open feature_importance.png from your project folder.
You should see a horizontal bar chart of the top 10 features driving customer churn. Features like tenure, MonthlyCharges, TotalCharges, and Contract type typically rank near the top.
What do these features tell you?
The chart connects model outputs to real-world business reasoning. Short-tenure customers on month-to-month contracts with high monthly bills are the most likely to churn.
A retention team could act on this by targeting those customers with contract incentives or pricing adjustments. That's the difference between a model that reports a number and one that drives a decision.
Chart not appearing or looks empty?
- Check that feature_importance.png exists in your project folder. If it doesn't, make sure the plotting code is below the feature importance print block, not inside it.
- If the chart is blank, confirm that top_features is defined before the plotting code. The variable is created in the feature extraction block above.
- Make sure you have plt.close() at the end. Without it, the figure may not save properly.
Need a hand? Ask for help.
✔️ Awesome, I've got everything!
Great. Double check you've saved churn_prediction.py and that feature_importance.png is in your project folder.
ⓧ I'd like to double check the full code
Your churn_prediction.py file should contain these sections in order. Scroll through your file and confirm each one is present:
- Imports at the top: pandas, matplotlib, and all sklearn imports.
- Step 2: Load, explore, and clean the data (ends with saving churn_distribution.png).
- Step 3: Preprocess and build baseline model (ends with printing Logistic Regression accuracy).
- Step 4: Uncover the accuracy trap (ends with saving lr_confusion_matrix.png).
- Step 5: Improve with Random Forest. This includes the RF training, confusion matrix, classification report, model comparison, feature importance extraction, and the feature_importance.png chart.
Secret mission
Generate a Churn Risk Report
Your Random Forest model can do more than predict Yes or No. It can estimate the probability that each customer will churn. In this secret mission, you'll use those probabilities to build a ranked risk report that a retention team could act on immediately.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference or delete them to free up disk space.
Resources you used:
- The churn-prediction project folder on your Desktop, containing churn_prediction.py, Telco-Customer-Churn.csv, and your generated charts.
- Python packages installed via pip: scikit-learn, pandas, and matplotlib.
Keep everything running
No action needed. Choose this if you want to keep your churn prediction script and charts for future reference or experimentation.
- Your churn-prediction folder stays on your Desktop with all your files and generated charts.
- The scikit-learn, pandas, and matplotlib packages remain installed in your Python environment for future projects.
- There are no ongoing costs. Everything is local.
Pause - I'll come back to this later
Since this project has no running processes or cloud resources, pausing is the same as keeping everything. Your files sit on disk until you return.
- Your project folder and installed packages stay in place at no cost.
- When you come back, open the churn-prediction folder in VS Code and run python churn_prediction.py to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and optionally uninstall the Python packages you added.
- Delete the project folder by running this command in your terminal.
On macOS or Linux:
rm -rf ~/Desktop/churn-prediction
On Windows:
rmdir /s /q %USERPROFILE%\Desktop\churn-prediction
- This removes churn_prediction.py, Telco-Customer-Churn.csv, and all generated chart images (churn_distribution.png, lr_confusion_matrix.png, feature_importance.png, churn_risk_distribution.png).
- To uninstall the Python packages you installed for this project, run:
pip uninstall scikit-learn pandas matplotlib -y
- Only uninstall these packages if no other projects on your machine depend on them.
Nice Work!
Nice Work!
You just built a complete customer churn prediction system using IBM's Telco dataset and two competing machine learning models. You can now clean messy data, train classifiers, and explain exactly why a model that looks accurate might actually be failing.
You've learned how to:
- Loaded and cleaned 7,032 customer records from IBM's Telco Customer Churn dataset, discovering and fixing a hidden data quality issue in the TotalCharges column.
- Built a Logistic Regression baseline model and learned why raw accuracy is misleading for imbalanced datasets by examining the confusion matrix and classification report.
- Trained a Random Forest classifier that improved churn detection recall, extracted the top 10 features driving customer churn, and produced a feature importance visualization showing which customer attributes predict churn.
- Secret Mission: Generated a Churn Risk Report that ranks customers by their probability of leaving, categorized into High, Medium, and Low risk tiers.
Ready to quiz yourself?