Predict Customer Churn with scikit-learn

Train a Random Forest classifier to identify at-risk telecom customers.

Introduction

30 Second Summary

Every subscription service you use tracks whether you're about to cancel. Streaming platforms, phone carriers, and SaaS companies all use machine learning to predict which customers are at risk of leaving so they can intervene before it's too late.

In this project, you will build a customer churn predictor using scikit-learn that trains on real telecom customer data and predicts which customers are likely to cancel their service.

What You'll Build

Feed in a customer's demographics, service history, and billing info, and your trained model tells you whether they're about to churn. A confusion matrix and classification report prove exactly how well it catches at-risk customers.

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

  • A trained Random Forest model you can feed any customer record and get a churn or no-churn prediction back instantly.
  • A side-by-side comparison of two models showing why accuracy alone is misleading for imbalanced datasets, with precision, recall, and F1-score revealing the full picture.
  • Bar charts and a confusion matrix heatmap showing exactly how many churners your model catches versus misses, and which features drive churn the most.
  • Secret Mission: Tune your Random Forest's hyperparameters with GridSearchCV to squeeze out even better performance.

Are there any prerequisites?

This project assumes you have Python and VS Code already installed on your computer. You should also be comfortable with Python basics like loops, functions, and imports.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your ML Environment

Every ML project starts with an isolated environment. Installing libraries into your system Python leads to version conflicts between projects.

In this step, you will create a dedicated project folder, set up a virtual environment, install your ML libraries, and prepare a Jupyter notebook to work in.

In this step, get ready to:
  • Create a project folder with an isolated Python environment.
  • Install scikit-learn, pandas, matplotlib, and seaborn.
  • Set up a Jupyter notebook and download the customer churn dataset.
Create a project folder and virtual environment

A virtual environment is a self-contained copy of Python that keeps this project's libraries separate from everything else on your machine.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • 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

Now create and activate the virtual environment. The commands differ slightly between macOS and Windows.

macOS

  • Create and activate your virtual environment by running these commands:
python3 -m venv venv
source venv/bin/activate

Windows

  • Create and activate your virtual environment by running these commands:
python -m venv venv
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms you are working inside the virtual environment.

Don't see (venv) in your prompt?

Make sure you ran the activation command (not just the creation command). On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned first, then try the activation command again.

Help me fix my venv activation

  • Install the four ML libraries you need by running this command:
pip install scikit-learn pandas matplotlib seaborn

What do these libraries do?

  • scikit-learn provides ML algorithms (classification, regression, clustering) and evaluation tools.
  • pandas loads and manipulates tabular data (like spreadsheets).
  • matplotlib creates charts and plots.
  • seaborn adds statistical visualization on top of matplotlib.

The install takes about a minute. You will see progress bars as each package downloads.

  • Verify the installation succeeded by running this command:
python -c "import sklearn; print(sklearn.__version__)"

You should see 1.9.0 (or a similar version number) printed. This confirms scikit-learn installed correctly.

Seeing ModuleNotFoundError?

Make sure your terminal still shows (venv) in the prompt. If it disappeared, re-run the activation command from the platform tab above. Then run the pip install command again.

Help me fix this import error

Set up Jupyter in VS Code

The Jupyter extension lets you run Python code in notebook cells directly inside VS Code. Each cell runs independently, which is perfect for exploring data step by step.

  • Open VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • 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 Extensions panel.
  • Click Install on the extension named Jupyter by Microsoft.

The extension installs in a few seconds. You will see the Install button change to Disable once it finishes.

  • Open your project folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to Desktop, select the churn-predictor folder, 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 dropdown.
  • Save the notebook immediately by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Name the file churn_predictor.ipynb and save it in the churn-predictor folder.
  • Select your virtual environment as the notebook kernel. Click the kernel picker button in the top right corner of the notebook (it may say Select Kernel or show a Python version).
  • Click Python Environments.
  • Select the venv interpreter from the list (it will show the path to your project's venv folder).

Why select the venv kernel?

The kernel tells the notebook which Python installation to use when running cells. Selecting your venv ensures the notebook uses the libraries you just installed, not a different Python on your system.

Don't see venv in the kernel list?

Close and reopen VS Code after installing the Jupyter extension. Make sure you opened the churn-predictor folder (not a parent folder) in VS Code. The venv folder must be directly inside the open workspace.

Help me find my venv kernel

Download the dataset

You will use the IBM Telco Customer Churn dataset, which contains 7,043 real customer records with 21 features including demographics, services, and billing information.

  • Return to your terminal (it should still be in the churn-predictor folder with (venv) active).
  • Download the dataset by running this command:
curl -o Telco-Customer-Churn.csv https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv
  • Confirm the file downloaded by running:
ls

You should see Telco-Customer-Churn.csv listed alongside your venv folder and churn_predictor.ipynb.

curl command not found?

On older Windows systems, curl may not be available. Open the URL directly in your browser instead:

https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv

Save the page as Telco-Customer-Churn.csv inside your churn-predictor folder on your Desktop.

Help me download the dataset

Now write your first notebook cells to load the data and confirm everything works together.

  • Switch back to churn_predictor.ipynb in VS Code.
  • In the first cell, paste the following import statements:
# Cell 1: Imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, ConfusionMatrixDisplay

Why import everything now?

In Jupyter notebooks, it is standard practice to put all imports in the first cell. This way you know immediately if any library is missing, and every other cell can use these imports without repeating them.

  • Run Cell 1 by clicking the play button to the left of the cell (or pressing Shift+Enter).

The cell should run without any errors. If it completes silently (no output), that means all libraries imported successfully.

  • Click the + Code button below Cell 1 to add a new cell.
  • Paste the following code into Cell 2:
# Cell 2: Load Data
df = pd.read_csv('Telco-Customer-Churn.csv')
print(df.shape)
df.head()

What does this code do?

  • pd.read_csv() reads the CSV file into a pandas DataFrame (a table of rows and columns).
  • df.shape prints the number of rows and columns as a tuple.
  • df.head() displays the first five rows so you can preview the data.

Before you run this cell, what output do you expect to see for the shape?

  • Run Cell 2 by pressing Shift+Enter.

You should see (7043, 21) printed, followed by a table showing the first five rows of customer data. This confirms your dataset has 7,043 customers and 21 columns.

Seeing FileNotFoundError?

This means the notebook cannot find Telco-Customer-Churn.csv. Make sure the CSV file is saved directly inside the churn-predictor folder (not inside a subfolder). Also confirm that VS Code opened the churn-predictor folder as the workspace.

Help me fix this file path error

✔️ Awesome, I've got everything!

Great. Double check you have saved churn_predictor.ipynb with both cells present and Cell 2 showing the (7043, 21) output.

ⓧ I'd like to double check the full code

Your notebook should have exactly two cells at this point. Here is the complete code:

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

# Cell 2: Load Data
df = pd.read_csv('Telco-Customer-Churn.csv')
print(df.shape)
df.head()

Your ML environment is ready and your data is loaded. Next up, you will explore this dataset visually to spot the patterns that predict customer churn.

Explore the Data and Spot Churn Patterns

Your environment is set up and the dataset is loaded. You can see the shape (7043 rows, 21 columns) in your notebook output.

Before building any model, you need to understand what the data actually looks like. Which customers churn? Are there obvious patterns in contract type or pricing? This step gives you your first visible insight into the problem.

In this step, get ready to:
  • Inspect the dataset structure and class distribution.
  • Visualize churn patterns with bar charts and a boxplot.
  • Identify which features are likely to predict churn.
Inspect the dataset structure

You know the dataset has 7043 rows and 21 columns. Now you need to understand what those columns actually contain and how many customers churned.

  • Add a new cell below Cell 2 in your notebook and paste the following code:
# Cell 3: Inspect Data
df.info()
print(df['Churn'].value_counts())
  • Run the cell.

What does this output tell you?

The df.info() output shows each column's data type and non-null count. Notice that TotalCharges appears as object (text) instead of a number. That means it has some non-numeric values you will need to fix later.

The value_counts() output reveals the class imbalance: roughly 5174 customers did not churn (No) versus 1869 who did (Yes). That is about 73% No and 27% Yes.

You should see a table of column types and a count showing approximately 5174 No and 1869 Yes. This imbalance will matter when you evaluate your model later.

Not seeing any output?

Make sure you clicked the Run button (or pressed Shift+Enter) on the cell. Check that the kernel in the top-right corner says venv and not a different Python interpreter.

help me debug why my Jupyter cell is not producing output

Visualize churn patterns

Numbers alone do not tell the full story. A quick chart makes the imbalance and feature relationships instantly visible.

  • Add a new cell below Cell 3 and paste this code to visualize the churn distribution:
# Cell 4: Visualize Churn Distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(x='Churn', data=df, ax=ax)
ax.set_title('Churn Distribution')
plt.show()
  • Run the cell.

What does this code do?

This creates a bar chart using seaborn's countplot function. It counts how many customers fall into each category (No vs Yes) and draws a bar for each.

The figsize=(6, 4) controls the chart dimensions. The ax object lets you add a title and customise the plot.

You should see a bar chart with two bars. The "No" bar is roughly three times taller than the "Yes" bar, confirming the 73/27 split.

  • Add another new cell and paste this code to compare churn rates by contract type:
# Cell 5: Churn by Contract Type
fig, ax = plt.subplots(figsize=(8, 5))
churn_by_contract = df.groupby(['Contract', 'Churn']).size().unstack(fill_value=0)
churn_by_contract.plot(kind='bar', ax=ax)
ax.set_title('Churn by Contract Type')
ax.set_ylabel('Count')
plt.xticks(rotation=0)
plt.show()
  • Run the cell.

What does this code do?

This groups the data by both Contract type and Churn status, counts the customers in each group, then unstacks the result into a format suitable for a grouped bar chart.

The three contract types (Month-to-month, One year, Two year) each get a pair of bars showing how many customers stayed vs left.

You should see that month-to-month customers have a dramatically higher churn bar compared to one-year and two-year contract customers. This is your first strong signal.

  • Add one more cell and paste this code to see how monthly charges relate to churn:
# Cell 6: MonthlyCharges vs Churn
fig, ax = plt.subplots(figsize=(8, 5))
sns.boxplot(x='Churn', y='MonthlyCharges', data=df, ax=ax)
ax.set_title('Monthly Charges by Churn Status')
plt.show()
  • Run the cell.

What does this code do?

A boxplot shows the distribution of a numeric variable (MonthlyCharges) split by a category (Churn). The box shows the middle 50% of values. The line inside is the median.

Comparing the two boxes side by side reveals whether churners tend to pay more or less than customers who stay.

You should see that the "Yes" (churn) box sits higher on the y-axis than the "No" box. Customers who churn tend to have higher monthly charges.

Charts not rendering in your notebook?

Confirm that Cell 1 imported matplotlib.pyplot and seaborn without errors. If you see a blank output, try restarting the kernel (click the restart icon in the toolbar) and running all cells from the top.

help me fix charts not rendering in my Jupyter notebook in VS Code

Summarise the key insights

You now have three concrete observations from your exploration that will guide the model.

What patterns did you find?

  • The dataset is imbalanced: about 27% of customers churned. A model that always predicts "No Churn" would be 73% accurate without learning anything useful.
  • Month-to-month contract customers churn far more than those on one-year or two-year contracts. Contract type will likely be a strong predictor.
  • Higher MonthlyCharges correlates with churn. Price-sensitive customers leave more often.

✔️ Awesome, I've got everything!

Great. Confirm you have saved your notebook with all four new cells (Cell 3 through Cell 6).

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells exist in order:

  • Cell 1: Imports (pandas, matplotlib, seaborn, sklearn modules).
  • Cell 2: Load data with pd.read_csv and df.head().
  • Cell 3: df.info() and df['Churn'].value_counts().
  • Cell 4: sns.countplot showing churn distribution.
  • Cell 5: Grouped bar chart of churn by Contract type.
  • Cell 6: sns.boxplot of MonthlyCharges by Churn.

You have explored the data and found clear patterns. Next up, you will preprocess these messy columns into clean numbers that a machine learning model can actually use.

Preprocess the Data for Machine Learning

In the last step, you explored the data and spotted clear churn patterns. Month-to-month contracts and higher charges both correlate with customers leaving.

Now you need to turn those insights into something a machine learning model can actually use. ML models only understand numbers. Your dataset still has text columns like Contract, PaymentMethod, and a TotalCharges column hiding blank strings instead of proper numbers.

In this step, get ready to:
  • Clean messy columns and convert the target to numeric.
  • Encode all categorical text features into numbers.
  • Split the data into training and test sets.
Clean the data

The raw dataset has a few problems that will crash a model. The TotalCharges column contains blank strings for 11 customers with zero tenure. The customerID column is just an identifier with no predictive value. And the Churn column still holds text ("Yes"/"No") instead of the 1/0 the model needs.

  • Create a new cell below Cell 6 in your notebook and add this code:
# Cell 7: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'] = df['TotalCharges'].fillna(0)
df = df.drop('customerID', axis=1)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})

What does this code do?

  • pd.to_numeric(..., errors='coerce') forces the column to numbers. Any blank strings become NaN instead of crashing.
  • .fillna(0) replaces those NaN values with 0, since these customers had zero tenure and zero charges.
  • .drop('customerID', axis=1) removes the ID column because a random identifier cannot help the model predict anything.
  • .map({'Yes': 1, 'No': 0}) converts the target column to numeric so the model can learn from it.
  • Run Cell 7.
  • Verify the fix worked by adding a quick check in a temporary cell: df['TotalCharges'].dtype. You should see float64 confirming the column is now numeric.

Seeing a KeyError for TotalCharges?

Make sure you ran Cells 1-6 in order before running Cell 7. If you restarted your kernel, you need to re-run from the top.

help me debug a KeyError when cleaning my TotalCharges column

Encode categorical features and split the data

Your DataFrame still has text columns like Contract, InternetService, and PaymentMethod. One-hot encoding converts each category into its own binary column (1 if present, 0 if not).

  • Create a new cell below Cell 7 and add this code to encode all categorical columns:
# Cell 8: Encode Categorical Features
df_encoded = pd.get_dummies(df, drop_first=True)

Why drop_first=True?

If a column has two categories (like gender: Male/Female), you only need one binary column. If it is 0, that already tells you it is the other category. Dropping one prevents multicollinearity, which can confuse some models.

  • Run Cell 8.
  • Verify by checking the new shape in a temporary cell: df_encoded.shape. You should see approximately (7043, 20), confirming the text columns have been expanded into numeric ones.

Now you need to separate the features (everything the model learns from) from the target (what it predicts). Then split both into a training set and a test set so you can evaluate how well the model performs on data it has never seen.

  • Create a new cell below Cell 8 and add this code:
# Cell 9: Train/Test Split
X = df_encoded.drop('Churn', axis=1)
y = df_encoded['Churn']
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
print(X_train.shape, X_test.shape)

What does this code do?

  • X holds all the feature columns (everything except Churn). y holds just the Churn column (the answer the model tries to predict).
  • test_size=0.2 reserves 20% of the data for testing. The model trains on the other 80%.
  • random_state=42 locks in a specific split so you get reproducible results every time.
  • stratify=y ensures both the training and test sets keep the same 73/27 churn ratio as the full dataset. Without this, the test set might end up with too many or too few churners by random chance.

Before you run Cell 9, what shape do you expect the training set to have? Remember the full dataset has 7043 rows and you are reserving 20% for testing.

  • Run Cell 9.

You should see (5634, 19) (1409, 19) printed. That confirms 5634 rows for training, 1409 rows for testing, and 19 feature columns. Your data is now a clean numeric matrix with no NaN values, ready for a model.

Seeing different numbers of columns?

If you see a number other than 19 columns, make sure you ran Cell 7 before Cell 8. The get_dummies call encodes whatever is in df at that moment. If you skipped the cleaning step, extra columns like customerID would still be present.

help me debug my train/test split shape

✔️ Awesome, I've got everything!

Great. Double check you have saved your notebook.

ⓧ I'd like to double check the full code

Here are all three cells you added in this step. Scroll up and confirm each one matches.

# Cell 7: Data Cleaning
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce')
df['TotalCharges'] = df['TotalCharges'].fillna(0)
df = df.drop('customerID', axis=1)
df['Churn'] = df['Churn'].map({'Yes': 1, 'No': 0})
# Cell 8: Encode Categorical Features
df_encoded = pd.get_dummies(df, drop_first=True)
# Cell 9: Train/Test Split
X = df_encoded.drop('Churn', axis=1)
y = df_encoded['Churn']
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)
print(X_train.shape, X_test.shape)

Your data is now fully preprocessed and split. Next up, you will train your first model on this data and discover why a "good" accuracy score can be deeply misleading.

Train a Baseline Model and Discover Its Weakness

In the previous step, you cleaned the data, encoded categorical features, and split everything into training and test sets. Your data is ready for a model to learn from.

The natural first choice is Logistic Regression. It trains fast, it is easy to interpret, and it gives you a baseline to beat. But there is a trap waiting. When your dataset is imbalanced (27% churn vs 73% no churn), a model can score high accuracy by mostly ignoring the minority class.

In this step, get ready to:
  • Train a Logistic Regression baseline model on the training data.
  • Evaluate it with accuracy and see a misleadingly high score.
  • Expose the real weakness using precision, recall, and a confusion matrix.
Train Logistic Regression

Logistic Regression is a linear classifier that draws a boundary between classes. It is fast to train and works well as a first model to establish a performance floor.

  • Create a new cell in your notebook (Cell 10) and add the following code to train the baseline model:
# Cell 10: Train Logistic Regression Baseline
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
y_pred_lr = lr_model.predict(X_test)

What does this code do?

  • LogisticRegression(max_iter=1000, random_state=42) creates the model. max_iter=1000 gives the solver enough iterations to converge on this dataset.
  • .fit(X_train, y_train) teaches the model to find patterns in the training data that separate churners from non-churners.
  • .predict(X_test) uses the trained model to classify each test customer as churn (1) or no churn (0). These predictions are stored in y_pred_lr for evaluation.
  • Run the cell.

The cell completes without printed output. The model is now trained and the predictions are stored in y_pred_lr.

Evaluate with accuracy (the misleading metric)

Accuracy measures how many predictions the model got right overall. It is the first metric most people check. Let's see what it says about your baseline.

  • Create a new cell (Cell 11) and print the accuracy score:
# Cell 11: Evaluate Baseline - Accuracy
print(f"Accuracy: {accuracy_score(y_test, y_pred_lr):.4f}")
  • Run the cell.

You should see output around Accuracy: 0.8005. That is roughly 80% correct predictions. Sounds good, right?

Why does 80% accuracy look decent?

The dataset is 73% non-churners. A model that simply predicts "No Churn" for every single customer would still get 73% accuracy. Your Logistic Regression is doing slightly better than that. But the question that matters in business is: does it catch the customers who are actually about to leave?

Reveal the weakness with classification report and confusion matrix

Accuracy treats all predictions equally. But in a churn problem, missing a churner (false negative) is far more costly than incorrectly flagging a loyal customer. You need metrics that break down performance per class.

Before you run the next cell, think about this: the model has 80% accuracy. Do you think it catches most of the customers who actually churn?

  • Create a new cell (Cell 12) to print the full classification report:
# Cell 12: Evaluate Baseline - Classification Report
print(classification_report(y_test, y_pred_lr, target_names=['No Churn', 'Churn']))
  • Run the cell.

Look at the recall value for the Churn row. It is around 0.55 to 0.60. That means the model misses nearly half of all customers who actually leave.

What do precision, recall, and F1-score mean?

  • Precision answers: of everyone the model flagged as churners, how many actually churned?
  • Recall answers: of everyone who actually churned, how many did the model catch?
  • F1-score is the harmonic mean of precision and recall. It balances both into a single number.

For a churn problem, recall on the Churn class is critical. A missed churner is a lost customer you could have saved.

  • Create a new cell (Cell 13) to see the exact numbers in a confusion matrix:
# Cell 13: Baseline Confusion Matrix
print(confusion_matrix(y_test, y_pred_lr))
  • Run the cell.

The confusion matrix is a 2x2 grid. The bottom-left number shows how many actual churners the model predicted as "No Churn." Those are your false negatives. You should see roughly 150+ missed churners out of about 374 total.

How to read the confusion matrix

The rows represent actual values and the columns represent predictions. Top-left is true negatives (correctly predicted No Churn). Bottom-right is true positives (correctly predicted Churn). Bottom-left is false negatives: real churners the model missed.

Seeing an error when running the cells?

  • If you see a NameError for X_train or y_test, make sure you ran all previous cells (Cells 1-9) first. Jupyter cells must be run in order.
  • If the accuracy value is very different from 0.80, double-check that Cell 9 uses random_state=42 and stratify=y. Different splits produce different results.

My Logistic Regression accuracy or recall looks wrong. Help me debug.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook with all four new cells (10-13).

ⓧ I'd like to double check the full code

Scroll through your notebook and confirm these cells exist in order, matching what you built above:

  • Cells 1-9 from previous steps (imports, load, inspect, visualize, clean, encode, split).
  • Cell 10: Train Logistic Regression Baseline (creates lr_model, fits, predicts y_pred_lr).
  • Cell 11: Evaluate Baseline - Accuracy (prints accuracy with accuracy_score).
  • Cell 12: Evaluate Baseline - Classification Report (prints classification_report with target_names=['No Churn', 'Churn']).
  • Cell 13: Baseline Confusion Matrix (prints confusion_matrix(y_test, y_pred_lr)).

So here is the key takeaway: 80% accuracy hides a serious weakness. The model catches most non-churners (the easy majority class) but misses nearly half the actual churners. In the next step, you will fix this with a Random Forest that accounts for class imbalance and properly evaluates with recall.

Fix It with a Random Forest and Proper Evaluation

Your Logistic Regression scored 80% accuracy, but the classification report exposed a critical blind spot. The model misses nearly half the customers who actually churn.

In this step, you will train a Random Forest with class balancing to close that gap. The class_weight='balanced' setting forces the model to pay extra attention to the minority class, directly addressing the imbalance problem you uncovered.

In this step, get ready to:
  • Train a Random Forest classifier with balanced class weights.
  • Compare precision, recall, and F1-score against the baseline.
  • Visualize feature importances to confirm your EDA findings.
Train a Random Forest with class balancing

A Random Forest builds many independent decision trees and combines their votes for the final prediction. By setting class_weight='balanced', you tell the algorithm to weight the minority class (churners) more heavily during training.

  • Create a new cell in your notebook and add the following code to train the Random Forest:
# Cell 14: Train Random Forest
rf_model = RandomForestClassifier(
    n_estimators=100, class_weight='balanced', random_state=42
)
rf_model.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_test)

What does this code do?

  • n_estimators=100 builds 100 decision trees that each vote on the prediction.
  • class_weight='balanced' automatically increases the importance of churners during training, compensating for the 27% vs 73% class imbalance.
  • random_state=42 ensures reproducible results so you get the same model each time.
  • Run the cell.
  • The cell completes without output. The model is trained and predictions are stored in y_pred_rf, ready for evaluation.
Compare evaluation metrics

Now for the moment of truth. Did the Random Forest actually catch more churners than the baseline?

Before you run the next cell, think about this: the Logistic Regression had about 55-60% recall on the churn class. Do you think the Random Forest will improve that number, and by how much?

  • Create a new cell and print the classification report for the Random Forest:
# Cell 15: Evaluate Random Forest
print(classification_report(y_test, y_pred_rf, target_names=['No Churn', 'Churn']))
  • Run the cell.

Look at the recall value for the Churn row. You should see it jump to approximately 70-80%, a significant improvement over the baseline's 55-60%.

Why did recall improve?

The class_weight='balanced' parameter told the Random Forest to penalize misclassifying churners more heavily. The model now works harder to catch at-risk customers, even if it means occasionally flagging a loyal customer as a churner (lower precision on the churn class).

This precision-recall tradeoff is exactly the kind of decision you make in real ML work. Missing a churner costs the business a lost customer. Flagging a loyal customer just means an unnecessary retention offer.

Numbers are helpful, but a confusion matrix gives you the full picture of correct vs. incorrect predictions.

  • Create a new cell and add the following code to visualize the confusion matrix as a heatmap:
# Cell 16: Confusion Matrix Heatmap
fig, ax = plt.subplots(figsize=(6, 5))
cm = confusion_matrix(y_test, y_pred_rf)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['No Churn', 'Churn'])
disp.plot(ax=ax, cmap='Blues')
ax.set_title('Random Forest Confusion Matrix')
plt.show()

How do I read this heatmap?

  • The top-left quadrant shows correctly predicted non-churners (true negatives).
  • The bottom-right quadrant shows correctly caught churners (true positives).
  • The bottom-left quadrant shows missed churners (false negatives). This is the number you want to minimize.
  • Compare the bottom-left value to what the Logistic Regression showed in Cell 13. You should see fewer missed churners.
  • Run the cell.
  • You should see a blue heatmap with four quadrants. The bottom-right cell (caught churners) should be noticeably larger than it was for the Logistic Regression baseline.

Heatmap not displaying?

  • Make sure you ran Cell 14 first. The variable y_pred_rf must exist before you can build the confusion matrix.
  • Check that plt.show() is the last line of the cell. Without it, the plot may not render.

Why isn't my confusion matrix heatmap showing up in Jupyter?

Inspect feature importance

Random Forests can tell you which features contributed most to the predictions. This lets you confirm whether the model agrees with the patterns you spotted during your exploratory analysis.

  • Create a new cell and add the following code to extract and plot the top 10 most important features:
# Cell 17: Feature Importance
feature_importances = pd.Series(
    rf_model.feature_importances_, index=X_train.columns
).sort_values(ascending=False)

fig, ax = plt.subplots(figsize=(10, 6))
feature_importances[:10].plot(kind='barh', ax=ax)
ax.set_title('Top 10 Feature Importances')
ax.set_xlabel('Importance')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

What does this code do?

  • rf_model.feature_importances_ extracts an importance score for every feature the model used.
  • sort_values(ascending=False) ranks features from most important to least.
  • The horizontal bar chart (kind='barh') makes feature names easy to read alongside their importance bars.
  • Run the cell.
  • You should see a horizontal bar chart with the top 10 features. Confirm that tenure, MonthlyCharges, TotalCharges, and Contract type rank among the top predictors.

This matches exactly what you found during your EDA in Step 2. Month-to-month contracts and higher charges were strongly associated with churn, and the model picked up on the same signals.

Feature names look strange or are cut off?

  • The plt.tight_layout() call should prevent label clipping. If names are still truncated, try increasing the figure width in figsize=(10, 6) to something larger like (12, 6).
  • Feature names with underscores (like Contract_Two year) come from the one-hot encoding in Cell 8. These are normal.

Help me fix my feature importance bar chart display in matplotlib.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your notebook with all four new cells (Cells 14-17) complete.

ⓧ I'd like to double check the full code

Your notebook should now contain Cells 1 through 17. Scroll through and confirm the following cells are present, in order:

  • Cells 1-9: Imports, data loading, EDA visualizations, cleaning, encoding, and train/test split.
  • Cells 10-13: Logistic Regression training and evaluation (accuracy, classification report, confusion matrix).
  • Cell 14: Random Forest training with class_weight='balanced'.
  • Cell 15: Random Forest classification report.
  • Cell 16: Confusion matrix heatmap using ConfusionMatrixDisplay.
  • Cell 17: Feature importance bar chart using rf_model.feature_importances_.

Secret mission

Tune Hyperparameters with GridSearchCV

Your Random Forest uses default hyperparameters. Can you systematically search for the best combination using cross-validation and a custom scoring metric? This is a technique interviewers regularly ask about.

Clean Up Your Resources

Clean Up Your Resources

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

Resources you used:

  • churn-predictor project folder (contains your virtual environment, notebook, dataset, and installed Python packages).
  • Jupyter extension in VS Code.

Keep everything running

No action needed. Choose this if you are still actively experimenting with your churn predictor or want to try new models and features.

  • Your churn-predictor folder stays on your Desktop with all files intact.
  • The Jupyter extension remains available in VS Code for future notebooks.
  • Your virtual environment stays ready to reactivate whenever you return.

Pause - I'll come back to this later

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

  • Close the churn_predictor.ipynb tab in VS Code to free up the notebook kernel's memory.
  • If your virtual environment is still active in the terminal, deactivate it by running:
deactivate
  • When you come back, reactivate the environment and reopen the notebook to continue.

Delete - I don't want to use this again

Remove all project files and extensions. This frees up disk space and leaves no trace of the project.

  • Deactivate your virtual environment first (if it is still active):
deactivate
  • Delete the entire project folder. On macOS or Linux, run:
rm -rf ~/Desktop/churn-predictor
  • On Windows, run:
rmdir /s /q "$env:USERPROFILE\Desktop\churn-predictor"
  • Uninstall the Jupyter extension in VS Code. Open the Extensions panel, find Jupyter, and click Uninstall.

That's a Wrap!

That's a Wrap!

Nice work. You just built a customer churn predictor that identifies at-risk telecom customers using real-world data, proper preprocessing, and a model that actually catches the churners a naive baseline misses.

You've learned how to:

  • Load and explore a real-world dataset (IBM Telco, 7043 customers, 21 features) with pandas and seaborn, spotting churn patterns through visualizations before writing a single line of model code.
  • Train a Logistic Regression baseline and discover first-hand why accuracy is misleading for imbalanced binary classification problems. Your model looked 80% accurate but missed nearly half the actual churners.
  • Fix the problem with a Random Forest using class_weight="balanced", then evaluate with precision, recall, and F1-score to prove your model catches significantly more churners.
  • Secret Mission: Tuned hyperparameters with GridSearchCV using recall as the scoring metric to squeeze out even better churn detection.

Ready to quiz yourself?