MLOps & Model Deployment

The half of ML that isn't modeling: serving (batch vs online), the deploy pipeline and model registry, shipping safely (shadow/canary/rollback), monitoring data & concept drift, training-serving skew, and retraining triggers.

mlopsdeploymentmonitoringdriftservingmodel-registry

Starting from Zero — A Physical Intuition

Before looking at deployment graphs, let's understand MLOps through a physical factory scanner analogy:

  • The Conveyor Scanner (Serving): Imagine you have a physical scanner at a bottle factory checking for caps. Batch Serving is taking all bottles produced overnight, putting them in a separate room, and scanning them at 8:00 AM in one large bin. Online Serving is scanning each bottle individually as it rolls down the active conveyor belt, giving a pass/fail output in milliseconds.
  • The Factory Blueprint Registry (Model Registry): If a scanner breaks, you don't guess how to fix it. You look up the model registry—a secure folder containing the exact blueprint of the scanner hardware, the software version, and the training calibrations. Reverting to a previous scanner version is a simple copy-paste of the blueprint.
  • The Input Quality Alarm (Data Drift): Imagine the factory changes the color of the bottles from clear to green. The scanner continues to output predictions, but because the input bottles look different (data distribution shift), it starts failing silently. The Population Stability Index (PSI) is a sensor that alerts the manager: "The color distribution of bottles today is completely different from the training baseline!" This triggers a recalibration (retraining) before defective bottles leave the factory.

The half of ML that isn't modeling

Training a good model is maybe 10% of the work. The other 90% is getting it to users and keeping it healthy: serving it at low latency, versioning it, shipping new versions without breaking anything, and noticing when the world shifts out from under it. A model is the only kind of software that silently gets worse after you ship it — not because the code changed, but because reality did. MLOps is the discipline that handles that.

Serving: how the model reaches users

  • Batch / offline — score a whole table on a schedule (nightly churn scores, recommendations). Simple, high-throughput, latency doesn't matter.
  • Online / real-time — the model sits behind an API and answers per request (fraud check at checkout). Latency is a hard requirement; you cache, batch requests, and right-size hardware (GPU vs CPU).
  • Streaming — score events as they arrive off a queue.

In every case the model is a versioned artifact (weights + preprocessing) behind an interface — treat it like a deployable, not a notebook.

The deployment pipeline

The model registry versions every model with its data, code and metrics, so any deploy is reproducible and any bad release is one rollback away. Reproducibility in ML means pinning data + code + model, not just code.

Shipping safely

  • Shadow — run the new model alongside the old on real traffic but don't serve its output; compare predictions offline. Zero user risk.
  • Canary / A/B — route a small % of traffic to the new model, watch the metrics, ramp up if healthy.
  • Rollback — because the model is a versioned artifact, reverting is instant.

Monitoring & drift

You can watch latency and error rate like any service — but the model-specific risk is drift: the live data drifting away from what the model trained on.

  • Data drift — the input distribution shifts (a feature's range moves). Detect it without labels by comparing live vs training distributions, e.g. with the Population Stability Index (PSI).
  • Concept drift — the relationship between inputs and the target changes (fraud patterns evolve), so accuracy decays even if inputs look the same.
  • The catch: labels are delayed (you learn the true outcome days later), so input-distribution monitoring is your early-warning system.

Here's PSI in action: a monitored feature quietly drifts right window by window; the score climbs through the warning band and trips the retrain threshold — the signal to act, long before you'd have ground-truth labels:

Model drift — PSI crosses the retrain linetime per-windowspace O(bins)
<0
0–1
1–2
2–3
3–4
>4
reference (training)live (window t+0)
PSI (drift score)0.000 · healthy
00.1 warn0.25 retrain

1/7Window t+0: the live distribution still matches training. PSI = 0.000 < 0.1 — model is healthy, no action.

window = t+0PSI = 0.000status = healthy

Training-serving skew

The most common production ML bug: a feature is computed one way in training and a different way in serving (different code paths, time zones, a fix applied to one pipeline but not the other). The model then sees inputs at serving time it never really trained on, and accuracy quietly tanks. The fix is a feature store (or shared feature code) so the exact same transformation runs in both places.

Retraining

Decide what triggers a refresh:

  • Scheduled — retrain nightly/weekly (simple, may retrain when unnecessary).
  • Drift-triggered — retrain when PSI / a monitor crosses a threshold (efficient, needs solid monitoring).
  • Performance-triggered — retrain when measured accuracy drops (best, but needs timely labels).

Experiment Tracking with MLflow

To prevent "it worked in my notebook" syndrome, production MLOps relies on experiment tracking systems (like MLflow or Weights & Biases) to log every run's hyperparameters, training metrics, and code states, and register the model artifact.

Python
import mlflow
import mlflow.sklearn
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

# Set experiment name
mlflow.set_experiment("fraud-detection-model")

# Start run tracking
with mlflow.start_run():
    # Define hyperparameters
    n_estimators = 100
    max_depth = 5
    
    # Log parameters
    mlflow.log_param("n_estimators", n_estimators)
    mlflow.log_param("max_depth", max_depth)
    
    # Train model
    # (Assuming X_train, y_train, X_test, y_test are loaded)
    clf = RandomForestClassifier(n_estimators=n_estimators, max_depth=max_depth)
    clf.fit(X_train, y_train)
    
    # Log metrics
    y_pred = clf.predict(X_test)
    acc = accuracy_score(y_test, y_pred)
    mlflow.log_metric("accuracy", acc)
    
    # Log and register the model in the registry
    mlflow.sklearn.log_model(
        sk_model=clf,
        artifact_path="model",
        registered_model_name="RandomForestFraudModel"
    )

Model Containerization (Docker + FastAPI)

To deploy models consistently across staging and production, we package the model artifact and serving code into a container. Below is a standard Dockerfile and serving API using FastAPI:

Python
# app.py (Serving API)
from fastapi import FastAPI
import joblib
import numpy as np

app = FastAPI()
# Load model from registry / disk
model = joblib.load("model.joblib")

@app.post("/predict")
def predict(features: list[float]):
    # Expects features as list of float values
    prediction = model.predict([np.array(features)])
    return {"prediction": int(prediction[0])}
# Dockerfile
FROM python:3.10-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy model artifact and serving script
COPY model.joblib .
COPY app.py .

# Expose API port
EXPOSE 8000

# Run FastAPI app with Uvicorn
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

CI/CD for ML (GitHub Actions)

Continuous Integration in ML ensures that changes to training data, config files, or preprocessing code do not degrade performance. Below is a GitHub Actions workflow that automatically triggers on pull requests to run unit tests and check model performance regression:

name: ML CI/CD Pipeline

on: [push, pull_request]

jobs:
  test_model:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.10"

      - name: Install dependencies
        run: |
          pip install -r requirements.txt

      - name: Validate dataset schemas
        run: |
          pytest tests/test_data_validation.py

      - name: Evaluate model performance threshold
        run: |
          # Fails the build if accuracy falls below 85%
          python scripts/evaluate_regression.py --threshold 0.85

Model Cards

A Model Card is a structured markdown document that serves as the "public label" of an ML model, providing transparency into its intent, performance, limitations, and biases.

# Model Card: Fraud Detection Random Forest (v1.2.0)

## Overview
- **Developer**: Risk Platform Team
- **Model Type**: Random Forest Classifier
- **Release Date**: June 2026

## Intended Use
- **Primary Use**: Predicting credit card transaction fraud in real-time.
- **Out of Scope**: Credit score evaluations or long-term lending risks.

## Training Data & Methodology
- Trained on 1.2M anonymized transactions (Q1 2026 data).
- Features include: transaction amount, category, user location frequency, distance from home.

## Metrics & Performance
- **Accuracy**: 94.2%
- **Precision (Fraud class)**: 91.5% (minimizes false positive blocks)
- **Recall (Fraud class)**: 88.0% (captures true fraud events)

## Ethical Considerations & Limitations
- **Limitations**: The model lacks context on global traveling events, leading to higher false-positives during holidays.
- **Biases**: Tends to flag transactions from IP addresses in newly registered server pools; regular checks are run to ensure geographic fairness.

Common Interview Questions

Drift Detection in Code

Here is a complete, library-free Python implementation of a Population Stability Index (PSI) calculator to detect feature distribution drift between training (baseline) and serving (live) datasets:

Python
import math
from typing import List, Tuple

def calculate_psi(expected: List[float], actual: List[float], num_buckets: int = 10) -> Tuple[float, str]:
    """
    Calculate the Population Stability Index (PSI) between two 1D datasets.
    
    Args:
        expected: Baseline/training feature values.
        actual: Production/serving feature values.
        num_buckets: Number of quantiles to split the data.
        
    Returns:
        A tuple of (psi_value, drift_level).
    """
    # 1. Sort the baseline data to determine bucket boundaries (quantiles)
    sorted_expected = sorted(expected)
    n_expected = len(expected)
    n_actual = len(actual)
    
    if n_expected == 0 or n_actual == 0:
        raise ValueError("Inputs cannot be empty.")
        
    # Get split boundaries based on equal-frequency quantiles from expected distribution
    boundaries = []
    for i in range(1, num_buckets):
        idx = int(i * n_expected / num_buckets)
        boundaries.append(sorted_expected[idx])
        
    # Helper to count frequencies in each bucket
    def get_bucket_counts(data: List[float]) -> List[int]:
        counts = [0] * num_buckets
        for val in data:
            placed = False
            for bucket_idx, boundary in enumerate(boundaries):
                if val <= boundary:
                    counts[bucket_idx] += 1
                    placed = True
                    break
            if not placed:
                counts[-1] += 1
        return counts

    expected_counts = get_bucket_counts(expected)
    actual_counts = get_bucket_counts(actual)
    
    # 2. Calculate PSI
    psi_val = 0.0
    for e_count, a_count in zip(expected_counts, actual_counts):
        # Convert to percentages (probabilities)
        e_pct = e_count / n_expected
        a_pct = a_count / n_actual
        
        # Handle zero counts using a tiny epsilon to prevent division-by-zero/log-zero errors
        if e_pct == 0:
            e_pct = 0.0001
        if a_pct == 0:
            a_pct = 0.0001
            
        psi_val += (a_pct - e_pct) * math.log(a_pct / e_pct)
        
    # 3. Classify drift level
    # PSI < 0.1: No change / Stable
    # 0.1 <= PSI < 0.25: Slight change / Warning
    # PSI >= 0.25: Significant change / Action required
    if psi_val < 0.1:
        status = "Healthy (No significant distribution shift)"
    elif psi_val < 0.25:
        status = "Warning (Moderate distribution shift detected)"
    else:
        status = "Alert (Significant distribution shift detected - Action Required)"
        
    return psi_val, status

# Example usage with mock data
if __name__ == "__main__":
    # expected: centered around 0
    # actual: shifted slightly to the right (simulating drift)
    expected_data = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0] * 10
    actual_data = [-1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] * 10

    psi, level = calculate_psi(expected_data, actual_data, num_buckets=5)
    print(f"Calculated PSI: {psi:.4f}")
    print(f"Drift Level: {level}")

Think it through like the interview

Don't just trigger retraining on a schedule — derive the correct diagnosis when system performance begins to degrade.

Think it through: Diagnosing Data Drift vs. Concept DriftMLOps Architecture0/3 stages

PROBLEMYour production credit-risk model's accuracy drops by 8% over six months. Design a diagnostic workflow to determine whether this is data drift or concept drift, and outline the proper fix.

  1. 1

    Isolate the symptom without labels

    True loan default labels take months to realize, but you need to know *now* why predictions look different. What is the first statistic you check on the incoming production features?

  2. 2

    Distinguish between Data and Concept Drift

    Suppose you find that the PSI for 'yearly income' is 0.32, but the model's mapping of income to default rate remains accurate. How do you distinguish if this is a simple shift in candidate demographics vs a change in how creditworthy high-income earners are?

    unlocks after the stage above
  3. 3

    Formulate the remediation strategy

    You confirm Concept Drift is occurring due to changing macroeconomic conditions. What steps should you take to safely redeploy the model?

    unlocks after the stage above

Interactive Quiz

Check yourself0/3 answered

1.

2.

3.


Practice

  1. Drift monitor: Run the custom Python PSI drift detection code provided above. Modify the code to calculate the PSI for a feature with 10 buckets instead of 5 and check how the sensitivity changes.
  2. Rollout plan: write the shadow → canary → full rollout steps for a new fraud model, naming the metric you'd watch and the rollback trigger at each stage.
  3. Skew hunt: take a feature defined in a training query and re-implement it in serving code — list three ways they could silently diverge.
  4. Retraining policy: for a recommendation model whose labels (clicks) arrive within minutes vs a credit model whose labels (defaults) take months, design a different retraining trigger for each and justify it.

This completes Level 11 — AI & Machine Learning, end to end: from the math through modeling, transformers, LLMs, RAG, agents, evaluation, and operating it all in production.