This content originally appeared on DEV Community and was authored by Apoorv Tripathi
Introduction
Ever wondered how to bridge the gap between your ML experiments and real-world applications? I used to spend days, perfecting machine learning models, only to face the harsh reality that production deployment is a completely different beast.
I recently completed a customer churn prediction project that demonstrates the full ML lifecycle - from initial data exploration in Jupyter notebooks to a production-ready FastAPI service that can handle real customer data efficiently. This journey taught me that your ML model is only as good as the infrastructure that serves it.
The Challenge: From Notebook to Production
The typical ML workflow looks something like this:
- Research Phase: Data exploration, feature engineering, model training in Jupyter
- Validation Phase: Cross-validation, hyperparameter tuning, model selection
- Production Gap: ??? (This is where most of my projects used to fail)
The missing piece is the production infrastructure - the API layer, data validation, error handling, and scalability considerations that make your model actually usable in the real world.
What Makes This Project Special?
π§ Custom Pipeline Architecture
The foundation of any production ML system is a robust, reproducible pipeline. I built a scikit-learn pipeline with custom transformers that encapsulate domain-specific feature engineering:
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
class durationTransform(BaseEstimator, TransformerMixin):
def fit(self, x, y=None):
return self
def transform(self, x):
#Handle both DataFrame and numpy array inputs
if isinstance(x, pd.DataFrame):
db = x.copy()
else:
db = pd.DataFrame(x, columns=["transactiondate", "membershipexpiredate"])
#Calculate subscription duration in days
db["transactiondate"] = pd.todatetime(db["transactiondate"])
db["membershipexpiredate"] = pd.todatetime(db["membershipexpiredate"])
result = (db["membershipexpiredate"] - db["transactiondate"]).dt.days
return result.values.reshape(-1, 1)
#Build the complete pipeline
genencoding = ColumnTransformer([
("gender", OneHotEncoder(), [1])
], remainder='passthrough')
substime = ColumnTransformer([
("durationindays", durationTransform(), [8, 9])
], remainder='passthrough')
pipe = Pipeline([
('genencoding', genencoding),
('substime', substime)
])
Why This Matters: Custom transformers ensure that the same feature engineering logic is applied consistently during training and inference, preventing data leakage and ensuring reproducibility.
π Handling Imbalanced Data
Customer churn datasets are notoriously imbalanced - you typically have 10-15% churners vs 85-90% loyal customers. This imbalance can severely impact model performance:
from sklearn.utils import resample
Original data distribution
zeros = dbtrain[dbtrain['ischurn'] == 0] 9,354 non-churners
ones = dbtrain[dbtrain['ischurn'] == 1] 646 churners
#Undersampling to balance the dataset
zerosundersampled = resample(zeros,
replace=False,
nsamples=len(ones),
randomstate=42)
#Combine and shuffle
dbtrain = pd.concat([zerosundersampled, ones])
dbtrain = dbtrain.sample(frac=1, randomstate=42).resetindex(drop=True)
The Result: Balanced dataset with 646 churners vs 646 non-churners, leading to more reliable model performance metrics.
π Production API with FastAPI
The API layer is where most ML projects fail. I built a comprehensive FastAPI service with multiple endpoints:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
from datetime import date
app = FastAPI(title="Customer Churn Prediction",
description="Production-ready ML API for customer churn prediction",
version='1.0.0')
#Pydantic model for data validation
class dataval(BaseModel):
userid: Optional[int] = None
city: int
gender: str
registeredvia: int
paymentmethodid: int
paymentplandays: int
actualamountpaid: int
isautorenew: int
transactiondate: date
membershipexpiredate: date
@app.post("/predict")
def predict(
city: int,
gender: str,
registeredvia: int,
paymentmethodid: int,
paymentplandays: int,
actualamountpaid: int,
isautorenew: int,
transactiondate: date,
membershipexpiredate: date,
userid: Optional[int] = None
):
# Validate input data
data = dataval(
userid=userid,
city=city,
gender=gender,
registeredvia=registeredvia,
paymentmethodid=paymentmethodid,
paymentplandays=paymentplandays,
actualamountpaid=actualamountpaid,
isautorenew=isautorenew,
transactiondate=transactiondate,
membershipexpiredate=membershipexpiredate
)
# Generate user ID if not provided
user = validuser(data.userid)
Transform data through pipeline
pipedata = [[
data.city, data.gender, data.registeredvia,
data.paymentmethodid, data.paymentplandays,
data.actualamountpaid, data.isautorenew,
data.transactiondate, data.membershipexpiredate
]]
try:
transformed = pipe.transform(pipedata)
dftransformed = pd.DataFrame(transformed,
columns=["durationofsubscription", "female", "male", "city",
"registeredvia", "paymentmethodid", "paymentplandays",
"actualamountpaid", "isautorenew"])
# Make prediction
prediction = model.predict(dftransformed)
result = {user: dftransformed.iloc[0].todict()}
result[user]["prediction"] = int(prediction[0])
# Store result
saveprediction(result)
return result
except Exception as e:
raise HTTPException(statuscode=500,
detail=f"Prediction failed: {str(e)}")
Key Features:
Automatic API Documentation: FastAPI generates interactive docs at /docs
Type Validation: Pydantic ensures data integrity
Error Handling: Graceful degradation with informative error messages
User Management: Automatic ID generation and data persistence
πΎ Persistent Storage with User Management
Production systems need to track predictions and user data:
import json
import os
def validuser(user: int):
"""Generate or validate user IDs with persistent storage"""
if pd.isna(user):
with open("data/users.json", "r") as f:
data = json.load(f)
maxuser = max(data)
user = maxuser + 1
data.append(int(user))
with open("data/users.json", "w") as f:
json.dump(data, f, indent=2)
return user
else:
with open("data/users.json", "r") as f:
data = json.load(f)
if user not in data:
data.append(int(user))
with open("data/users.json", "w") as f:
json.dump(data, f, indent=2)
return user
def saveprediction(result: dict):
"""Persist prediction results with user data"""
jsonpath = "data/userdata.json"
if os.path.exists(jsonpath):
with open(jsonpath, "r") as f:
jsonfile = json.load(f)
else:
jsonfile = {}
jsonfile.update(result)
with open(jsonpath, "w") as f:
json.dump(jsonfile, f, indent=2)
π Model Serialization with Cloudpickle
Traditional pickle often fails with complex ML pipelines. Cloudpickle handles custom transformers and complex objects:
import cloudpickle
Save the trained model and pipeline
with open("model/model.pickle", "wb") as f:
cloudpickle.dump(adaboostmodel, f)
with open("model/pipe.pickle", "wb") as f:
cloudpickle.dump(pipe, f)
Load in production
with open("model/model.pickle", "rb") as f:
model = cloudpickle.load(f)
with open("model/pipe.pickle", "rb") as f:
pipe = cloudpickle.load(f)
The Complete System Architecture
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Jupyter β β FastAPI β β Production β
β Notebook βββββΆβ Service βββββΆβ Deployment β
β β β β β β
β β’ Data EDA β β β’ REST API β β β’ Load Balancer β
β β’ Model Trainingβ β β’ Validation β β β’ Auto-scaling β
β β’ Pipeline Dev β β β’ Error Handlingβ β β’ Monitoring β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Custom β β Pydantic β β JSON Storage β
β Transformers β β Models β β & User Mgmt β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
Performance Results
The production system achieved impressive results:
Model Accuracy Production Status
AdaBoost 89.08% β
Production
Random Forest 87.39% β
Backup
Decision Tree 88.24% β
Interpretable
Voting Classifier 82.35% β
Ensemble
Key Lessons Learned
1. Infrastructure Matters More Than You Think
Your ML model is only as good as the infrastructure that serves it. A 95% accurate model is worthless if it crashes in production.
2. Data Validation is Non-Negotiable
Pydantic models saved me countless hours of debugging by catching data issues early.
3. Custom Transformers Are Game-Changers
They encapsulate domain knowledge and ensure consistency between training and inference.
4. User Management is Critical
Production systems need to track predictions, manage user data, and handle GDPR compliance.
5. Error Handling Makes the Difference
Graceful degradation and informative error messages are essential for production reliability.
Technical Stack
Backend: FastAPI, Python 3.8+
ML: scikit-learn, pandas, numpy
Validation: Pydantic
Serialization: cloudpickle
Storage: JSON files (can be upgraded to database)
Development: Jupyter Notebooks
Conclusion
Building a production-ready ML API requires more than just a good model. It requires:
- Robust Infrastructure: Proper API design, error handling, and scalability
- Data Integrity: Validation, transformation, and persistence
- User Experience: Clear documentation, helpful error messages, and efficient processing
- Business Logic: User management, audit trails, and compliance considerations
The key insight? Your ML model is only as good as the infrastructure that serves it. By combining proper data preprocessing, model serialization, and RESTful API design, I created a system that can handle real customer data efficiently and reliably.
This project demonstrates that the gap between research and production isn't insurmountable - it just requires thinking beyond the model and building a complete system that serves real users.
What's your experience with taking ML models from research to production? Share your challenges and solutions in the comments below!
This content originally appeared on DEV Community and was authored by Apoorv Tripathi

Apoorv Tripathi | Sciencx (2025-07-09T02:23:36+00:00) From Research to Production: How I Built a Customer Churn Prediction API That Actually Works. Retrieved from https://www.scien.cx/2025/07/09/from-research-to-production-how-i-built-a-customer-churn-prediction-api-that-actually-works/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.