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.
.
├── 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.
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.
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.
Before any API talk, the notebook had to do its job. Here's the flow, in plain English:
gender
, tenure
, Contract
, MonthlyCharges
, and the target column, Churn
(Yes/No).TotalCharges
becomes totalcharges
), and a numeric column that was secretly stored as text gets converted properly. A handful of broken rows get dropped.tenure
and MonthlyCharges
) get scaled, and categorical columns (like Contract
or PaymentMethod
) get one-hot encoded, because models only understand numbers.joblib
, I saved the entire fitted pipeline (preprocessing + model, bundled together) plus the label encoder, so predictions later come back as "Yes"/"No" instead of 1
/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.
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.
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
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:
ml_pipeline.joblib
) is loaded into memory. This is the exact same preprocessing + model combo we saved from the notebook.target_labels.joblib
) is loaded too, so we can turn a 0
/1
prediction back into a human-readable "No"/"Yes".This only happens once, when the server starts — not on every request. That's important for speed.
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.
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):
input_df = pd.DataFrame([payload.model_dump()])
prediction = model.predict(input_df)
decoded_prediction = labels.inverse_transform(prediction)
return {"prediction": decoded_prediction[0]}
Walking through it:
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 0
or 1
), so labels.inverse_transform(...)
converts it back into "No"
or "Yes"
.That's the whole loop: JSON in → DataFrame → pipeline → prediction → JSON 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"}
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.