Predict Customer Churn with scikit-learn

Train a classifier on real telecom data to predict which customers will churn.

Introduction

30 Second Summary

Every subscription service you use, from your phone plan to your streaming apps, has a team trying to figure out which customers are about to cancel. Predicting who will leave before they actually do is one of the most common real-world applications of machine learning.

In this project, you will build a customer churn predictor using scikit-learn and real telecom data from 7,043 customers. You will load messy real-world data, clean it, train two competing models, and discover why accuracy alone can be dangerously misleading.

What You'll Build

You will feed a customer profile into your trained model and instantly see a churn or no-churn prediction, then pull up the confusion matrix and feature importance chart to understand exactly why.

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

  • A working churn predictor that takes any customer profile and outputs a churn or no-churn prediction.
  • A confusion matrix showing exactly where your model gets predictions right and where it fails.
  • A ranked feature importance chart revealing which customer behaviors actually drive churn.
  • A head-to-head comparison of two algorithms (Logistic Regression vs Random Forest) on the same data.
  • Secret Mission: Cross-validation and hyperparameter tuning to squeeze more performance out of your model.

Are there any prerequisites?

You should have Python installed and Cursor installed on your machine. Familiarity with Python basics (variables, functions, and loops) is helpful, but no prior machine learning experience is needed.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the project goal before any hands-on work begins.

Set Up Your ML Environment

Machine learning projects need specific libraries to work. scikit-learn provides the algorithms, pandas handles data loading and manipulation, and matplotlib creates visualizations.

You also need a notebook environment where you can run code in small cells and see outputs inline. In this step, you'll create a clean Python environment, install the ML libraries, and set up a Jupyter notebook in Cursor.

In this step, get ready to:
  • Create a project folder and virtual environment.
  • Install scikit-learn, pandas, matplotlib, and ipykernel.
  • Create a Jupyter notebook in Cursor and verify imports.
Create your project folder and virtual environment

A virtual environment keeps your ML libraries isolated from the rest of your system. This prevents version conflicts between projects.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.11 or higher

Your Python version is compatible. Move on to creating the project folder below.

ⓧ I see a version lower than 3.11

This project requires Python 3.11 or higher because pandas 3.0 needs it.

Visit python.org/downloads and install Python 3.11 or newer.

  • Download and run the installer for your operating system.
  • On macOS, the system python3 may still point to the old version after installing. Use python3.11 or python3.12 directly, or use a version manager like pyenv to set the default.
  • After installing, close and reopen your terminal, then run python3 --version again to confirm 3.11 or higher appears.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir churn-predictor
cd churn-predictor
  • Create a virtual environment by running this command:
python3 -m venv venv
  • Activate the virtual environment by running this command:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.

On macOS, make sure you typed source before the path. Without it, the shell tries to execute the file directly and fails silently.

Help me activate my Python virtual environment.

Install scikit-learn, pandas, matplotlib, and ipykernel

These four packages give you everything you need for this project. ipykernel connects your virtual environment to Jupyter notebooks in Cursor.

  • Install all four packages by running this command:
pip install scikit-learn==1.9.0 pandas==3.0.4 matplotlib==3.11.0 ipykernel

What are these packages?

  • scikit-learn provides machine learning algorithms (the models you will train).
  • pandas loads and manipulates tabular data (your customer records).
  • matplotlib creates charts and visualizations (confusion matrices, bar charts).
  • ipykernel makes your virtual environment available as a Jupyter kernel inside Cursor.

The install takes about 30-60 seconds. You should see a series of download and install messages ending with a success confirmation.

Seeing an installation error?

Make sure (venv) appears in your prompt. If it does not, re-run the activate command from the previous substep.

If you see a permissions error, confirm you are installing inside the virtual environment, not system-wide. Never use sudo pip install inside a venv.

Help me fix my pip install error.

Create a Jupyter notebook in Cursor and verify imports

Cursor supports Jupyter notebooks natively. You will create a .ipynb file and connect it to your virtual environment's kernel.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the churn-predictor folder on your Desktop, then click Open.
  • Create a new notebook file by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
  • Type Create: New Jupyter Notebook and select it from the list.
  • Save the file immediately with Cmd+S (macOS) or Ctrl+S (Windows).
  • Name the file churn_predictor.ipynb and save it in the churn-predictor folder.
  • Click Select Kernel in the top right corner of the notebook.
  • Select Python Environments.
  • Choose the venv environment (it will show the path to your project's venv folder).

Why select a kernel?

The kernel tells the notebook which Python installation to use when running code. By selecting your virtual environment, the notebook uses the scikit-learn, pandas, and matplotlib versions you just installed.

Now you will write the imports cell that every cell in this project depends on.

  • In the first cell of your notebook, paste the following code:
# Cell 1: Imports
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

What does this code do?

  • pandas loads your customer data from CSV into a table structure called a DataFrame.
  • train_test_split divides your data into training and test sets so you can evaluate your model fairly.
  • LogisticRegression and RandomForestClassifier are the two ML algorithms you will compare.
  • The metrics imports give you tools to measure how well your models perform beyond simple accuracy.
  • matplotlib.pyplot creates the visual charts (confusion matrices and feature importance plots).
  • Run the cell by pressing Shift+Enter.

If the cell runs with no errors, all packages imported successfully.

Before you verify the version, what do you think sklearn.__version__ will print given the exact version you installed earlier?

  • Click the + Code button below your first cell to add a new cell.
  • Type and run this verification code:
import sklearn
print(sklearn.__version__)

You should see 1.9.0 printed below the cell. This confirms scikit-learn installed at the correct version.

  • Delete this verification cell now by clicking on it, then pressing the trash icon or right-clicking and selecting Delete Cell. You only needed it to confirm the install.

Seeing a ModuleNotFoundError?

This means the notebook is using a different Python than your virtual environment. Click Select Kernel in the top right and make sure you selected the venv environment from your churn-predictor folder.

If the kernel does not appear in the list, return to your terminal (with (venv) active) and run python -m ipykernel install --user --name=venv to register it manually.

Help me connect my Jupyter notebook to my virtual environment.

✔️ Awesome, I've got everything!

Great. Double check you have saved churn_predictor.ipynb with the imports cell.

ⓧ I'd like to double check the full code

Your notebook should have exactly one cell with this content:

# Cell 1: Imports
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

Your ML environment is ready. Next up, you will load real customer data and explore what 7,043 telecom records look like up close.

Load and Explore the Data

Your ML environment is ready and your notebook has all the libraries imported. Now it's time to meet your data.

Before you can predict anything, you need to see what you're working with. In this step, you'll load 7,043 real telecom customer records and discover a pattern in the data that will shape every modeling decision you make.

In this step, get ready to:
  • Load the telco churn dataset from a URL into a pandas DataFrame.
  • Inspect the data shape, types, and first few rows.
  • Check the churn rate to understand class imbalance.
Load the CSV from a URL

The dataset you'll use contains real customer data from a telecom company. Each row is one customer, and each column is a feature like their contract type, monthly charges, or whether they churned.

Why load from a URL instead of downloading a file?

The dataset is hosted publicly on GitHub, so pandas can read it directly with a URL. This skips the hassle of downloading files, creating accounts, or managing local paths.

  • Create a new cell below Cell 1 by clicking the + Code button that appears when you hover between cells.
  • Add the following code to load the dataset and preview it:
# Cell 2: Load data
url = "https://raw.githubusercontent.com/plotly/datasets/master/telco-customer-churn-by-IBM.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()

What does this code do?

  • url stores the direct link to the CSV file hosted on GitHub.
  • pd.read_csv(url) downloads the CSV and loads it into a DataFrame called df.
  • print(df.shape) shows how many rows and columns the dataset has.
  • df.head() displays the first 5 rows so you can see what the data looks like.
  • Run the cell by pressing Shift+Enter.
  • You should see (7043, 21) printed, followed by a table showing the first 5 rows of customer data with columns like customerID, gender, tenure, and Churn.

Seeing a connection error?

  • Make sure you have an active internet connection. The dataset is downloaded from GitHub each time you run this cell.
  • Check that the URL string is on a single line with no line breaks.
  • If you see NameError: name 'pd' is not defined, go back to Cell 1 and run it first to import pandas.

Help me debug a connection error when loading a CSV from a URL with pandas.

Explore data types and check class imbalance

You now know the dataset has 7,043 rows and 21 columns. The next question is: what types of data are in those columns, and how many customers actually churned?

Before you run the next cell, think about this: what percentage of customers do you think churned? Is it a 50/50 split, or something more lopsided?

  • Create a new cell below Cell 2 by pressing Shift+Enter (which ran Cell 2 and created a new cell) or by clicking + Code.
  • Add this code to inspect data types and the churn distribution:
# Cell 3: Explore the data
print(df.dtypes)
print(df['Churn'].value_counts())
print(f"\nChurn rate: {df['Churn'].value_counts(normalize=True)['Yes']:.1%}")

What does this code do?

  • df.dtypes shows the data type of each column. You'll see a mix of object (text) and int64/float64 (numbers).
  • df['Churn'].value_counts() counts how many customers have No (stayed) vs Yes (left).
  • The f-string calculates the churn rate as a percentage, giving you a single number to summarize the imbalance.
  • Run the cell by pressing Shift+Enter.

You should see that only about 27% of customers churned. The output shows roughly 5,174 customers with No and 1,869 with Yes.

What is class imbalance?

Class imbalance means one outcome is far more common than the other. Here, "No Churn" outnumbers "Churn" by nearly 3 to 1.

This matters because a model could predict "No Churn" for every single customer and still score about 73% accuracy. That sounds good on paper, but it would miss every churner. You'll see this exact trap play out in Step 4.

Seeing KeyError: 'Yes'?

  • Make sure you typed 'Churn' with a capital C. Column names are case-sensitive.
  • Confirm Cell 2 ran successfully before this cell. Cell 3 depends on the df variable created in Cell 2.

Help me fix a KeyError when checking churn value counts.

✔️ 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 full notebook code up to this point. Make sure your cells match this exactly:

# Cell 1: Imports
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

# Cell 2: Load data
url = "https://raw.githubusercontent.com/plotly/datasets/master/telco-customer-churn-by-IBM.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()

# Cell 3: Explore the data
print(df.dtypes)
print(df['Churn'].value_counts())
print(f"\nChurn rate: {df['Churn'].value_counts(normalize=True)['Yes']:.1%}")

You now know what you're working with: 7,043 customers, 21 features, and a lopsided churn rate that will trip up naive models. Next up, you'll clean this data and prepare it for machine learning.

Clean and Prepare the Data

You now know the shape of your data: 7,043 customers, 21 features, and a 27% churn rate. But that raw data is full of problems that will crash or confuse a machine learning model.

ML models operate on numbers. Your dataset has text columns like Contract and PaymentMethod, a TotalCharges column hiding blank strings, and a useless customerID. This step transforms the messy real-world data into clean numeric arrays your models can learn from.

In this step, get ready to:
  • Handle the TotalCharges data quality issue and drop customerID.
  • Encode categorical text features into numeric columns.
  • Split the data into training and test sets with stratification.
Handle the TotalCharges issue and drop customerID

The TotalCharges column looks numeric but contains blank strings for customers with zero tenure. pandas read these as text, which means your model will crash if you try to use them directly.

  • In your notebook, create a new cell below Cell 3 and add the following code:
# Cell 4: Clean TotalCharges and drop customerID
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'] = df['TotalCharges'].fillna(df['TotalCharges'].median())
df = df.drop('customerID', axis=1)

What does this code do?

  • pd.to_numeric(errors='coerce') forces the column to numeric, turning any non-numeric values (the blank strings) into NaN.
  • fillna(df['TotalCharges'].median()) replaces those NaN gaps with the median value, which is more robust to outliers than the mean.
  • df.drop('customerID', axis=1) removes the ID column because a random identifier has no predictive value.
  • Run the cell.
  • Verify the fix by typing this in a new cell below:
df['TotalCharges'].dtype
  • Run the cell. You should see dtype('float64') printed, confirming the column is now numeric.
  • Delete this verification cell once you have confirmed the output.

Seeing an error on pd.to_numeric?

  • Make sure you ran Cell 2 first so the DataFrame exists.
  • Check that the column name is spelled exactly as TotalCharges with capital T and C.

Help me debug my TotalCharges cleaning code

Encode target and categorical features

Your Churn column contains "Yes" and "No" as text. The model needs 1 and 0. The same problem applies to every other text column.

One-hot encoding solves this by creating a separate 0/1 column for each category value. A Contract column with values "Month-to-month", "One year", and "Two year" becomes two new columns (with drop_first=True removing one to avoid redundancy).

  • Create a new cell below Cell 4 and add this code:
# Cell 5: Encode target and categorical features
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})

categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=True)

What does this code do?

  • df['Churn'].map({'Yes': 1, 'No': 0}) converts the target column to binary numbers.
  • select_dtypes(include=['object']) automatically finds all remaining text columns.
  • pd.get_dummies(drop_first=True) creates one-hot encoded columns for each category, dropping the first to avoid multicollinearity.
  • Run the cell.
  • Verify the encoding by typing this in a new cell below:
df_encoded.shape
  • Run the cell. You should see (7043, 31) printed. The row count is the same, but columns jumped from 21 to 31 because each categorical value became its own numeric column.
  • Delete this verification cell once confirmed.

Seeing a different column count?

  • Make sure Cell 4 ran successfully first. If it didn't drop customerID, you'll have an extra column.
  • Make sure you mapped Churn to 0/1 before calling get_dummies. If Churn is still text, it gets one-hot encoded too.

Help me figure out why my encoded DataFrame has a different shape

Split into training and test sets

You need to separate features (the inputs) from the target (what you are predicting). Then split the data so your model trains on 80% and gets tested on the remaining 20% it has never seen.

Stratification ensures both the training and test sets preserve the same 27% churn rate. Without it, you might accidentally get a test set with very few churners, giving you misleading results.

  • Create a new cell below Cell 5 and add this code:
# Cell 6: Split into features and target, then train/test
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}")
print(f"Test set: {X_test.shape}")

What does this code do?

  • X = df_encoded.drop('Churn', axis=1) creates the feature matrix by removing the target column.
  • y = df_encoded['Churn'] isolates the target variable (what the model will predict).
  • train_test_split(test_size=0.2, stratify=y) splits the data 80/20 while keeping the churn ratio equal in both sets.
  • random_state=42 makes the split reproducible so you get the same results every time.

Before you run this cell, predict: how many rows do you expect in the training set if the split is 80/20 on 7,043 total rows?

  • Run the cell.

You should see:

Training set: (5634, 30)
Test set: (1409, 30)

5,634 rows for training (80%) and 1,409 for testing (20%), each with 30 feature columns.

  • Confirm all features are numeric by typing this in a new cell:
X_train.dtypes
  • Run the cell. Every row should show a numeric dtype like float64, int64, or bool. No object types should remain.
  • Delete this verification cell once confirmed.

Seeing object dtypes in X_train?

  • Go back and make sure Cell 5 ran without errors. The get_dummies call must complete before you split the data.
  • If you re-ran Cell 2 (reloading the data), you need to re-run Cells 4 and 5 in order before running Cell 6.

Help me figure out why my training data still has non-numeric columns

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook file.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm each cell is present in this order:

  • Cell 1: Imports (pandas, scikit-learn, matplotlib).
  • Cell 2: Load data from URL and print shape.
  • Cell 3: Explore dtypes, value counts, and churn rate.
  • Cell 4: Clean TotalCharges (to_numeric, fillna) and drop customerID.
  • Cell 5: Map Churn to 0/1, find categorical columns, run get_dummies.
  • Cell 6: Define X and y, run train_test_split with stratify, print shapes.

If any cell is missing or out of order, add or reorder it to match the list above.

Your data is clean, encoded, and split. Every feature is numeric and ready for a model to learn from. Next up, you will train your first classifier and discover why accuracy alone can be dangerously misleading.

Train Your First Model and Hit the Accuracy Trap

Your data is clean, numeric, and split into training and test sets. Now it's time to train your first model and see if it can actually predict which customers will churn.

You'll start with Logistic Regression, the simplest classification algorithm. It trains fast and gives you a quick baseline. The interesting question isn't whether it trains successfully. It's whether 80% accuracy actually means the model is useful.

In this step, get ready to:
  • Train a Logistic Regression classifier on your training data.
  • Check the accuracy score and classification report.
  • Visualize the confusion matrix to see where the model gets it wrong.
Train a Logistic Regression and run predictions

Logistic Regression learns a boundary between two classes (churn vs. no churn) based on patterns in the training data. It's fast, interpretable, and a standard first model for any classification problem.

  • Create a new cell below Cell 6 in your notebook and add this code:
# Cell 7: Train Logistic Regression baseline
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
lr_pred = lr_model.predict(X_test)

print(f"Logistic Regression Accuracy: {accuracy_score(y_test, lr_pred):.3f}")
print("\nClassification Report:")
print(classification_report(y_test, lr_pred, target_names=['No Churn', 'Churn']))

What does this code do?

  • LogisticRegression(max_iter=1000, random_state=42) creates the model. max_iter=1000 gives the algorithm enough iterations to converge. random_state=42 makes results reproducible.
  • .fit(X_train, y_train) trains the model by finding patterns in the training data that separate churners from non-churners.
  • .predict(X_test) runs the trained model on data it has never seen before and outputs churn/no-churn predictions.
  • classification_report breaks down performance for each class (No Churn and Churn) separately, showing precision, recall, and f1-score.

Before you run this cell, think about what 80% accuracy means. If 73% of customers in the dataset do NOT churn, do you think 80% accuracy means the model is actually catching churners? Or could it be getting most of its score just by predicting "No Churn" for everyone?

  • Run the cell.

You'll see the accuracy printed as approximately 0.800. That sounds great. Now look at the classification report below it.

Look at the recall row for the Churn class. It's around 50-55%. That means the model is missing roughly half of the actual churners. It predicts "No Churn" for them, and it's wrong.

What is the accuracy trap?

When your dataset is imbalanced (73% of customers don't churn), a model can score 73% accuracy by predicting "No Churn" for every single customer. Your Logistic Regression beats that naive guess only slightly.

The business cares about catching churners before they leave. A model that misses half of them is not useful, no matter what the accuracy number says. This is why recall (the percentage of actual churners the model catches) matters more than raw accuracy for imbalanced problems.

Seeing a convergence warning?

If you see a warning about the model not converging, double check that you set max_iter=1000 in your LogisticRegression call. The default is 100, which is often not enough for datasets with many features.

Help me debug my Logistic Regression convergence warning.

Visualize the confusion matrix

Numbers in a classification report can be abstract. A confusion matrix shows you the exact counts: how many churners the model caught, and how many it missed.

  • Create a new cell below Cell 7 and add this code:
# Cell 8: Show confusion matrix for Logistic Regression
cm = confusion_matrix(y_test, lr_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['No Churn', 'Churn'])
disp.plot(cmap='Blues')
plt.title('Logistic Regression - Confusion Matrix')
plt.show()

How to read a confusion matrix

The confusion matrix is a 2x2 grid. Rows represent the actual labels (what really happened). Columns represent what the model predicted.

  • Top-left: customers who didn't churn, correctly predicted as No Churn (true negatives).
  • Top-right: customers who didn't churn, incorrectly predicted as Churn (false positives).
  • Bottom-left: customers who DID churn, but the model missed them and said No Churn (false negatives). This is the problem cell.
  • Bottom-right: customers who DID churn, correctly predicted as Churn (true positives).
  • Run the cell.

You'll see a blue heatmap. Look at the bottom-left cell. That large number is the count of actual churners your model completely missed. It predicted they would stay, but they left.

This is the accuracy trap in action. The model scores 80% overall because it's excellent at predicting the majority class (No Churn). But it fails at the one thing you actually need: catching customers before they leave.

Not seeing the plot?

Make sure you have import matplotlib.pyplot as plt in your Cell 1 imports. If the plot appears in a separate window instead of inline, close it and the notebook will display it below the cell on the next run.

Help me get my confusion matrix plot to display in Cursor.

✔️ Awesome, I've got everything!

Great. Make sure both cells are saved in your notebook.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells appear in order after your previous cells:

  • Cell 7 (Train Logistic Regression baseline): creates lr_model, fits it, predicts with lr_pred, and prints accuracy plus classification report.
  • Cell 8 (Show confusion matrix): creates ConfusionMatrixDisplay and calls plt.show().

Your baseline model exposes the accuracy trap: 80% accuracy masks the fact that half of actual churners slip through undetected. Next up, you'll train a Random Forest with a technique that forces it to pay more attention to the minority churn class.

Beat Your Baseline with Random Forest

Your Logistic Regression model reported ~80% accuracy, but the confusion matrix told the real story. It misses roughly half of the customers who actually churn.

Random Forest handles imbalanced data better because it captures non-linear patterns that a straight line cannot. In this step, you will train a Random Forest with a special class_weight='balanced' setting that forces the model to pay more attention to the minority churn class. Then you will see which customer behaviors actually drive churn.

In this step, get ready to:
  • Train a RandomForestClassifier with balanced class weights.
  • Compare its classification report and confusion matrix against Logistic Regression.
  • Extract and display feature importances to see what drives churn.
Train a RandomForestClassifier with balanced class weights

The key to fixing the recall problem is the class_weight='balanced' parameter. This automatically adjusts weights inversely proportional to class frequencies, penalizing the model more for missing minority class examples.

Why class_weight='balanced' instead of resampling?

When your data has way more "No Churn" than "Churn" examples, the model learns to just predict "No" for everyone because that is the easy path to high accuracy. The class_weight='balanced' setting tells the model to treat each churn example as more important, without needing to duplicate or generate synthetic rows.

Before you run this next cell, think: do you expect the churn recall to improve beyond the ~50-55% you saw from Logistic Regression? By how much?

  • Create a new cell below Cell 8 in your notebook and paste the following code:
# Cell 9: Train Random Forest with balanced class weights
rf_model = RandomForestClassifier(
    n_estimators=100,
    class_weight='balanced',
    random_state=42
)
rf_model.fit(X_train, y_train)
rf_pred = rf_model.predict(X_test)

print(f"Random Forest Accuracy: {accuracy_score(y_test, rf_pred):.3f}")
print("\nClassification Report:")
print(classification_report(y_test, rf_pred, target_names=['No Churn', 'Churn']))

What does this code do?

RandomForestClassifier builds 100 decision trees (n_estimators=100) and combines their votes. Each tree sees a random subset of the data and features, which reduces overfitting.

class_weight='balanced' adjusts weights using the formula n_samples / (n_classes * np.bincount(y)). This makes the model penalize itself more heavily for missing a churn customer than for missing a non-churn customer.

  • Run the cell.

Look at the classification report output. The accuracy may be slightly lower than Logistic Regression (around 0.75-0.78), but the recall for the Churn class should jump to ~70% or higher. That means the model now catches significantly more of the customers who actually leave.

Seeing an error about RandomForestClassifier?

If you see a NameError, make sure you ran Cell 1 (imports) earlier in this session. The RandomForestClassifier import was already included there.

If you see a NameError for X_train or y_train, re-run Cell 6 to recreate the train/test split.

Help me debug my Random Forest training error

Compare confusion matrices side by side

The classification report tells you the numbers. A confusion matrix lets you see the shape of the improvement at a glance.

  • Create a new cell below Cell 9 and paste the following code:
# Cell 10: Compare confusion matrices
cm_rf = confusion_matrix(y_test, rf_pred)
disp_rf = ConfusionMatrixDisplay(confusion_matrix=cm_rf, display_labels=['No Churn', 'Churn'])
disp_rf.plot(cmap='Blues')
plt.title('Random Forest (Balanced) - Confusion Matrix')
plt.show()

What does this code do?

This generates a confusion matrix for the Random Forest model so you can visually compare it against the Logistic Regression matrix from Cell 8. The bottom-right cell (true churners predicted as churners) should be noticeably larger now.

  • Run the cell.
  • Compare this confusion matrix to the one from Cell 8. The bottom-right quadrant (correctly predicted churners) should be higher, and the bottom-left quadrant (missed churners) should be lower.

How to read the improvement

In the Logistic Regression matrix, the model missed roughly half of all churners (bottom-left was high). In the Random Forest matrix, that number drops significantly. The tradeoff is a few more false positives (top-right), but catching more actual churners is usually worth it in a real business context.

Confusion matrix not displaying?

Make sure you ran Cell 9 first so rf_pred exists. If the plot appears tiny or cut off, try adding plt.tight_layout() before plt.show().

Help me fix my confusion matrix display

Extract and display feature importances

You know the model works better now. The next question is: why? Random Forest can tell you exactly which features matter most for predicting churn.

  • Create a new cell below Cell 10 and paste the following code:
# Cell 11: Feature importance
importances = rf_model.feature_importances_
feature_importance_df = pd.DataFrame({
    'feature': X.columns,
    'importance': importances
}).sort_values('importance', ascending=False).head(10)

plt.figure(figsize=(10, 6))
plt.barh(feature_importance_df['feature'], feature_importance_df['importance'])
plt.xlabel('Importance')
plt.title('Top 10 Features Driving Customer Churn')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

print("\nTop 10 features driving churn:")
print(feature_importance_df.to_string(index=False))

What does this code do?

feature_importances_ is an attribute that every trained Random Forest exposes. It returns an array of impurity-based importance scores, one per feature. Higher values mean the model relied on that feature more heavily when making split decisions.

The code builds a DataFrame of feature names and their scores, sorts by importance, takes the top 10, and plots them as a horizontal bar chart. The printed table gives you the exact numbers.

Before you run this, what do you think the top features driving churn will be? Think about what would make you leave a telecom provider.

  • Run the cell.

You should see a horizontal bar chart showing the top 10 features. Tenure, MonthlyCharges, and Contract type should appear near the top. The printed table below the chart confirms the exact importance scores.

What do these top features mean for the business?

Tenure (how long the customer has been with the company) being the top predictor makes intuitive sense. New customers churn more because they have less investment in the service. High monthly charges and month-to-month contracts also drive churn because customers with less commitment and higher bills have less reason to stay.

This is the kind of insight a business can act on: target new customers on month-to-month contracts with retention offers before they leave.

Bar chart looks empty or shows wrong features?

Make sure you ran Cell 9 so rf_model is trained. If you see a NameError for X, re-run Cell 6 which defines X and y.

Help me fix my feature importance chart

✔️ Awesome, I've got everything!

Great. Double check you have Cells 9, 10, and 11 all saved in your notebook.

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells are present in order:

  • Cell 1: Imports (pandas, sklearn, matplotlib).
  • Cell 2: Load data from URL.
  • Cell 3: Explore the data.
  • Cell 4: Clean TotalCharges and drop customerID.
  • Cell 5: Encode target and categorical features.
  • Cell 6: Split into features/target and train/test.
  • Cell 7: Train Logistic Regression baseline.
  • Cell 8: Show confusion matrix for Logistic Regression.
  • Cell 9: Train Random Forest with balanced class weights.
  • Cell 10: Compare confusion matrices.
  • Cell 11: Feature importance.

You now have a model that actually catches churners and you know exactly which features drive those predictions. In the Secret Mission, you will use cross-validation and hyperparameter tuning to squeeze even more performance out of this model.

Secret mission

Cross-Validation and Hyperparameter Tuning

Your Random Forest model used default hyperparameters. Can you systematically search for the best configuration and prove it with cross-validation? This extension teaches you how to squeeze more performance out of any model.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • Project folder on your Desktop (contains churn_predictor.ipynb and your virtual environment).
  • Python virtual environment with scikit-learn, pandas, matplotlib, and ipykernel installed.

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment further with the churn predictor.

  • Your project folder stays on your Desktop with the notebook and virtual environment intact.
  • You can reopen churn_predictor.ipynb in Cursor at any time and re-run cells.
  • The virtual environment uses approximately 500-800 MB of disk space.

Pause - I'll come back to this later

Shut down any running Jupyter kernel to free up memory. Your files stay on disk so you can pick up where you left off.

  • In Cursor, click Restart on the kernel toolbar at the top of the notebook to stop the running kernel.
  • Close the notebook tab in Cursor.
  • When you come back, reopen the notebook and select your virtual environment kernel again.

Delete - I don't want to use this again

Remove all project files including the virtual environment and notebook. This frees up all disk space used by the project.

  • Close the notebook and any open terminals in Cursor.
  • Delete the project folder by running the appropriate command for your operating system.

On macOS or Linux, open a terminal and run:

rm -rf ~/Desktop/churn-predictor

On Windows, open PowerShell and run:

Remove-Item -Recurse -Force ~\Desktop\churn-predictor

This removes the notebook, virtual environment, and all installed packages in one step.

That's a Wrap!

That's a Wrap!

Nice work! You just built a customer churn predictor that takes raw telecom data and turns it into actionable predictions using scikit-learn.

You've learned how to:

  • Built a complete ML pipeline from raw CSV to trained model, handling real-world data issues like blank values, categorical encoding, and class imbalance along the way.
  • Discovered that accuracy is misleading with imbalanced data, and used confusion matrices and recall to tell the real story of model performance.
  • Compared Logistic Regression and Random Forest head-to-head, then used feature importances to reveal which customer behaviors actually drive churn.
  • Secret Mission: Used cross-validation and hyperparameter tuning with GridSearchCV to find the optimal model configuration.

Ready to quiz yourself?