FALSE POSITIVE ALERT
features/transaction_features.py · skew monitor
B
BYTECOP
CONCEPT · SKEW
New here? Start with the basics.

Imagine a recipe developed in a test kitchen. The chef weighs every ingredient on a digital scale, times each step to the second, tastes and adjusts before writing anything down. The finished recipe gets typed up and sent to a restaurant.

At the restaurant, during a Friday dinner rush, nobody has time to weigh flour on a scale. The line cook eyeballs a scoop, glances at a timer instead of watching it, and moves on to the next ticket. They're following the same recipe. On paper, it's identical. In practice, it isn't the same dish.

That's what happens when a machine learning model's inputs, called features, get computed one way while the model is being built, and a different way once the model is actually answering real requests. Same feature name. Same intention. Different arithmetic underneath.

"Average transaction amount, last 30 days" sounds like one well-defined number. But define what happens when a customer has no transactions yet, and you've quietly defined two different features that just happen to share a name.

training:  avg_amount = df['amount'].fillna(df['amount'].mean())
serving:   avg_amount = transaction_cache.get(user_id, default=0.0)

Same column name. Same intention. A missing value quietly becomes the dataset's average in training, and exactly zero the moment the model runs in production.

TRAINING DATA
Historical records used to teach the model a pattern.
FEATURES (OFFLINE)
Raw data turned into the numbers the model actually learns from.
MODEL
Learns a mapping from those numbers to an outcome.
LIVE REQUEST
A real event, no history file, needs an answer in milliseconds.
FEATURES (ONLINE)
The same-named features, recomputed under real-time constraints.
Concept · going deeper

Training-Serving Skew

So far, that's the pipeline working as intended: the same features feed the model whether it's learning or answering. This concept starts one step later, at the moment "the same features" turns out to mean two different pieces of code.

A feature is a name plus a computation, not a name plus a meaning. The model never sees "average transaction amount." It sees whatever number lands in that slot. It has no way to check that the number was produced the same way it was during training. If two different computations produce the number under the same label, the model can't tell the difference, because there isn't one from where it's sitting.

Training and serving are naturally different code paths. Training runs offline, in a batch job, with patient access to the full historical dataset and nobody watching the clock. Serving runs online, has to answer in milliseconds, often can't afford a slow database join, and is frequently written by a different team, sometimes in a different language entirely. Nothing forces those two pipelines to agree on what "missing" means, or how to fill it in.

The divergence is invisible to normal testing. The training pipeline gets tested against training data. The serving pipeline gets tested against serving inputs. Both pass. Neither test ever runs both pipelines side by side on the same real-world input to check whether they agree, because that's not what either test was written to do.

The rule: a feature isn't defined by its name. It's defined by the code that computes it.

The trap: the model trains fine, validates fine, and looks great on every training-set metric, because training never runs the serving code at all. Skew doesn't show up as a bad accuracy number during development. It shows up later, quietly, on requests that never touched the training pipeline in the first place.

Training-Serving Skew: the same feature name, computed two different ways Training fills a missing value with the dataset mean. Serving defaults the same missing value to zero. Both produce a number under the same feature name, but they disagree. Training (offline batch) missing values filled with the dataset mean new customer, no history avg_amount = $612.40 (dataset mean) Serving (online, real-time) missing values default to zero same new customer avg_amount = $0.00 (default) Same feature name. Same customer. Two different numbers, and the model can't tell which one it's looking at.
training-time value  ·  serving-time value

Don't take our word for it. Watch the same customer get two different numbers.

Training vs. Serving, Side by Side

Pick a customer. Both pipelines compute the same named feature from the same underlying record. Watch whether they agree.

Customer
Training · offline batch
avg_amount = df['amount'] .fillna(dataset_mean)
avg_transaction_amount_30d $0.00
Serving · online, real-time
avg_amount = cache.get( user_id, default=0.0)
avg_transaction_amount_30d $0.00
WHAT THE MODEL DOES WITH IT

Select Customer B and see the two pipelines disagree on the exact same feature to unlock the problem stage.

featurestransaction_features.py
transaction_features.py
tap a line to flag it

Flag the line that returns the wrong default for a customer with no transaction history, then propose a fix.

Hints