04 · MLOps for Data Scientists¶
A model that scores 0.89 AUC in a notebook and is never deployed reliably delivers 0.00 business value. MLOps is the set of practices that get a model from "works in my notebook" to "runs correctly in production, and someone finds out fast when it stops." This module covers the parts of that pipeline a data scientist (not a dedicated ML engineer) is commonly responsible for.
The lifecycle beyond training¶
Train → Validate → Package → Deploy → Monitor → Retrain → (repeat)
↑
this loop is what "MLOps" actually refers to —
training a good model is the easy 20%
Most data science curricula stop at "Validate." The remaining stages are where models actually fail in practice: a model with excellent offline metrics that's deployed via a manual, undocumented process, monitored not at all, and never retriggered for retraining will silently degrade and no one will notice until a stakeholder complains.
Packaging: making a model reproducibly loadable¶
import joblib
import json
# Save the model AND the exact preprocessing it expects
joblib.dump(model, "model_v3.pkl")
metadata = {
"model_version": "v3",
"trained_on": "2024-06-01",
"feature_order": ["income", "tenure_months", "days_since_last_purchase"],
"sklearn_version": "1.4.2",
"training_metric": {"auc": 0.891},
}
with open("model_v3_metadata.json", "w") as f:
json.dump(metadata, f, indent=2)
# At inference time — fail loudly on mismatch rather than silently
# feeding columns in the wrong order
def load_and_validate(model_path: str, metadata_path: str):
model = joblib.load(model_path)
with open(metadata_path) as f:
meta = json.load(f)
return model, meta
def predict(model, meta, features: dict):
row = [features[col] for col in meta["feature_order"]] # explicit order, not dict order
return model.predict([row])[0]
The feature_order check is the single highest-value line in this
snippet: a model silently fed columns in the wrong order still produces a
prediction — it just produces a wrong one, with no error to signal it.
Serving patterns¶
Batch scoring:
- Run predictions on a schedule (e.g. nightly) over a table of records
- Simple, no latency requirement, easiest to get right first
- Good default when "real-time" isn't actually a requirement
Online/real-time serving:
- A request comes in, a prediction is returned within an SLA (e.g. 100ms)
- Needs the online half of the feature store (Module 03)
- Justified only when the product genuinely needs a live decision
# Minimal real-time serving endpoint (FastAPI)
from fastapi import FastAPI
import joblib
app = FastAPI()
model, meta = load_and_validate("model_v3.pkl", "model_v3_metadata.json")
@app.post("/predict")
def predict_endpoint(features: dict):
try:
pred = predict(model, meta, features)
return {"prediction": float(pred), "model_version": meta["model_version"]}
except KeyError as e:
return {"error": f"missing feature: {e}"}
Defaulting to batch scoring and only moving to real-time serving when a concrete product requirement demands it avoids a large class of unnecessary operational complexity — real-time serving adds a live dependency, an SLA to meet, and a new failure surface that batch scoring doesn't have.
Monitoring: what to watch after deploy¶
import numpy as np
from scipy.stats import ks_2samp
def feature_drift(reference: np.ndarray, current: np.ndarray, alpha: float = 0.01) -> bool:
"""KS test: is the current feature distribution meaningfully different
from what the model was trained on?"""
stat, p_value = ks_2samp(reference, current)
return p_value < alpha
training_income = np.random.normal(60000, 15000, 5000)
production_income_this_week = np.random.normal(63000, 15000, 500)
print(feature_drift(training_income, production_income_this_week))
Monitor at three levels:
1. Operational — latency, error rate, uptime (standard software monitoring)
2. Data — input feature distributions vs. training (drift, above)
3. Model — prediction distribution, and TRUE performance once
labels arrive (may be days/weeks delayed — churn,
fraud, and many labels aren't immediate)
The delayed-label problem is the part most new-to-MLOps data scientists miss: for a churn model, you may not know whether a prediction was correct for 30-90 days. Data-level drift monitoring is what gives an early warning before the delayed ground truth confirms a problem.
Retraining triggers¶
Options, roughly in order of sophistication:
1. Fixed schedule (retrain monthly regardless of signal) — simple, wasteful if nothing changed
2. Performance-triggered (retrain when live metric drops below threshold) — needs fast-arriving labels
3. Drift-triggered (retrain when input distributions shift materially) — works even with delayed labels
4. Manual (a human decides) — fine at low model count, doesn't scale
A schedule-only strategy is a reasonable starting point, but pairing it with a drift-triggered alert (option 3) catches the case where something breaks between scheduled retrains — e.g. an upstream data pipeline change that shifts a feature's distribution overnight.
Cheat sheet¶
| Stage | Data scientist's responsibility |
|---|---|
| Packaging | Save model + exact preprocessing/feature order together |
| Serving choice | Default to batch; justify real-time with a concrete need |
| Operational monitoring | Usually owned by platform/infra, but know what's tracked |
| Data drift monitoring | Often the DS's job — you know what "normal" looks like |
| Retraining trigger | Define explicitly; don't leave it to "someone will notice" |
How It Actually Works¶
Why feature order silently corrupts predictions: a fitted scikit-learn
model stores its learned coefficients (or split thresholds) positionally —
model.coef_[0] is "whatever the first training column was," with no
column-name awareness baked into the fitted object once it's serialized as
a plain array. Feeding [tenure_months, income, days_since_last_purchase]
at inference time when the model was trained on
[income, tenure_months, days_since_last_purchase] doesn't raise any
error — it just multiplies each coefficient against the wrong feature's
value, producing a numerically valid but meaningless prediction. This is
exactly the kind of bug that offline validation can't catch (validation
data uses the same code path, so it's consistently "wrong" in the same
consistent way) and that only shows up as unexplained production
degradation — which is why enforcing feature_order explicitly at
inference, rather than trusting dict ordering, converts a silent
correctness bug into a loud KeyError.
The KS test for drift works by comparing the two samples' empirical
CDFs directly: ks_2samp computes the maximum vertical distance between
the reference distribution's cumulative distribution function and the
current distribution's, D = max|F_ref(x) - F_current(x)|. Under the null
hypothesis that both samples come from the same underlying distribution,
D's sampling distribution is known (it depends only on the two sample
sizes), which is what lets the test convert an observed D into a
p-value without assuming any particular distributional shape for the
feature itself — it's nonparametric, unlike a t-test's normality
assumption, which matters because feature distributions in production
(income, click counts) are routinely skewed.
Why drift monitoring matters more than label-based monitoring for many models: true performance monitoring requires ground-truth labels, and for churn, fraud, or LTV-style targets, the label only exists 30-90 days (or more) after the prediction was made — a genuine reporting lag, not a tooling gap. A model that starts silently misfiring on day 1 due to an upstream schema change won't show up in a labeled performance metric until day 30+, but a KS test on the affected feature's distribution can flag the shift within the same monitoring cycle it happens in — drift is a leading indicator, label-based performance is a lagging one, and production monitoring needs both because drift without label deterioration can also be a false alarm (a benign shift the model happens to be robust to).
Exercise¶
Take a model from an earlier module. Write the metadata.json it should
ship with (feature order, training date, key metric). Then write a single
feature_drift check for its most important input feature, and decide:
weekly, monthly, or on-drift-alert — which retraining trigger fits this
specific model's label latency, and why?