Predict Customer Churn with ML
Build an end-to-end churn prediction pipeline from raw data to CEO recommendations.
Introduction
30 Second Summary
Think about how your phone carrier, streaming service, or gym is always offering loyalty deals to keep you from cancelling. That's because losing a customer costs far more than finding a new one.
In this project, you will build a machine learning model that predicts which telecom customers are about to cancel their subscription. You'll play the role of a startup's first data hire, turning raw customer data into a ranked list of who to call before they leave.
What You'll Build
A complete customer churn prediction pipeline in a Jupyter notebook, from raw data exploration through model training to business-ready recommendations.
By the end of this project, you'll have:
- A trained Gradient Boosting classifier that achieves ROC-AUC above 0.80 on held-out test data.
- Feature importance visualizations that reveal the top drivers of customer churn.
- A risk-scored customer table with actionable retention recommendations ready to present to the CEO.
- Secret Mission: Calculate the dollar value of your model by estimating revenue saved from targeting the highest-risk customers.
Are there any prerequisites?
This project assumes you're comfortable writing Python and working with pandas DataFrames. Basic statistics knowledge (mean, distribution, correlation) will also help you follow along.
Set Up Your Environment
Before you can build a churn prediction model, you need a clean workspace with all the right tools loaded.
In this step, you will create a dedicated project folder, set up a Jupyter notebook, and load the raw customer data you will analyse throughout this project.
In this step, get ready to:
- Create your project folder and Jupyter notebook in VS Code.
- Install and import all required libraries.
- Load the Telco Customer Churn dataset and confirm it is ready for analysis.
Create your project folder and notebook
You need a dedicated folder to keep all your project files together, and a Jupyter notebook to write and run your code in cells.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- 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 this folder in VS Code by running:
code .
What does this command do?
The code . command tells VS Code to open the current folder as your workspace. A new VS Code window appears with churn-prediction shown in the Explorer sidebar.
- Confirm the churn-prediction folder name appears in the VS Code Explorer sidebar on the left.
- Open the Command Palette by pressing Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS).
- Type Create: New Jupyter Notebook and select the command that appears.
- A new untitled notebook opens. Save it immediately with Ctrl+S (Windows) or Cmd+S (macOS).
- Name the file churn_prediction.ipynb and click Save.
Now verify that your existing Python tools work. This is a quick diagnostic cell you will delete afterwards.
- In the first cell of your notebook, paste the following verification code and run it with Shift+Enter:
import pandas as pd
import sklearn
print(pd.__version__, sklearn.__version__)
What should I see?
You should see two version numbers printed below the cell. These confirm that pandas and scikit-learn are installed and working in your Python environment.
Seeing a ModuleNotFoundError?
If Python cannot find pandas or sklearn, your notebook may be using a different Python kernel than the one where these packages are installed.
- Click the kernel name in the top-right of the notebook and select the Python interpreter where you installed pandas and scikit-learn.
- Re-run the cell after switching kernels.
Help me fix my Jupyter kernel so pandas and sklearn are found.
- Once you see the version numbers, delete this verification cell by clicking inside it and pressing Ctrl+Shift+Backspace (Windows) or selecting Delete Cell from the cell toolbar.
Install and import your libraries
Your project depends on matplotlib for plotting charts and seaborn for statistical visualizations. These may not be installed yet.
- Open a new terminal in VS Code by pressing Ctrl+` (Windows) or Cmd+` (macOS).
- Install both plotting libraries by running:
pip install matplotlib seaborn
What if they are already installed?
If matplotlib or seaborn are already on your system, pip will print "Requirement already satisfied" and move on. No harm done.
You should see a success message or "Requirement already satisfied" for each package.
Seeing a permission error?
If you get a permissions error, try running the command with the --user flag to install in your user directory.
Help me fix my pip install permissions issue.
Now you will create Cell 1 of your notebook, which imports every library you need for the entire project.
- Click into the first empty cell in your notebook and paste the following imports:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import (
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay,
roc_auc_score,
)
What does this code do?
This cell loads every tool you will use throughout the project in one place. Keeping all imports at the top of a notebook is a best practice so you can see dependencies at a glance.
- pandas and numpy handle data loading and numeric operations.
- matplotlib and seaborn produce charts and statistical visualizations.
- The sklearn imports bring in preprocessing tools, model classes, and evaluation metrics you will use in later steps.
- Run the cell with Shift+Enter. The cell should complete with no output and no errors.
Seeing an ImportError?
If any import fails, the package is likely missing from your active Python environment. Install the missing package using pip install <package-name> in your terminal, then re-run the cell.
Help me fix my import errors in Jupyter.
Load and verify the dataset
The Telco Customer Churn dataset is hosted publicly on IBM's GitHub repository. You will load it directly from the URL so there is no manual download step.
- Create a new cell below your imports cell and paste this code:
# Cell 2: Load Data
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()
- Run the cell with Shift+Enter.
What does this code do?
This cell downloads the raw CSV file from IBM's public repository and loads it into a pandas DataFrame called df.
- print(df.shape) shows you how many rows and columns the dataset has.
- df.head() displays the first five rows so you can see what the raw data looks like.
You should see (7043, 21) printed, followed by a table showing customer data with columns like customerID, gender, tenure, and Churn.
Now confirm the target column and explore the column types. Create a new cell and paste this code:
- Create a new cell below and run this exploration code:
# Cell 3: Initial Exploration
print(df.info())
print(df["Churn"].value_counts(normalize=True))
- Run the cell with Shift+Enter.
What does this code do?
df.info() prints the column names, data types, and non-null counts for every column. This helps you spot data quality issues early.
value_counts(normalize=True) shows the percentage of customers in each category of the Churn column. You should see roughly 73% "No" and 27% "Yes".
You should see the Churn column listed with values Yes and No, confirming the target column is present and the dataset is ready for analysis.
Seeing a connection error?
If pd.read_csv(url) fails with a network error, check that you have internet access. The URL points to a public GitHub file, so no authentication is needed.
Help me troubleshoot loading the dataset from a URL.
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook with Ctrl+S (Windows) or Cmd+S (macOS).
ⓧ I'd like to double check the full code
Here is everything your notebook should contain at the end of this step. Each comment marks the start of a separate cell.
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import (
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay,
roc_auc_score,
)
# Cell 2: Load Data
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()
# Cell 3: Initial Exploration
print(df.info())
print(df["Churn"].value_counts(normalize=True))
Your workspace is ready and the raw data is loaded. Next up, you will explore this data visually and frame the business problem before building any models.
Explore and Frame the Business Problem
Your environment is set up and the data is loaded. Now you need to answer the CEO's first question: how bad is the churn problem?
Before building any model, you need to understand the shape of the problem. Exploring the data first reveals which features matter most and whether the dataset has any quirks that could trip up your model later.
In this step, get ready to:
- Calculate and visualize the customer churn rate.
- Explore which features correlate most strongly with churn.
- Clean the dataset so it is ready for model training.
Calculate the churn rate and visualize the class distribution
The first thing any data scientist checks is the target variable's distribution. If churn is extremely rare (like 1%), you would need different techniques than if it is more common.
From your initial exploration in the previous step, you already saw the churn rate using value_counts(normalize=True). Now let's make it visual so it is easy to communicate to stakeholders.
- Create a new cell in your notebook and add 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("Customer Churn Distribution")
plt.tight_layout()
plt.show()
What does this code do?
This creates a bar chart showing how many customers churned versus how many stayed.
- plt.subplots(figsize=(6, 4)) creates a figure that is 6 inches wide and 4 inches tall.
- sns.countplot() automatically counts how many rows belong to each category ("Yes" vs "No") and draws the bars.
- plt.tight_layout() prevents labels from getting cut off at the edges.
- Run the cell.
- You should see a bar chart with two bars. The "No" bar is significantly taller than the "Yes" bar, showing that roughly 73% of customers stayed and 27% churned.
What does 27% churn mean for the business?
Nearly 1 in 4 customers is leaving. This is an imbalanced dataset because the two classes (churn vs stay) are not evenly split.
For modeling, this means plain accuracy is misleading. A model that always predicts "No churn" would be 73% accurate but completely useless. You will use metrics like ROC-AUC and recall to measure real performance later.
Not seeing a chart?
- Make sure you ran Cell 1 (imports) first. The matplotlib and seaborn imports must be loaded before you can plot.
- Check that Cell 2 also ran successfully. The df variable must exist before you can visualize it.
Help me debug why my seaborn chart isn't showing in Jupyter
Explore key features versus churn
Now that you know the overall churn rate, the next question is: what makes a customer likely to leave? Visualizing features side-by-side with churn reveals patterns that will guide your model later.
- Create a new cell and add this code to visualize four key features against churn:
# Cell 5: Explore Key Features vs Churn
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
sns.countplot(x="Contract", hue="Churn", data=df, ax=axes[0, 0])
axes[0, 0].set_title("Churn by Contract Type")
sns.histplot(data=df, x="tenure", hue="Churn", kde=True, ax=axes[0, 1])
axes[0, 1].set_title("Churn by Tenure")
sns.boxplot(x="Churn", y="MonthlyCharges", data=df, ax=axes[1, 0])
axes[1, 0].set_title("Monthly Charges by Churn")
sns.countplot(x="InternetService", hue="Churn", data=df, ax=axes[1, 1])
axes[1, 1].set_title("Churn by Internet Service")
plt.tight_layout()
plt.show()
What do these four charts tell you?
- Contract type: Month-to-month customers churn far more than those on one-year or two-year contracts. Longer contracts lock customers in.
- Tenure: Customers who leave tend to have very short tenures. The churn distribution spikes in the first few months.
- Monthly charges: Churners tend to pay more per month. The box for "Yes" sits higher than the box for "No".
- Internet service: Fiber optic customers churn at a much higher rate than DSL or no-internet customers.
These patterns give you an early hypothesis: month-to-month contracts, high monthly charges, and fiber optic service are the strongest churn signals.
- Run the cell.
- You should see a 2x2 grid of charts. The top-left chart clearly shows month-to-month customers dominating the churn category.
Charts look squished or overlapping?
- Make sure figsize=(12, 10) is set. A smaller figure will crowd the four subplots together.
- Confirm plt.tight_layout() is called before plt.show(). This adjusts spacing between subplots automatically.
Help me fix my subplot layout in matplotlib
Clean the data for modeling
Real-world data is never perfectly clean. Before you can train a model, you need to fix data type issues, remove unusable columns, and convert the target to a format scikit-learn can work with.
- Create a new cell and add this code to clean the dataset:
# Cell 6: Data Cleaning
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df.dropna(subset=["TotalCharges"], inplace=True)
df.drop("customerID", axis=1, inplace=True)
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
print(f"Clean dataset shape: {df.shape}")
What does this cleaning do?
- pd.to_numeric(errors="coerce") converts TotalCharges from a string to a number. Some rows have whitespace instead of a value. These become NaN.
- dropna(subset=["TotalCharges"]) removes the 11 rows where TotalCharges was empty. These correspond to customers with zero tenure.
- df.drop("customerID") removes the ID column. A unique identifier has no predictive value and would confuse the model.
- df["Churn"].map({"Yes": 1, "No": 0}) converts the target from text to binary integers. scikit-learn classifiers require numeric targets.
- Run the cell.
- You should see the output Clean dataset shape: (7032, 20). That is 7032 rows (11 dropped) and 20 columns (customerID removed, leaving 19 features plus the Churn target).
Seeing a different row count?
- If you see fewer than 7032 rows, you may have run Cell 6 multiple times. Each run of dropna() and drop() modifies df in place. Re-run Cell 2 to reload the original data, then run Cell 6 once.
- If you see 21 columns instead of 20, the df.drop("customerID", axis=1, inplace=True) line may not have run. Check for typos in the column name.
Help me fix my data cleaning cell
✔️ Awesome, I've got everything!
Great. Make sure your notebook is saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete notebook code through the end of this step:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import (
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay,
roc_auc_score,
)
# Cell 2: Load Data
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()
# Cell 3: Initial Exploration
print(df.info())
print(df["Churn"].value_counts(normalize=True))
# Cell 4: Visualize Churn Distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(x="Churn", data=df, ax=ax)
ax.set_title("Customer Churn Distribution")
plt.tight_layout()
plt.show()
# Cell 5: Explore Key Features vs Churn
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
sns.countplot(x="Contract", hue="Churn", data=df, ax=axes[0, 0])
axes[0, 0].set_title("Churn by Contract Type")
sns.histplot(data=df, x="tenure", hue="Churn", kde=True, ax=axes[0, 1])
axes[0, 1].set_title("Churn by Tenure")
sns.boxplot(x="Churn", y="MonthlyCharges", data=df, ax=axes[1, 0])
axes[1, 0].set_title("Monthly Charges by Churn")
sns.countplot(x="InternetService", hue="Churn", data=df, ax=axes[1, 1])
axes[1, 1].set_title("Churn by Internet Service")
plt.tight_layout()
plt.show()
# Cell 6: Data Cleaning
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df.dropna(subset=["TotalCharges"], inplace=True)
df.drop("customerID", axis=1, inplace=True)
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
print(f"Clean dataset shape: {df.shape}")
You now have a clean dataset and a clear picture of who is churning. Next up, you will preprocess these features and engineer them into a format that machine learning models can actually consume.
Preprocess and Engineer Features
In the previous step, you explored the Telco dataset and identified key churn drivers like month-to-month contracts and high monthly charges. You also cleaned the data so it's ready for modeling.
Machine learning models can't work with raw DataFrames directly. They need numeric input with no missing values. In this step, you'll split the data into training and test sets, then build a preprocessing pipeline that transforms both numeric and categorical columns into model-ready format.
In this step, get ready to:
- Separate features from the target and split into train/test sets.
- Build a preprocessing pipeline that handles numeric and categorical columns.
- Verify the data is ready for model training.
Separate features and split the data
Before training a model, you need to separate what the model learns from (features) and what it predicts (target). You also need to hold back a portion of data that the model never sees during training, so you can test its real-world performance honestly.
- Create a new cell in your notebook and define the features and target by pasting this code:
# Cell 7: Define Features and Target
X = df.drop("Churn", axis=1)
y = df["Churn"]
# Identify column types
numeric_cols = X.select_dtypes(include=["int64", "float64"]).columns.tolist()
categorical_cols = X.select_dtypes(include=["object"]).columns.tolist()
print(f"Numeric features ({len(numeric_cols)}): {numeric_cols}")
print(f"Categorical features ({len(categorical_cols)}): {categorical_cols}")
What does this code do?
X = df.drop("Churn", axis=1) creates the feature matrix by taking every column except the target. y = df["Churn"] isolates the binary target the model will predict.
The select_dtypes calls automatically sort your columns into numeric (like tenure, MonthlyCharges) and categorical (like Contract, InternetService). This separation matters because each type needs different preprocessing.
- Run the cell.
- You should see 3 numeric features and 16 categorical features printed below the cell.
Seeing a KeyError for Churn?
Make sure you ran Cell 6 (Data Cleaning) first. That cell converts the Churn column to binary 1/0 values. If you skipped it, the column may still have string values or a different name.
Help me debug a KeyError when dropping the Churn column from my DataFrame.
Now you need to split this data into a training set (for the model to learn from) and a test set (to evaluate performance). This prevents the model from memorizing answers instead of learning patterns.
- Create a new cell and split the data by pasting this code:
# Cell 8: Train-Test Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Training set: {X_train.shape[0]} rows")
print(f"Test set: {X_test.shape[0]} rows")
What does this code do?
train_test_split randomly divides the data into 80% training and 20% test. The random_state=42 ensures the split is reproducible every time you run it.
stratify=y is critical for imbalanced data like ours. It preserves the 27% churn ratio in both the training and test sets. Without it, you could end up with a test set that has far more or fewer churners than reality.
- Run the cell.
- You should see approximately 5634 training rows and 1409 test rows.
Seeing different row counts?
If your numbers are slightly different from 5634/1409, check that Cell 6 dropped the same rows. The dropna(subset=["TotalCharges"]) call should remove exactly 11 rows with whitespace values. If you see a very different count (like 5000 or 6000), you may have accidentally re-run a cleaning cell that dropped extra rows.
Help me figure out why my train-test split has unexpected row counts.
Build the preprocessing pipeline
Your features include both numbers (like tenure) and categories (like Contract type). ML models need all inputs as numbers, and they work best when those numbers are on a similar scale.
A ColumnTransformer lets you apply different transformations to different column types in a single step. Numeric columns get scaled. Categorical columns get encoded into binary indicators.
- Create a new cell and build the preprocessor by pasting this code:
# Cell 9: Build Preprocessing Pipeline
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), categorical_cols),
]
)
What does this code do?
StandardScaler() centers numeric columns around zero and scales them to unit variance. This prevents features with large values (like TotalCharges in the thousands) from dominating features with small values (like tenure in months).
OneHotEncoder(drop="first") converts each categorical column into binary columns (one per category), then drops the first category to avoid multicollinearity. handle_unknown="ignore" ensures the model won't crash if it encounters a new category value in future data.
- Run the cell to define the preprocessor.
- The cell should run without errors. No output is printed because you're just defining the transformer, not fitting it yet.
Seeing a NameError for numeric_cols or categorical_cols?
These variables are defined in Cell 7. Make sure you ran that cell first. In Jupyter, cells must be run in order for variables to be available to later cells.
Help me understand why my notebook can't find the numeric_cols variable.
Why a ColumnTransformer instead of manual encoding?
You could scale and encode columns one by one with separate lines of code. A ColumnTransformer bundles all transformations into a single object that you can plug directly into a scikit-learn Pipeline.
This matters because Pipelines prevent data leakage. The preprocessor only learns scaling parameters from training data, never from the test set. Manual encoding makes it easy to accidentally fit on the full dataset.
Verify your preprocessing setup
Before moving to model training, confirm that your data pipeline is correctly configured. The preprocessor you just built will be fitted automatically when you use it inside a Pipeline in the next step.
- Scroll back to Cell 7 and confirm the output shows 3 numeric features (SeniorCitizen, tenure, MonthlyCharges, TotalCharges) and 16 categorical features.
- Confirm Cell 8 shows approximately 5634 training rows and 1409 test rows.
- Confirm Cell 9 ran without errors.
What happens when the preprocessor is fitted?
When the preprocessor is fitted on training data (which happens automatically inside a Pipeline), StandardScaler learns the mean and standard deviation of each numeric column. OneHotEncoder learns the unique categories in each categorical column.
These learned parameters are then applied consistently to both training and test data. This ensures the model sees test data transformed in exactly the same way as training data, with no information leakage.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your notebook.
ⓧ I'd like to double check the full code
Here is the full notebook code up to this point. Compare it against your cells:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import (
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay,
roc_auc_score,
)
# Cell 2: Load Data
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()
# Cell 3: Initial Exploration
print(df.info())
print(df["Churn"].value_counts(normalize=True))
# Cell 4: Visualize Churn Distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(x="Churn", data=df, ax=ax)
ax.set_title("Customer Churn Distribution")
plt.tight_layout()
plt.show()
# Cell 5: Explore Key Features vs Churn
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
sns.countplot(x="Contract", hue="Churn", data=df, ax=axes[0, 0])
axes[0, 0].set_title("Churn by Contract Type")
sns.histplot(data=df, x="tenure", hue="Churn", kde=True, ax=axes[0, 1])
axes[0, 1].set_title("Churn by Tenure")
sns.boxplot(x="Churn", y="MonthlyCharges", data=df, ax=axes[1, 0])
axes[1, 0].set_title("Monthly Charges by Churn")
sns.countplot(x="InternetService", hue="Churn", data=df, ax=axes[1, 1])
axes[1, 1].set_title("Churn by Internet Service")
plt.tight_layout()
plt.show()
# Cell 6: Data Cleaning
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df.dropna(subset=["TotalCharges"], inplace=True)
df.drop("customerID", axis=1, inplace=True)
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
print(f"Clean dataset shape: {df.shape}")
# Cell 7: Define Features and Target
X = df.drop("Churn", axis=1)
y = df["Churn"]
numeric_cols = X.select_dtypes(include=["int64", "float64"]).columns.tolist()
categorical_cols = X.select_dtypes(include=["object"]).columns.tolist()
print(f"Numeric features ({len(numeric_cols)}): {numeric_cols}")
print(f"Categorical features ({len(categorical_cols)}): {categorical_cols}")
# Cell 8: Train-Test Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Training set: {X_train.shape[0]} rows")
print(f"Test set: {X_test.shape[0]} rows")
# Cell 9: Build Preprocessing Pipeline
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), categorical_cols),
]
)
Your data is split and your preprocessing pipeline is ready to go. Next up, you'll train two models and see which one better predicts customer churn.
Train and Compare Models
Your data is preprocessed and split into training and test sets. Now comes the core machine learning work: training models that learn patterns from your training data and predict churn on unseen customers.
You'll train two models with different strengths. Logistic Regression gives you a simple, interpretable baseline. Gradient Boosting gives you a more powerful model that captures complex patterns between features. Comparing both reveals whether the added complexity pays off.
In this step, get ready to:
- Train a Logistic Regression baseline model.
- Train a Gradient Boosting model.
- Compare both models using classification reports and ROC-AUC scores.
Train a Logistic Regression baseline
A scikit-learn Pipeline chains your preprocessing and model into a single object. This means you call .fit() once and both the scaler/encoder and the model train together on the raw data.
- Create a new cell in your notebook and build your Logistic Regression pipeline by pasting this code:
# Cell 10: Logistic Regression Pipeline
lr_pipeline = Pipeline(
steps=[
("preprocessor", preprocessor),
("classifier", LogisticRegression(max_iter=1000, random_state=42)),
]
)
lr_pipeline.fit(X_train, y_train)
print("Logistic Regression trained.")
What does this code do?
- Pipeline(steps=[...]) chains the preprocessor (scaling + encoding) with LogisticRegression into one trainable unit.
- max_iter=1000 gives the solver enough iterations to converge on this dataset.
- random_state=42 ensures reproducible results every time you re-run the cell.
- .fit(X_train, y_train) trains both the preprocessor and model on the training data in one call.
- Run the cell. You should see "Logistic Regression trained." printed in the output.
Seeing a ConvergenceWarning?
If you see a warning about the solver not converging, make sure max_iter=1000 is set in your LogisticRegression constructor. The default of 100 is too low for this dataset.
Also confirm that your preprocessor variable from the previous step is still defined. If you restarted your kernel, re-run all cells above first.
Help me debug my Logistic Regression pipeline
Why start with Logistic Regression?
Logistic Regression is the standard first model for binary classification problems. It's fast to train, easy to interpret, and gives you a performance floor. If a more complex model can't beat it meaningfully, the complexity isn't worth the tradeoff.
This "start simple, escalate when needed" pattern is how working data scientists operate. You'll train Gradient Boosting next to see if the extra power captures patterns Logistic Regression misses.
Train a Gradient Boosting model
Gradient Boosting builds many small decision trees sequentially, where each tree corrects the mistakes of the previous one. It typically outperforms Logistic Regression on structured tabular data like this.
- Create a new cell and build your Gradient Boosting pipeline by pasting this code:
# Cell 11: Gradient Boosting Pipeline
gb_pipeline = Pipeline(
steps=[
("preprocessor", preprocessor),
("classifier", GradientBoostingClassifier(n_estimators=100, random_state=42)),
]
)
gb_pipeline.fit(X_train, y_train)
print("Gradient Boosting trained.")
What does this code do?
- The same Pipeline pattern chains your preprocessor with GradientBoostingClassifier.
- n_estimators=100 means the model builds 100 sequential decision trees. More trees generally means better performance up to a point.
- random_state=42 keeps results reproducible.
- Run the cell. You should see "Gradient Boosting trained." printed. This cell takes a few seconds longer than Logistic Regression because it's fitting 100 trees.
Cell running for too long?
Gradient Boosting typically takes 5-15 seconds on this dataset. If it runs longer than 30 seconds, check that your training set has roughly 5634 rows (not the full 7043). A much larger training set means something went wrong with your train-test split in the previous step.
Help me debug my Gradient Boosting training time
Generate predictions and compare performance
With both models trained, you can now generate predictions on the test set. You'll get both hard predictions (churn yes/no) and probability scores (how confident the model is). The probability scores are what make this useful for a business: you can rank customers by churn risk.
- Create a new cell and generate predictions for both models by pasting this code:
# Cell 12: Generate Predictions
y_pred_lr = lr_pipeline.predict(X_test)
y_proba_lr = lr_pipeline.predict_proba(X_test)[:, 1]
y_pred_gb = gb_pipeline.predict(X_test)
y_proba_gb = gb_pipeline.predict_proba(X_test)[:, 1]
What does this code do?
- .predict() returns hard class labels (0 or 1) for each test customer.
- .predict_proba()[:, 1] returns the probability that each customer belongs to class 1 (churn). This is the churn risk score.
- You generate both types for each model so you can compare them with different metrics.
- Run the cell. You won't see output yet. The next cell uses these predictions to print classification reports.
- Create one more cell and print the classification reports for both models by pasting this code:
# Cell 13: Classification Reports
print("=== Logistic Regression ===")
print(classification_report(y_test, y_pred_lr))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba_lr):.4f}")
print("\n=== Gradient Boosting ===")
print(classification_report(y_test, y_pred_gb))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba_gb):.4f}")
What does this code do?
classification_report prints precision, recall, and F1-score for each class (0 = stayed, 1 = churned). These metrics tell you different things:
- Precision for class 1: of the customers the model flagged as churners, what percentage actually churned.
- Recall for class 1: of the customers who actually churned, what percentage did the model catch.
- ROC-AUC: an overall score (0 to 1) measuring how well the model separates churners from non-churners across all thresholds.
- Run the cell. You should see two full classification reports printed, one for each model, with ROC-AUC scores at the bottom of each.
Gradient Boosting should achieve a ROC-AUC around 0.84-0.85 and outperform Logistic Regression on recall for the churn class. This means it catches more of the customers who are actually about to leave.
Seeing a NameError for y_pred_lr or y_proba_gb?
This means your predictions cell (Cell 12) did not run successfully. Go back and run it before running the classification reports cell.
If you restarted your Jupyter kernel at any point, re-run all cells from the top. Each cell depends on variables created by the cells above it.
Help me fix my classification report errors
✔️ Awesome, I've got everything!
Great. Double check you've saved your notebook before moving on.
ⓧ I'd like to double check the full code
Here is the complete notebook code up to this point. Compare it against your cells:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import (
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay,
roc_auc_score,
)
# Cell 2: Load Data
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()
# Cell 3: Initial Exploration
print(df.info())
print(df["Churn"].value_counts(normalize=True))
# Cell 4: Visualize Churn Distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(x="Churn", data=df, ax=ax)
ax.set_title("Customer Churn Distribution")
plt.tight_layout()
plt.show()
# Cell 5: Explore Key Features vs Churn
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
sns.countplot(x="Contract", hue="Churn", data=df, ax=axes[0, 0])
axes[0, 0].set_title("Churn by Contract Type")
sns.histplot(data=df, x="tenure", hue="Churn", kde=True, ax=axes[0, 1])
axes[0, 1].set_title("Churn by Tenure")
sns.boxplot(x="Churn", y="MonthlyCharges", data=df, ax=axes[1, 0])
axes[1, 0].set_title("Monthly Charges by Churn")
sns.countplot(x="InternetService", hue="Churn", data=df, ax=axes[1, 1])
axes[1, 1].set_title("Churn by Internet Service")
plt.tight_layout()
plt.show()
# Cell 6: Data Cleaning
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df.dropna(subset=["TotalCharges"], inplace=True)
df.drop("customerID", axis=1, inplace=True)
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
print(f"Clean dataset shape: {df.shape}")
# Cell 7: Define Features and Target
X = df.drop("Churn", axis=1)
y = df["Churn"]
numeric_cols = X.select_dtypes(include=["int64", "float64"]).columns.tolist()
categorical_cols = X.select_dtypes(include=["object"]).columns.tolist()
print(f"Numeric features ({len(numeric_cols)}): {numeric_cols}")
print(f"Categorical features ({len(categorical_cols)}): {categorical_cols}")
# Cell 8: Train-Test Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Training set: {X_train.shape[0]} rows")
print(f"Test set: {X_test.shape[0]} rows")
# Cell 9: Build Preprocessing Pipeline
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), categorical_cols),
]
)
# Cell 10: Logistic Regression Pipeline
lr_pipeline = Pipeline(
steps=[
("preprocessor", preprocessor),
("classifier", LogisticRegression(max_iter=1000, random_state=42)),
]
)
lr_pipeline.fit(X_train, y_train)
print("Logistic Regression trained.")
# Cell 11: Gradient Boosting Pipeline
gb_pipeline = Pipeline(
steps=[
("preprocessor", preprocessor),
("classifier", GradientBoostingClassifier(n_estimators=100, random_state=42)),
]
)
gb_pipeline.fit(X_train, y_train)
print("Gradient Boosting trained.")
# Cell 12: Generate Predictions
y_pred_lr = lr_pipeline.predict(X_test)
y_proba_lr = lr_pipeline.predict_proba(X_test)[:, 1]
y_pred_gb = gb_pipeline.predict(X_test)
y_proba_gb = gb_pipeline.predict_proba(X_test)[:, 1]
# Cell 13: Classification Reports
print("=== Logistic Regression ===")
print(classification_report(y_test, y_pred_lr))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba_lr):.4f}")
print("\n=== Gradient Boosting ===")
print(classification_report(y_test, y_pred_gb))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba_gb):.4f}")
Your models are trained and the numbers are in. Next up, you'll visualize these results with ROC curves and dig into which features drive churn the most.
Evaluate with Business-Relevant Metrics
Your two models are trained and you have raw classification reports in hand. Numbers like accuracy and F1-score are useful for comparing models, but they don't answer the question your CEO actually cares about: "How good is this model at catching churners before they leave?"
In this step, you will evaluate your models using visualizations and metrics that translate directly into business decisions. You will plot ROC curves, explore precision-recall tradeoffs, and identify which features drive churn the most.
In this step, get ready to:
- Plot ROC curves for both models and confirm Gradient Boosting achieves ROC-AUC above 0.80.
- Visualize the precision-recall tradeoff and frame it as a business decision.
- Extract feature importances and translate the top drivers into actionable business insights.
Plot ROC curves for both models
The ROC curve plots how well a model separates the two classes at every possible decision threshold. The area under this curve (AUC) gives you a single number summarizing overall discrimination power.
- Create a new cell in your notebook (Cell 14) and plot both models' ROC curves on the same figure by running this code:
# --- Cell 14: ROC Curves ---
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba_lr, name="Logistic Regression", ax=ax)
RocCurveDisplay.from_predictions(y_test, y_proba_gb, name="Gradient Boosting", ax=ax)
ax.set_title("ROC Curve Comparison")
plt.tight_layout()
plt.show()
What does this code do?
RocCurveDisplay.from_predictions() takes the true labels and predicted probabilities, then draws the ROC curve directly. By passing the same ax to both calls, both curves appear on a single figure for easy comparison.
A curve that hugs the top-left corner means the model catches many true churners without raising too many false alarms. The diagonal line represents a random guess (AUC = 0.50).
- Run the cell. You should see two curves plotted on the same axes. Gradient Boosting's curve should sit slightly above Logistic Regression's, confirming the AUC scores you printed in the previous step.
Why does AUC matter more than accuracy here?
Your dataset is imbalanced (roughly 73% non-churn, 27% churn). A model that simply predicts "no churn" for everyone would score 73% accuracy. That looks impressive but catches zero actual churners.
AUC measures how well the model ranks churners above non-churners regardless of the threshold. It is immune to class imbalance, making it the right metric for your CEO's question.
- Add a markdown cell below Cell 14 explaining why you chose AUC over accuracy. Write 1-2 sentences referencing the class imbalance. This documents your reasoning for anyone reviewing the notebook later.
Don't see both curves?
Make sure you ran Cell 12 (predictions) and Cell 13 (classification reports) from the previous step before running this cell. The variables y_proba_lr and y_proba_gb must exist in memory.
Help me debug my ROC curve plot. I have two models but only one curve is showing, or I'm getting a NameError.
Plot the precision-recall curve and frame the tradeoff
The precision-recall curve zooms in on a specific business question: when your model flags a customer as "about to churn," how often is it right (precision)? And of all the customers who actually churn, how many does the model catch (recall)?
- Create Cell 15 and plot the precision-recall curve for the Gradient Boosting model by running this code:
# --- Cell 15: Precision-Recall Curve ---
fig, ax = plt.subplots(figsize=(8, 6))
PrecisionRecallDisplay.from_predictions(y_test, y_proba_gb, name="Gradient Boosting", ax=ax)
ax.set_title("Precision-Recall Curve (Gradient Boosting)")
plt.tight_layout()
plt.show()
What does this code do?
PrecisionRecallDisplay.from_predictions() plots precision (y-axis) against recall (x-axis) at every possible classification threshold. You only plot the Gradient Boosting model here because it is the stronger model you would deploy.
- Run the cell. You should see a curve that starts high on the left (high precision at low recall) and slopes downward as recall increases.
How do I read this as a business decision?
Think of it this way: if you want to catch 80% of churners (recall = 0.80), trace horizontally on the curve to see what precision you get. A precision of 0.50 means that for every 100 customers your model flags, about 50 were actually going to churn.
Is it worth calling 100 customers if 50 of them were actually going to churn? For a startup where each retained customer is worth hundreds of dollars annually, the answer is almost certainly yes.
- Add a markdown cell below Cell 15 framing the precision-recall tradeoff as a business decision. Write 2-3 sentences explaining what recall target you would recommend and why.
Seeing a flat or unexpected curve?
Make sure you passed y_proba_gb (probability scores), not y_pred_gb (binary predictions). Binary predictions produce a curve with only one point, not a smooth line.
Help me understand my Precision-Recall curve. The shape looks wrong or it only has a single point.
Extract and visualize feature importances
Knowing which features drive churn is just as valuable as the prediction itself. Feature importances tell you where to focus your retention strategy.
- Create Cell 16 and extract the top 10 feature importances from the Gradient Boosting model by running this code:
# --- Cell 16: Feature Importance ---
feature_names = (
numeric_cols
+ list(
lr_pipeline.named_steps["preprocessor"]
.transformers_[1][1]
.get_feature_names_out(categorical_cols)
)
)
importances = gb_pipeline.named_steps["classifier"].feature_importances_
feat_imp_df = pd.DataFrame({"feature": feature_names, "importance": importances})
feat_imp_df = feat_imp_df.sort_values("importance", ascending=False).head(10)
fig, ax = plt.subplots(figsize=(10, 6))
sns.barplot(x="importance", y="feature", data=feat_imp_df, ax=ax)
ax.set_title("Top 10 Feature Importances (Gradient Boosting)")
plt.tight_layout()
plt.show()
What does this code do?
feature_names reconstructs the full list of column names after preprocessing. Numeric columns keep their original names. Categorical columns get expanded names from the OneHotEncoder via get_feature_names_out().
feature_importances_ is an attribute of the trained Gradient Boosting classifier that scores how much each feature contributed to reducing prediction error. Higher values mean the feature was more useful for splitting churners from non-churners.
The result is sorted into a DataFrame and the top 10 are plotted as a horizontal bar chart for easy reading.
- Run the cell. You should see a horizontal bar chart with features like Contract, tenure, and MonthlyCharges appearing near the top.
What do these features mean for the business?
Month-to-month contracts are the strongest churn signal. Customers without a commitment can leave at any time. Offering contract upgrade incentives (annual or two-year plans) is a direct retention lever.
Low tenure and high monthly charges also rank highly. New customers paying a lot are the riskiest segment. Fiber optic service correlates with higher churn, likely because those customers pay more and have more switching options.
- Add a markdown cell below Cell 16 translating the top features into business language. Write 2-3 sentences as if briefing the CEO on which customer segments to target for retention.
Getting a length mismatch error?
If you see an error like "arrays must all be same length" when creating the DataFrame, it means feature_names and importances have different lengths. Make sure you ran all cells from Cell 1 through Cell 11 in order so that numeric_cols and categorical_cols are defined correctly.
Help me debug a length mismatch error when building my feature importance DataFrame.
✔️ Awesome, I've got everything!
Great. Double check you have saved your notebook with all cells through Cell 16.
ⓧ I'd like to double check the full code
Here is the complete notebook code through Cell 16. Compare your cells against this:
# Cell 1: Imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import (
classification_report,
RocCurveDisplay,
PrecisionRecallDisplay,
roc_auc_score,
)
# Cell 2: Load Data
url = "https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
df = pd.read_csv(url)
print(df.shape)
df.head()
# Cell 3: Initial Exploration
print(df.info())
print(df["Churn"].value_counts(normalize=True))
# Cell 4: Visualize Churn Distribution
fig, ax = plt.subplots(figsize=(6, 4))
sns.countplot(x="Churn", data=df, ax=ax)
ax.set_title("Customer Churn Distribution")
plt.tight_layout()
plt.show()
# Cell 5: Explore Key Features vs Churn
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
sns.countplot(x="Contract", hue="Churn", data=df, ax=axes[0, 0])
axes[0, 0].set_title("Churn by Contract Type")
sns.histplot(data=df, x="tenure", hue="Churn", kde=True, ax=axes[0, 1])
axes[0, 1].set_title("Churn by Tenure")
sns.boxplot(x="Churn", y="MonthlyCharges", data=df, ax=axes[1, 0])
axes[1, 0].set_title("Monthly Charges by Churn")
sns.countplot(x="InternetService", hue="Churn", data=df, ax=axes[1, 1])
axes[1, 1].set_title("Churn by Internet Service")
plt.tight_layout()
plt.show()
# Cell 6: Data Cleaning
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
df.dropna(subset=["TotalCharges"], inplace=True)
df.drop("customerID", axis=1, inplace=True)
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
print(f"Clean dataset shape: {df.shape}")
# Cell 7: Define Features and Target
X = df.drop("Churn", axis=1)
y = df["Churn"]
numeric_cols = X.select_dtypes(include=["int64", "float64"]).columns.tolist()
categorical_cols = X.select_dtypes(include=["object"]).columns.tolist()
print(f"Numeric features ({len(numeric_cols)}): {numeric_cols}")
print(f"Categorical features ({len(categorical_cols)}): {categorical_cols}")
# Cell 8: Train-Test Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Training set: {X_train.shape[0]} rows")
print(f"Test set: {X_test.shape[0]} rows")
# Cell 9: Build Preprocessing Pipeline
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numeric_cols),
("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), categorical_cols),
]
)
# Cell 10: Logistic Regression Pipeline
lr_pipeline = Pipeline(
steps=[
("preprocessor", preprocessor),
("classifier", LogisticRegression(max_iter=1000, random_state=42)),
]
)
lr_pipeline.fit(X_train, y_train)
print("Logistic Regression trained.")
# Cell 11: Gradient Boosting Pipeline
gb_pipeline = Pipeline(
steps=[
("preprocessor", preprocessor),
("classifier", GradientBoostingClassifier(n_estimators=100, random_state=42)),
]
)
gb_pipeline.fit(X_train, y_train)
print("Gradient Boosting trained.")
# Cell 12: Generate Predictions
y_pred_lr = lr_pipeline.predict(X_test)
y_proba_lr = lr_pipeline.predict_proba(X_test)[:, 1]
y_pred_gb = gb_pipeline.predict(X_test)
y_proba_gb = gb_pipeline.predict_proba(X_test)[:, 1]
# Cell 13: Classification Reports
print("=== Logistic Regression ===")
print(classification_report(y_test, y_pred_lr))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba_lr):.4f}")
print("\n=== Gradient Boosting ===")
print(classification_report(y_test, y_pred_gb))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba_gb):.4f}")
# Cell 14: ROC Curves
fig, ax = plt.subplots(figsize=(8, 6))
RocCurveDisplay.from_predictions(y_test, y_proba_lr, name="Logistic Regression", ax=ax)
RocCurveDisplay.from_predictions(y_test, y_proba_gb, name="Gradient Boosting", ax=ax)
ax.set_title("ROC Curve Comparison")
plt.tight_layout()
plt.show()
# Cell 15: Precision-Recall Curve
fig, ax = plt.subplots(figsize=(8, 6))
PrecisionRecallDisplay.from_predictions(y_test, y_proba_gb, name="Gradient Boosting", ax=ax)
ax.set_title("Precision-Recall Curve (Gradient Boosting)")
plt.tight_layout()
plt.show()
# Cell 16: Feature Importance
feature_names = (
numeric_cols
+ list(
lr_pipeline.named_steps["preprocessor"]
.transformers_[1][1]
.get_feature_names_out(categorical_cols)
)
)
importances = gb_pipeline.named_steps["classifier"].feature_importances_
feat_imp_df = pd.DataFrame({"feature": feature_names, "importance": importances})
feat_imp_df = feat_imp_df.sort_values("importance", ascending=False).head(10)
fig, ax = plt.subplots(figsize=(10, 6))
sns.barplot(x="importance", y="feature", data=feat_imp_df, ax=ax)
ax.set_title("Top 10 Feature Importances (Gradient Boosting)")
plt.tight_layout()
plt.show()
You now have a complete evaluation suite: ROC curves proving your model's discrimination power, a precision-recall analysis framing the tradeoff as a business decision, and a feature importance chart pointing your retention team at the right customers. Your notebook tells a compelling story from raw data to actionable insight.
Secret mission
Calculate the Business Value of Your Model
Your model can predict churn, but can you put a dollar figure on it? In this extension, you'll calculate how much annual revenue your model could save by targeting the highest-risk customers. This is the kind of analysis that turns a technical exercise into a CEO-ready recommendation.
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 reference, or delete them to free up disk space.
Resources you used:
- The churn-prediction folder on your Desktop containing churn_prediction.ipynb.
- Python packages installed via pip: matplotlib and seaborn.
Keep everything running
No action needed. Choose this if you want to revisit your churn prediction notebook, experiment with different models, or use it as a reference for future ML projects.
- Your churn-prediction folder stays on your Desktop.
- The matplotlib and seaborn packages remain installed and available for other Python projects.
Pause - I'll come back to this later
Close VS Code to free up memory. Your files stay on disk and you can reopen the notebook anytime.
- Close VS Code.
- Your churn-prediction folder and installed packages remain intact for next time.
Delete - I don't want to use this again
Remove all project files and uninstall the packages you added during this project.
- Close VS Code.
- Delete the project folder by opening a terminal and running:
cd ~\Desktop
rmdir /s /q churn-prediction
- Uninstall the packages you installed for this project by running:
pip uninstall matplotlib seaborn -y
Nice Work!
Nice Work!
You just built a complete customer churn prediction pipeline from raw data to CEO-ready business recommendations. That is the exact workflow startup founders hire data scientists to deliver.
Here is what you accomplished:
- Built an end-to-end ML classification pipeline that loads raw data, cleans it, engineers features, trains two models, and evaluates them with business-relevant metrics.
- Trained and compared Logistic Regression and Gradient Boosting models using scikit-learn Pipelines, achieving ROC-AUC above 0.80 on held-out test data.
- Translated model outputs into actionable business insights by identifying the top churn drivers (contract type, tenure, monthly charges) and framing them as retention recommendations.
- Secret Mission: Quantified the dollar value of your model by calculating annual revenue at risk for the top 20% highest-risk customers and producing a CEO-ready retention recommendation.
Ready to quiz yourself?