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.
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.
Setup
# 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.
# 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")