Customer Churn Prediction
Predicts whether a telecom customer will churn using the Telco Customer Churn dataset. The project covers data cleaning and EDA, comparison of three classifiers, and a FastAPI service that serves the final model.
Project Structure
.
├── API/
│ └── main.py # FastAPI app serving the model
├── Data/
│ └── WA_Fn-UseC_-Telco-Customer-Churn.csv # raw dataset (7,043 customers)
├── Models/
│ ├── ml_pipeline.joblib # fitted preprocessing + classifier pipeline
│ └── target_labels.joblib # LabelEncoder for the churn target
├── Notebooks/
│ └── customer_churn_prediction.ipynb # data cleaning, EDA, model training
├── cpp/ # (Python virtual environment)
├── .env # environment variables (not committed)
├── .gitignore
├── requirements.txt
└── README.md
main.py loads ../models/ml_pipeline.joblib and ../models/target_labels.joblib,
so it must be run from inside API/ for those relative paths to resolve.
Dataset
The raw data is the Telco Customer Churn dataset 7,043 rows and 21 columns covering customer demographics, account…
If you've ever trained a model in a Jupyter notebook and then wondered "okay... now what?" — this post is for you. I recently built a Customer Churn Prediction API, and I want to walk you through it the way I wish someone had walked me through my first one: no jargon dump, just what each piece does and why it's there.
Think of it like a Safaricom customer care team trying to figure out which subscribers are about to switch to Airtel — before they actually leave. That's churn prediction. And instead of leaving that insight trapped inside a notebook, I turned it into an API anyone (or any app) can call.
The Problem: A Notebook Is Not a Product
I started, like most people do, in a Jupyter notebook. I had a dataset of telecom customers — their contract type, monthly charges, whether they had streaming services, and so on — with a label saying whether they churned (left) or not.
I cleaned the data, explored it, trained a few models, and picked a winner. Cool. But here's the thing: a trained model sitting in a notebook is useless to anyone else. A frontend developer can't "import your notebook" into an app. A business analyst can't click a button in your .ipynb file to get a prediction.
That's the gap an API fills.
Step 1: From Raw Data to a Trained Model
Before any API talk, the notebook had to do its job. Here's the flow, in plain English:
-
Load the data — a CSV of ~7,000 customers with columns like
gender,tenure,Contract,MonthlyCharges, and the target column,Churn(Yes/No). -
Clean it up — column names get lowercased and stripped of spaces (
TotalChargesbecomestotalcharges), and a numeric column that was secretly stored as text gets converted properly. A handful of broken rows get dropped. - Explore it (EDA) — check the churn rate, look at unique values in each category, plot the class balance. This is where you catch surprises before they wreck your model.
- Split the data — into training and testing sets, so you can honestly check how well the model generalizes instead of just memorizing.
-
Preprocess — numeric columns (like
tenureandMonthlyCharges) get scaled, and categorical columns (likeContractorPaymentMethod) get one-hot encoded, because models only understand numbers. - Train and compare models — I tried Logistic Regression, K-Nearest Neighbors, and a Random Forest, then compared their classification reports.
-
Save the winner — using
joblib, I saved the entire fitted pipeline (preprocessing + model, bundled together) plus the label encoder, so predictions later come back as "Yes"/"No" instead of1/0.
That last step is the bridge to everything that follows. Once your pipeline is saved to disk, it stops being "notebook code" and becomes a reusable artifact — a file any Python program can load and use.
Step 2: What Even Is an API, in Plain Terms?
An API is just a way for two programs to talk to each other over the web, using a set of agreed-upon rules. You send it something, it sends something back.
For this project, the deal is simple:
"Send me a customer's details, and I'll tell you whether they're likely to churn."
That's it. No magic. Just a structured request in, a structured response out.
Step 3: Enter FastAPI
I used FastAPI, a Python framework built specifically for creating APIs quickly, with automatic validation and interactive documentation baked in.
Here's the skeleton of what's happening in main.py:
import joblib
from fastapi import FastAPI
from pydantic import BaseModel
import pandas as pd
# Load the model and target labels
labels = joblib.load('../models/target_labels.joblib')
model = joblib.load('../models/ml_pipeline.joblib')
app = FastAPI(
title="Customer Churn Prediction API",
description="An API for predicting customer churn using a pre-trained machine learning model."
)
Two things happen right at the top, before the API even starts handling requests:
- The trained pipeline (
ml_pipeline.joblib) is loaded into memory. This is the exact same preprocessing + model combo we saved from the notebook. - The label encoder (
target_labels.joblib) is loaded too, so we can turn a0/1prediction back into a human-readable "No"/"Yes".
This only happens once, when the server starts — not on every request. That's important for speed.
Step 4: Defining What "Valid Input" Looks Like
This is the part that impressed me most as a beginner. FastAPI uses something called Pydantic to define exactly what shape your input data must be:
class InputData(BaseModel):
gender: str
seniorcitizen: int
partner: str
dependents: str
tenure: int
phoneservice: str
multiplelines: str
internetservice: str
onlinesecurity: str
onlinebackup: str
deviceprotection: str
techsupport: str
streamingtv: str
streamingmovies: str
contract: str
paperlessbilling: str
paymentmethod: str
monthlycharges: float
totalcharges: float
Every field here mirrors a column the model was trained on. If someone sends tenure as "twelve" instead of 12, FastAPI rejects the request automatically — before it ever reaches your model. You don't write a single line of manual validation code. That alone saved me from a bunch of silent bugs.
Step 5: The Actual Endpoints
An API exposes "endpoints" — specific URLs that do specific things. This project has two.
A health check, so you (or a monitoring tool) can confirm the API is alive:
@app.get("/health", tags=["Health Check"])
def health_check():
return {"status": "API is running"}
The actual prediction endpoint:
@app.post("/predict", tags=["Prediction"])
def predict_churn(payload: InputData):
# Convert input data to DataFrame
input_df = pd.DataFrame([payload.model_dump()])
# Make prediction
prediction = model.predict(input_df)
# Decode the prediction
decoded_prediction = labels.inverse_transform(prediction)
return {"prediction": decoded_prediction[0]}
Walking through it:
- FastAPI already validated and parsed the incoming JSON into
payload. -
payload.model_dump()turns it into a plain dictionary, which gets wrapped into a one-row pandas DataFrame — because that's the format the trained pipeline expects. -
model.predict(...)runs that row through the exact same preprocessing steps used during training (scaling, one-hot encoding), then through the classifier. - The result comes back as a number (
0or1), solabels.inverse_transform(...)converts it back into"No"or"Yes". - We return it as JSON.
That's the whole loop: JSON in → DataFrame → pipeline → prediction → JSON out.
Step 6: Trying It Out
Once you run:
uvicorn main:app --reload
FastAPI gives you a free interactive docs page at http://127.0.0.1:8000/docs, where you can literally fill in a form and hit "Execute" to test /predict without writing a single line of client code. As a beginner, this is where it finally clicked for me — seeing my model respond to a request in real time made it feel like a real product, not just a school notebook exercise.
Send it something like:
{
"gender": "Female",
"seniorcitizen": 0,
"partner": "Yes",
"dependents": "No",
"tenure": 1,
"phoneservice": "No",
"multiplelines": "No phone service",
"internetservice": "DSL",
"onlinesecurity": "No",
"onlinebackup": "Yes",
"deviceprotection": "No",
"techsupport": "No",
"streamingtv": "No",
"streamingmovies": "No",
"contract": "Month-to-month",
"paperlessbilling": "Yes",
"paymentmethod": "Electronic check",
"monthlycharges": 29.85,
"totalcharges": 29.85
}
And you get back:
{"prediction": "No"}
The Lesson That Stuck With Me
Training a model answers the question "does this work?" Building an API answers a completely different question: "can anyone else use this?"
Those are two separate skills, and honestly, the second one felt more like real software engineering — dependency management, input validation, documentation, deployment thinking — than the modeling part did. If you've been putting off learning how to serve your models, don't. It's a smaller leap than it looks, and FastAPI in particular makes it a genuinely beginner-friendly one.
If you're working through something similar — maybe predicting M-Pesa transaction fraud, or matatu route demand — the pattern is identical: train, save the pipeline, load it in an API, validate input, predict, respond. Once you've done it once, you'll do it in your sleep.
Have you built your first ML API yet? What tripped you up the most — validation, deployment, or something else entirely? Let me know in the comments.
Top comments (0)