The Model Did Not Change. Accuracy Collapsed.
Training accuracy
97%
Production accuracy
61%
The cause was one line: training lowercased every comment; production did not. “IDIOT” became an unknown or different token from “idiot.” The classifier received a representation it had never learned to interpret.
Why can one missing preprocessing step destroy an excellent model?
Because the model never sees raw comments. It sees the output of a feature pipeline. Change that pipeline and you change the model’s effective input space.
Retrieval Warm-Up
- Why must a feature exist at prediction time?
- Which lesson artifact tells us what “toxicity” means?
- Why can a balanced annotation set distort deployment estimates?
Check answers after retrieving
Future information leaks; the versioned label policy; and balanced sampling changes class prevalence. If these were slow, revisit Data Collection and Labels before continuing.
Learning objectives
- Separate raw observations from model features
- Eliminate training-serving skew
- Design offline, online, batch, and streaming features
- Compute point-in-time-correct stateful features
- Understand feature stores and lineage
- Validate schemas, values, and distributions
Feature Pipeline Failure Lab
Inspect every representation
Output
"YOU ARE SUCH AN IDIOT!!! 😂😂"
Schema
str
Python
event.text
Break production on purpose
Training remains fixed at 97%.
Production accuracy
97%
Feature validation dashboard
Shift production comment length away from training.
Schema
pass
Missing language
0.7%
Length drift
pass
Unknown tokens
3.2%
One Comment, Seven Representations
Start with "YOU ARE SUCH AN IDIOT!!! 😂😂". Unicode normalisation makes equivalent characters canonical. Whitespace cleanup removes accidental layout variation. Case folding produces "you are such an idiot!!! 😂😂". Language detection yields en. Tokenisation maps text to stable token IDs. An encoder maps those IDs to a fixed-dimensional vector.
Every intermediate value needs a schema and version. Emoji removal would discard intensity. Unversioned language detection could route the same comment differently after an upgrade. A tokenizer vocabulary change can silently reinterpret every integer.
The Model Sees Features, Not Reality
For raw observation , a versioned pipeline computes:
The model only receives . Offline training produces feature distribution ; online serving produces . If implementations differ, then:
This is training-serving skew: a distribution shift created by the system itself.
Offline And Online Must Share A Definition
Offline training
historical events → batch computation → feature table → training
Optimised for throughput, backfills, and point-in-time joins.
Online serving
live event → feature service → feature vector → prediction
Optimised for low latency, freshness, and availability.
The storage and execution engines may differ. The feature definition, schema, defaults, tokenizer, windows, and versions must not. Prefer one shared transformation library or declarative feature definition over copied notebook and service code.
Point-In-Time Correctness Prevents Leakage
12:00:06 first user report
12:03:20 moderator review
14:40:00 appeal filed
At 12:00:00, report count, moderator action, and appeal outcome do not exist. Joining them into the training row teaches from the future. Offline generation must ask: what value would the online system have returned at that exact timestamp?
A legal feature might be the author’s prior violations before 12:00:00. The same author’s moderation decision at 12:03:20 is illegal for this prediction.
Stateful Features Need Window Semantics
“Toxic comments in the last 30 days” requires an entity key, event-time clock, window boundaries, lateness policy, update frequency, and default. Define it precisely:
The strict excludes the current and future event. Online state may update continuously; offline backfills must reproduce the same boundary and late-event rules. UTC avoids region-dependent windows.
What A Feature Store Solves
├── offline computation → historical store → training export
└── streaming computation → online store → low-latency lookup
└── lineage + freshness + schema
A feature store is a coordination layer, not magic. It centralises definitions, materialises historical and fresh values, supports point-in-time joins, and exposes online lookup. Tools such as Feast embody this pattern, but the design matters more than a particular tool.
It does not automatically prevent a logically leaky feature or guarantee the offline and online implementations are semantically identical; tests and validation still do that.
Validate Before The Model Receives Anything
Schema
Required names, types, shapes, and version.
Missingness
Null rate by feature and slice; explicit defaults.
Ranges
Comment length ≥ 0; probability in [0,1].
Categories
Known languages plus a deliberate unknown bucket.
Freshness
User-history timestamp is recent enough for the SLA.
Statistical drift
Training and production histograms remain comparable.
One Deterministic Pipeline For Both Paths
from dataclasses import dataclass
import unicodedata
@dataclass(frozen=True)
class FeatureConfig:
version: str = "tox-features-v4"
tokenizer_version: str = "toxic-2026-04"
unknown_language: str = "und"
def normalise(text: str) -> str:
text = unicodedata.normalize("NFKC", text)
return " ".join(text.split()).casefold()
def transform(event, tokenizer, language_detector, config=FeatureConfig()):
text = normalise(event["text"])
language = language_detector(text) or config.unknown_language
token_ids = tokenizer.encode(text)
return {"token_ids": token_ids, "language": language,
"char_count": len(text), "feature_version": config.version}
# Training and serving import this exact function.
assert transform(sample, tok, detect) == transform(sample, tok, detect)Miniature timestamped store
class FeatureStore:
def __init__(self): self.values = {}
def write(self, entity, feature, event_time, value):
self.values.setdefault((entity, feature), []).append((event_time, value))
def lookup(self, entity, feature, as_of):
valid = [(t, v) for t, v in self.values.get((entity, feature), []) if t <= as_of]
return max(valid)[1] if valid else 0
def offline_export(self, entities, feature, timestamps):
return [self.lookup(e, feature, t) for e, t in zip(entities, timestamps)]Failure Catalogue
Capitalised words map to different tokens.
Identical text becomes different token IDs.
Vector dimensions or semantic space change.
A renamed or retyped field breaks consumers.
Thirty-day windows include different events.
Training imputes ‘unknown’; serving emits null.
Category IDs move between runs.
Two copies drift after one is updated.
Interview Discussion
Can offline and online systems use different technologies?
Yes, if they share versioned semantics and parity tests. Identical infrastructure is not required; identical outputs are.
How would you detect skew?
Log online feature vectors, recompute them offline for the same event and timestamp, compare field-by-field, then monitor distribution and missingness.
Why is a feature store insufficient by itself?
It serves definitions and values; it cannot decide whether a definition leaks or encodes the wrong product concept.
Apply And Review
- For one comment, name every representation before prediction.
- Explain training-serving skew without using the word “different.”
- Decide whether “reports in the next hour” is legal and justify using the event timeline.
- Specify a 30-day user feature with entity, clock, boundary, and default.
- Design three parity tests and three production validations.
- In one day, retrieve skew, point-in-time correctness, and feature lineage; in three days, reproduce the stateful-feature formula without notes.
What Comes Next?
Models rarely fail because matrix multiplication changes. They fail because inputs quietly change. With reproducible inputs, the next systems question is how another engineer can recreate the entire trained model six months from now.