Exercises — Week 6 — Features Are the Model's API¶
What you are building¶
Two scaler fits compared across a split, a missingness flag, and assert_score_payload — a contract test, not another transformer.
Predict before you run¶
- If a feature is built from behavior seven days after
as_of, what happens to offline AUC vs production AUC? - Will the all-rows scaler mean for
mrrdiffer much from the train-only mean on an 8k sample? - Does
has_usagesplit churn even iftotal_usageis already in the matrix?
Before you start¶
- Keys and labels never go in the payload: no
user_id, nochurn_date.
Each task has three hints, closed by default. Open only as far as you need.
Task¶
Work in starter.py. Run from the repo root:
0. Predict first. Suppose someone adds a feature built from customer behavior recorded seven days after the prediction timestamp (as_of). Before touching any code: what happens to offline holdout AUC — better, worse, unchanged? What happens to production AUC once that feature is scored on customers who have not lived those seven days yet? Write both guesses, then read the "time machine" picture in Week 8 to check the second one.
1. Two scaler fits. Fit a scaler on all rows, then only on train. Print the two means used for mrr. How far apart are they?
Hint 1 — a nudge
The test set stands in for customers you will score next month. Which of the two scalers has already "seen" them?
Hint 2 — the approach
Split first (train_test_split, stratified), then fit one StandardScaler on all of X and one on X_train only. scaler.mean_ holds the number each one would subtract. The size of the gap on an 8k sample is one question; whether the habit is acceptable is a separate one.
Hint 3 — most of the code
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from pipelines.features import NUMERIC, build_features
df = build_features(as_of="2024-06-01")
X, y = df[NUMERIC], df["is_churned"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
all_rows = StandardScaler().fit(X)
train_only = StandardScaler().fit(X_train)
i = NUMERIC.index("mrr")
print(f"mrr mean all rows={all_rows.mean_[i]:.3f} train only={train_only.mean_[i]:.3f}")
2. Missingness flag. Add has_usage = total_usage > 0. Does churn differ? Would you keep the flag and the count?
Hint 1 — a nudge
build_features fills "no usage rows" with 0. Is a customer with zero usage the same story as a customer with a little usage?
Hint 2 — the approach
Add the boolean column, then groupby("has_usage")["is_churned"].agg(["count", "mean"]). Keep both columns only if zero vs some tells a different story than the count already does.
Hint 3 — most of the code
3. Contract test. Write assert_score_payload(payload: dict) that checks required keys and types. This is more production-shaped than another transformer.
Hint 1 — a nudge
What does the service on the other side of /predict rely on? List it — then treat anything else as an error, not a bonus.
Hint 2 — the approach
A dict of key → allowed types. Check missing keys, unknown keys, then each type. Prove it with two bad payloads — one missing a key, one with a string where a number belongs — each wrapped in try / except.
Hint 3 — most of the code
NUM = (int, float)
REQUIRED = {"mrr": NUM, "tenure_so_far": NUM, "log_usage": NUM, "plan_type": (str,)}
def assert_score_payload(payload: dict) -> None:
missing = REQUIRED.keys() - payload.keys()
extra = payload.keys() - REQUIRED.keys()
if missing or extra:
raise ValueError(f"missing={sorted(missing)} extra={sorted(extra)}")
# check each value against REQUIRED[key]; raise TypeError on a mismatch
good = {"mrr": 49.0, "tenure_so_far": 120, "log_usage": 3.2, "plan_type": "pro"}
assert_score_payload(good)
Success criteria¶
- Two scaler means printed.
- Churn rates with/without usage.
- Contract test rejects a missing key and a wrong type.
After you run¶
Features are the model's public API. Week 15's validate() is this function with a worse mood.