Feature Store Basics: Point-in-Time Correctness and Training-Serving Skew
Hands-on lab · IDE in your browser

Feature Store Basics: Point-in-Time Correctness and Training-Serving Skew

Build the core of a feature store on a payments fraud model and learn why the same feature computed two ways silently breaks production: point-in-time feature computation that never peeks at the future, a training set built as of each event, an online store materialized for low-latency lookups, a skew report that compares the training and serving code paths, and a measurement of what training-serving skew costs when it ships.

Time
60 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

The job

Nimbus Pay approves or declines a card transaction the moment it arrives. Its fraud model reads a few features per account: how active the account has been and how large its recent charges are. You build those features the way a feature store must - as of the moment of each decision, never using the future - materialize them for serving, then find and price the training-serving skew that a second code path quietly introduces.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Point-in-time features

    Nimbus Pay decides whether to approve a card transaction the instant it arrives.

  2. 2

    Build the training set

    To train the model you need one row per past transaction: the features as they were just before that transaction, and whether it turned out to be fraud.

  3. 3

    An online store

    Training reads history in a batch.

  4. 4

    Training-serving skew

    Here is where feature stores earn their keep.

  5. 5

    The cost of skew

    Now measure what the skew would have cost if it had shipped.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Point-in-time features

Nimbus Pay decides whether to approve a card transaction the instant it arrives. The fraud model reads a few features about the account: how many transactions in the last week, the average and the largest amount recently. A feature store's first job is to compute those features as of a moment in time, using only what was known then. Use anything that happened later and your offline scores are a fantasy.

transactions.csv is the log: account, ts (a day number), amount, is_fraud. Write two functions in featurestore.py:

  • window(events, end, days): the events with ts in [end - days, end). The upper bound is strict: an event exactly at end is the one you are about to score, so it does not get to describe itself.
  • features_as_of(acct_events, end): return {count_7d, avg_30d, max_7d} for one account, using window over 7 and 30 days. avg_30d is 0 when there is nothing in the window; max_7d is 0 when the 7-day window is empty.

Run it: the features as of day 40 do not move when you add a transaction on day 41.

featurestore.py, the file you edit100 lines
"""Nimbus Pay: one definition of every feature, computed the same way for training and for serving.
Point-in-time correctness, an online store, and the training-serving skew that a second code path creates."""
import csv

import numpy as np
from sklearn.linear_model import LogisticRegression

FEATURE_NAMES = ["count_7d", "avg_30d", "max_7d"]
DEFAULT = {"count_7d": 0, "avg_30d": 0.0, "max_7d": 0.0}


def load(path):
    """Read the transaction log into (account, ts, amount, is_fraud) tuples, oldest first."""
    out = []
    with open(path) as f:
        for r in csv.DictReader(f):
            out.append((int(r["account"]), float(r["ts"]), float(r["amount"]), int(r["is_fraud"])))
    return out


def by_account(txns):
    """Group transactions by account into (ts, amount, is_fraud) lists, oldest first."""
    acc = {}
    for a, t, amt, f in txns:
        acc.setdefault(a, []).append((t, amt, f))
    for a in acc:
        acc[a].sort()
    return acc


def split(txns, frac=0.7):
    """Split chronologically: the earliest frac of transactions train, the rest test."""
    s = sorted(txns, key=lambda r: r[1])
    k = int(len(s) * frac)
    return s[:k], s[k:]


def vectorize(feat, cur_amount):
    """The fixed model input: the transaction amount, the three features, and amount / recent average."""
    return [cur_amount, feat["count_7d"], feat["avg_30d"], feat["max_7d"],
            cur_amount / (feat["avg_30d"] + 1.0)]


def fit_model(X, y):
    """Train the fraud classifier (fixed, so the lab can focus on the features)."""
    return LogisticRegression(max_iter=2000, class_weight="balanced").fit(X, y)


def score(model, X):
    """Fraud probability for each row."""
    return model.predict_proba(X)[:, 1]


def window(events, end, days):
    """The events with ts in [end - days, end): the point-in-time window ending strictly before end."""
    # TODO (Step 1): events with ts in [end - days, end); the upper bound is strict.
    raise NotImplementedError("Step 1: write window()")


def features_as_of(acct_events, end):
    """The three features for one account as of time end, using only earlier transactions."""
    # TODO (Step 1): {count_7d, avg_30d, max_7d} from window over 7 and 30 days; empty windows give 0.
    raise NotImplementedError("Step 1: write features_as_of()")


def build_training_set(by_acct, txns):
    """For every transaction, the features as of its own timestamp (never itself or later) and its label.
    Returns (feats, amounts, labels) as three aligned lists."""
    raise NotImplementedError("build_training_set() arrives in Step 2")


def materialize(by_acct, as_of):
    """The online store: one current feature vector per account, computed once as of a cutoff."""
    raise NotImplementedError("materialize() arrives in Step 3")


def get_online(store, account):
    """A low-latency lookup: the stored vector for an account, or zeros for an unknown one."""
    raise NotImplementedError("get_online() arrives in Step 3")


def serving_features(acct_events, end):
    """The serving path's own feature computation. It must match features_as_of exactly, or the model
    sees different values in production than it trained on."""
    w7 = window(acct_events, end, 30)
    w30 = window(acct_events, end, 30)
    return {"count_7d": len(w7),
            "avg_30d": float(np.mean([a for _, a, _ in w30])) if w30 else 0.0,
            "max_7d": max((a for _, a, _ in w7), default=0.0)}


def skew_report(offline_feats, online_feats):
    """The largest absolute gap between the offline and online value of each feature, over aligned rows.
    A healthy feature store reports ~0 for every feature."""
    raise NotImplementedError("skew_report() arrives in Step 4")


def evaluate(model, feats, amounts, labels, threshold=0.5):
    """Recall of fraud and the share of transactions flagged, for a set of feature vectors."""
    raise NotImplementedError("evaluate() arrives in Step 5")
Provided for you:transactions.csvtry_it.py

Frequently asked questions

What is point-in-time correctness in a feature store?

It means each training example's features are computed using only data available at that example's timestamp, never later events. Without it, features leak information from the future, offline metrics look great, and the model fails in production where the future is unknown.

What is training-serving skew?

Training-serving skew is when a feature is computed differently in the training pipeline than in the serving system, so the model receives values in production it never saw in training. It is silent because every offline test on the training features still passes; a feature store prevents it by sharing one feature definition across both paths.

What is the difference between an offline and an online feature store?

The offline store holds historical features for building training sets in batch. The online store holds the current feature vector per entity, materialized ahead of time for millisecond lookups at serving. Both must be produced from the same feature definitions to stay consistent.

Point-in-time correctness and training-serving skew

A feature store exists to compute a feature once and serve the same value everywhere: in the training set as of each historical event, and at serving time from a low-latency store. Two failures make models that look fine offline fail in production - using future data when building the training set, and computing a feature differently in the serving path than in training. In this lab you build point-in-time features on a payments log, a training set that never peeks ahead, an online store, and a skew report that catches a serving-path bug. Then you measure what the skew would have cost: the same model, fed features computed the wrong way, flags far more payments than it should.