100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
ML Ops & Data Science in Production
55 minadvanced

Practice — Deploy a Churn Prediction API

What You'll Build

In this hands-on exercise you will build and deploy a complete Churn Prediction API from scratch. The system trains a RandomForest classifier on synthetic cricket player performance data to predict whether an IPL player will be dropped from their squad for the next season. You will create a FastAPI application that exposes a /predict endpoint accepting player statistics such as batting average, strike rate, innings count, and economy rate. The API is containerised with Docker and deployed to Render as a live cloud service. By the end you will have a production URL that teammates, selectors, or CI pipelines can call with a JSON payload and receive an instant retention prediction — a complete MLOps loop from training to serving.

Analogy🏏Cricket
🏏 Think of it like cricket: Before the IPL auction, a franchise's analytics team evaluates every player across dozens of trial matches — tracking Rohit Sharma's strike rate in power plays, Virat Kohli's average against pace, MS Dhoni's finishing rate in the death overs, Shubman Gill's consistency across pitches, and Jasprit Bumrah's economy in the middle overs. Each trial is recorded in a shared logbook so the selectors can compare and pick the best combination. MLflow is exactly that shared logbook for your ML experiments — every training run is a trial match, every metric is a scorecard entry, and the Model Registry is the final squad announcement. Keep the auction framing in mind throughout the exercise, because it fixes the discipline the steps teach: a franchise never signs a player off one good highlight reel, and you never register a model off one lucky run — you log every trial, compare them on identical conditions, and promote only with the full scorecard in front of you.

Prerequisites

Before starting this exercise ensure the following tools are installed on your machine. You need Python 3.9 or higher with pip available in your PATH. Install FastAPI and Uvicorn for the web server, scikit-learn for the RandomForest model, joblib for model serialisation, and the requests library for testing. You also need Docker Desktop (or Docker Engine on Linux) running locally so you can build and run the container. A free Render account at render.com is required for the deployment step, and a GitHub account to host the repository. Confirm Python is working by running python --version and Docker by running docker info before proceeding.

Analogy🏏Cricket
🏏 Think of it like cricket: the kit check before a net session. Just as a batter arriving for practice needs pads, gloves, and a bat they already know how to use — but doesn't need to have faced the new bowling machine before, because today's session is exactly where they'll learn it — this exercise expects you to arrive comfortable with Python, basic scikit-learn (fit, predict, train_test_split), and pandas, while MLflow itself is taught from scratch. Just as the coach insists on a properly prepared practice pitch — a clean, dedicated strip rather than the match square — you need a clean virtual environment or Conda environment where packages can be installed freely. And just as the only outside help needed is the equipment delivery van arriving once before practice, network access is required only for the initial pip install; after that everything runs locally, whether your 'net' is a laptop, a Docker container, or a cloud notebook. The payoff: checking your kit now means the session ahead is pure skill-building, with no stoppages for missing gear.

Setup

bash
# Install all required Python packages
pip install fastapi uvicorn scikit-learn joblib numpy pandas requests

# Create the project directory structure
mkdir ipl-churn-api
cd ipl-churn-api
mkdir -p model tests

# Final structure should look like this:
# ipl-churn-api/
# ├── train_model.py          # Script to train and save the RandomForest model
# ├── main.py                 # FastAPI application
# ├── Dockerfile              # Container definition
# ├── render.yaml             # Render deployment config
# ├── requirements.txt        # Python dependencies
# ├── tests/
# │   └── test_api.py         # Integration test script
# └── model/
#     └── model.pkl           # Saved model (generated by train_model.py)

# Create requirements.txt
cat > requirements.txt << 'EOF'
fastapi==0.111.0
uvicorn[standard]==0.29.0
scikit-learn==1.4.2
joblib==1.4.2
numpy==1.26.4
pandas==2.2.2
requests==2.32.3
EOF

echo "Project structure ready. Run train_model.py next."

Step 1: Train the Churn Prediction Model

The first step is to generate a synthetic dataset of IPL player statistics and train a RandomForest classifier on it. The dataset contains columns representing real cricket performance metrics: batting_average, innings_count, strike_rate, wickets_taken, economy_rate, matches_played, and centuries_scored. The target column dropped indicates whether the player was removed from the squad (1) or retained (0). After generating 800 synthetic player records with realistic value ranges, we split the data, train the classifier, evaluate its accuracy, and serialise the trained model along with its feature scaler to model/model.pkl using joblib. This persisted artefact is what the FastAPI service will load at startup.

Analogy🏏Cricket
🏏 Think of it like cricket: when Mumbai Indians analysts study Rohit Sharma's career data — his batting average across seasons, strike rate in powerplays, and performance in knockout games — they build an intuition for which patterns signal a declining player. Training our RandomForest is like codifying that analyst intuition into a mathematical model: it learns from 800 historical player records which combination of stats reliably predicts squad exclusion, the same way Rohit's selectors learned which warning signs precede a player losing form. The 800 records are the model's equivalent of two decades of selection meetings: each row pairs a player's season numbers with what the committee actually decided, and the forest of decision trees learns splits like 'batting average under 22 and injury count above 2 usually means dropped'. Because the data is synthetic, you control the ground truth — a luxury real selectors never get — which makes it easy to verify the model has learned the intended patterns before you serve it.
python
# train_model.py — Create synthetic IPL player dataset and train the churn model

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report
import joblib
import os

np.random.seed(42)

# ── Generate synthetic IPL player performance dataset ──────────────────────
num_players = 800

# Retained players tend to have higher batting averages and more centuries
batting_average = np.concatenate([
    np.random.normal(35, 10, num_players // 2),   # retained players
    np.random.normal(18, 8,  num_players // 2),   # dropped players
])

innings_count = np.concatenate([
    np.random.randint(60, 120, num_players // 2),
    np.random.randint(10, 55,  num_players // 2),
])

strike_rate = np.concatenate([
    np.random.normal(140, 15, num_players // 2),
    np.random.normal(105, 20, num_players // 2),
])

wickets_taken = np.concatenate([
    np.random.randint(0, 30, num_players // 2),
    np.random.randint(0, 15, num_players // 2),
])

economy_rate = np.concatenate([
    np.random.normal(7.5, 1.2, num_players // 2),   # better economy → retained
    np.random.normal(9.8, 1.5, num_players // 2),
])

matches_played = np.concatenate([
    np.random.randint(50, 200, num_players // 2),
    np.random.randint(5,  45,  num_players // 2),
])

centuries_scored = np.concatenate([
    np.random.randint(2, 20, num_players // 2),
    np.random.randint(0, 3,  num_players // 2),
])

# Target: 0 = retained, 1 = dropped
dropped = np.concatenate([
    np.zeros(num_players // 2, dtype=int),
    np.ones(num_players  // 2, dtype=int),
])

# Shuffle all rows together
shuffled_idx = np.random.permutation(num_players)

ipl_players = pd.DataFrame({
    "batting_average":  batting_average[shuffled_idx].clip(0, 80),
    "innings_count":    innings_count[shuffled_idx],
    "strike_rate":      strike_rate[shuffled_idx].clip(50, 250),
    "wickets_taken":    wickets_taken[shuffled_idx],
    "economy_rate":     economy_rate[shuffled_idx].clip(4, 15),
    "matches_played":   matches_played[shuffled_idx],
    "centuries_scored": centuries_scored[shuffled_idx],
    "dropped":          dropped[shuffled_idx],
})

print(f"Dataset shape: {ipl_players.shape}")
print(f"Dropped rate : {ipl_players['dropped'].mean():.1%}")
print(ipl_players.head(3).to_string())

# ── Split features and target ──────────────────────────────────────────────
feature_cols = [
    "batting_average", "innings_count", "strike_rate",
    "wickets_taken", "economy_rate", "matches_played", "centuries_scored",
]

X = ipl_players[feature_cols].values
y = ipl_players["dropped"].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# ── Build and train pipeline (scaler + RandomForest) ──────────────────────
player_pipeline = Pipeline([
    ("scaler",  StandardScaler()),
    ("rf_model", RandomForestClassifier(
        n_estimators=200,
        max_depth=12,
        min_samples_leaf=4,
        class_weight="balanced",
        random_state=42,
        n_jobs=-1,
    )),
])

player_pipeline.fit(X_train, y_train)

# ── Evaluate ──────────────────────────────────────────────────────────────
rohit_preds = player_pipeline.predict(X_test)
print(f"\nTest accuracy : {accuracy_score(y_test, rohit_preds):.4f}")
print("\nClassification report:")
print(classification_report(y_test, rohit_preds, target_names=["Retained", "Dropped"]))

# ── Persist the model ─────────────────────────────────────────────────────
os.makedirs("model", exist_ok=True)
joblib.dump(player_pipeline, "model/model.pkl")
print("\nModel saved to model/model.pkl")
Lesson 18 of 35
0% complete