learn-feature-engineering-with-phoebe / Practitioner session 10 of 10
Learn Feature Engineering with Phoebe · Practitioner track · Session 10 of 10

Capstone: the feature spec

Nine sessions of craft, and none of it survives your departure unless it is written down. Tonight you assemble the whole Lumen pipeline into one Pipeline object, then write the artifact that makes it hand-overable: a seven-field spec for every feature, with one field that does more work than the other six combined. Then you defend it. A spec whose every line survives a hostile question is a feature set someone else can own; one that does not is a notebook with ambitions.

🔴 Practitioner track DS · analysts · ML engineers Python · live workbench on key pages 45 min
0-3 · Welcome 3-18 · The spec anatomy 18-42 · Build-along: assemble + defend 42-45 · Q&A
Part 1 · the course synthesis, plus Rules of ML #22

Seven fields, one of which is the whole course 9 min live

A feature spec is not documentation you write afterwards to satisfy someone. It is the shortest form of every decision this course taught you, and each field maps to a session you have already run. Write it for every feature, not just the interesting ones - the boring columns are where the surprises hide.

LiveThe seven fields, in order6 min▶

1 · Name and a one-sentence definition. What it is, in language a person outside the team can read. If the sentence needs a second sentence, the feature is doing two things and should be two features (Rule 19: "Use very specific features when you can").

2 · Source table and column. Where the raw value comes from, exactly. Not "the warehouse" - the table and the column, so a schema change has an owner and a blast radius.

3 · WRITE-TIME relative to the outcome. When is this value written, by which system, relative to the thing you are predicting? This is the leakage field, and it is the single most important line in the document. Everything in b5 exists to make this question askable, and every failure in b5 was a spec that never asked it.

4 · Transformation applied, and where it is fitted. Not just "scaled" - scaled by what, fitted on which rows, inside which pipeline stage. b5 Demo 2 is the proof that a transformation is not safe in the abstract, only in the way it is fitted.

5 · Owner. A name. Not a team, not a squad - a person who answers when the feature breaks at 2am and nobody remembers why it exists.

6 · Refresh cadence. How often the source updates, and therefore how stale the feature can be at serving time. A daily feature serving a real-time model is a defect that produces no error.

7 · Deprecation status. Rule 22: "Clean up features you are no longer using." A feature nobody uses is still a column somebody maintains, still a dependency on a source table, and still a line of risk. Mark it, then remove it.

FEATURE SPEC · one per feature 1 · Name + one-sentence definition 2 · Source table and column 3 · WRITE-TIME relative to the outcome - the leakage gate 4 · Transformation applied, and where it is fitted 5 · Owner (a person, not a team) 6 · Refresh cadence 7 · Deprecation status (Rule 22) THE LEAKAGE GATE "when exactly is this written, and by which system?" Six fields make a feature maintainable. Field 3 is the one that makes it real.
🔍 Click to zoom - the one-page spec, with the field that stops a launch
Part 2 · covers sklearn preprocessing, compose, pitfalls, selection

The whole course, in one Pipeline 10 min live

Here is Lumen, assembled. Every stage below traces to a session you ran, and the ordering is not stylistic: the split comes first because sklearn says "Always split the data into train and test subsets first, particularly before any preprocessing steps," and everything else lives inside the container because "the scikit-learn pipeline is a great way to prevent data leakage as it ensures that the appropriate method is performed on the correct data subset."

LiveThe capstone pipeline7 min▶
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, TargetEncoder
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score

X_train, X_test, y_train, y_test = train_test_split(         # b5 rule 1: FIRST
    X, y, test_size=0.4, stratify=y, random_state=42)

numeric     = ["prior_30d_spend", "pages_viewed", "tenure_days"]   # b4 derived
categorical = ["channel", "product_category", "device", "new_vs_returning"]
high_card   = ["postal_code"]                                # ~1,200 levels

prep = ColumnTransformer([
    ("num", Pipeline([
        ("impute", SimpleImputer(strategy="median",          # b7 imputation
                                 add_indicator=True)),       # b7 missingness
        ("scale",  StandardScaler()),                        # b2 scaling
    ]), numeric),
    ("cat", OneHotEncoder(handle_unknown="ignore",           # b3 encoding
                          min_frequency=0.01), categorical),
    ("hc",  TargetEncoder(), high_card),                     # b3 cross-fitted
])

pipe = Pipeline([                                            # b5 the container
    ("prep",   prep),
    ("select", SelectKBest(f_classif, k=25)),                # b8 selection
    ("model",  LogisticRegression(max_iter=1000)),
])

pipe.fit(X_train, y_train)                                   # b5 rule 2
print(cross_val_score(pipe, X_train, y_train, cv=5).mean())

Walk the mapping out loud, because this is the moment the ten sessions become one object:

  • b2 · scaling. StandardScaler sits after imputation, inside the numeric sub-pipeline, so its mean and variance are learned from training rows only.
  • b3 · encoding. OneHotEncoder(handle_unknown="ignore") for the low-cardinality columns, with min_frequency grouping rare levels; TargetEncoder for postal_code, whose internal cross-fitting is the only reason a 1,200-level column is safe here at all.
  • b4 · datetime. tenure_days and the weekday flags were derived from signup_date upstream and enter as ordinary numeric and categorical columns. Decomposition happens before the pipeline; the pipeline only transforms.
  • b5 · the pipeline itself. Not a convenience. The structure is what makes the wrong thing hard to type, which is worth more than remembering to do the right thing.
  • b7 · imputation. add_indicator=True keeps the missingness as its own feature, because "this was blank" is frequently the signal.
  • b8 · selection. SelectKBest sits inside, so it re-fits within every cross-validation fold. Outside, it sees every row before the split - which is precisely the sklearn experiment that scored 0.76 accuracy on pure noise where the honest answer was 0.50.
Every stage traces to a session, and the ordering is not stylistic train_test_split - first, before anything touches the data b5 rule 1 Pipeline · the container num median impute + indicator · b7 StandardScaler · b2 cat OneHotEncoder, min_frequency handle_unknown = ignore · b3 hc · postal_code TargetEncoder, cross-fitted 1,200 levels · b3 SelectKBest(f_classif, k=25) - inside, so it re-fits within every fold · b8 LogisticRegression(max_iter=1000) - the same estimator you built in b1 not in the pipeline: post_purchase_flag is in no list at all - field 3 doing its job in code Split first, everything else inside the container: the structure makes the wrong thing hard to type. Decomposition happens before the pipeline. The pipeline only transforms.
🔍 Click to zoom - ten sessions collapsed into one object you can hand over
Self-studyWhat is deliberately not in this pipeline3 min read▶
  • The b9 embedding block. Text encoding runs upstream and caches; the vectors join as columns. Add it only when it beats the hand-crafted flags, per Rule 17.
  • Interactions (b6). Lumen's spend-times-returning term is derived before the pipeline for readability. Anything you can name explicitly, name explicitly.
  • The leakage lever. post_purchase_flag is not in numeric, not in categorical, not anywhere. That absence is field 3 doing its job in code.
Part 3 · two checks against the workbench

Verify, then close the lever for good ★ 12 min · everyone runs it

Two last runs. The first reproduces the ladder you have climbed since b1 and then presses the leakage lever one final time, deliberately, so you remember the shape of it. The second is field 4 of the spec, demonstrated: the same transformation, safe or catastrophic depending only on how it was fitted.

Turn on the five honest levers one at a time and watch the held-out ladder rebuild itself: 0.500 → 0.696 → 0.785 → 0.806 → 0.812 → 0.813. Every rung is a session, and the algorithm never changed.

Now press the dashed target leakage lever one last time: train 0.967, held-out 0.789 - still below the honest 0.813. Then switch it off and leave it off forever.

The point of pressing it one final time: the spec's field 3 is what prevents that column ever entering the pipeline in the first place. Not vigilance, not code review, not a good week. A written line stating when the value is recorded.

Second check. Field 4 asks not only what transformation but where it is fitted, and this is why that half of the field exists.

Naive encoding of postal_code, a 1,200-level column drawn independently of the target: train 0.931, held-out 0.493. A column that knows nothing, scoring 0.931.

Cross-fitted, which is what TargetEncoder.fit_transform does internally: train 0.509, held-out 0.493. Honest and boring.

Read the lesson as a spec rule: a transformation is not safe in the abstract, only in the way it is fitted. "TargetEncoder" in field 4 is an incomplete answer; "TargetEncoder, fit_transform inside the pipeline, cross-fitted over 5 folds" is a complete one.

★ The honesty rail, one last time The model, the split, and every AUC on this page are computed live in your browser from embedded Lumen data. Only the data is synthetic. The sklearn and Google numbers quoted throughout the track come from their own documentation and are cited on the page where they appear.
Part 4 · the red-team round

Defend the spec 8 min live

Pair up. One of you reads a feature spec aloud; the other asks the hostile question for each field and does not accept "I would have to check." Every question below has cost somebody a quarter.

The red-team round: six questions, and what a weak answer means FIELD THE HOSTILE QUESTION WHAT A WEAK ANSWER MEANS 3 · Write-time When exactly is this written, and by which system? stop here - nothing downstream matters yet 4 · Transform What did this transform fit on? preprocessing leakage, live and invisible 5 · Owner Who do I call when this feature breaks? unowned means unmaintained, so it drifts 2 · Source What happens when the source table changes? no blast radius and no alerting 7 · Deprecation Which features are we no longer using? feature debt - the list never volunteers itself The split Does the split respect time and entities? b5 families 3 and 4 are still open The most common failure is a confident answer to field 3 that nobody ever tested at the source. Do not accept "I would have to check" - every question here has cost somebody a quarter.
🔍 Click to zoom - field 3 is a gate, not a row: a weak answer stops the review
FieldThe hostile questionWhat a weak answer means
3 · Write-time"When exactly is this written, and by which system?"You cannot rule out target leakage. Stop here; nothing downstream matters yet.
4 · Transformation"What did this transform fit on?"Preprocessing leakage is live and invisible. Move the transform inside the pipeline before answering anything else.
5 · Owner"Who do I call when this feature breaks?"The feature is unowned, which means it is unmaintained, which means it will drift silently.
2 · Source"What happens to this feature when the source table changes?"No blast radius, no alerting. A schema migration will take the model down and nobody will connect the two.
7 · Deprecation"Which features here are we no longer using?"Feature debt, accumulating. Rule 22 exists because this list is never empty and never volunteers itself.
The split"Does the split respect time and entities?"Families 3 and 4 from b5 are open. Repeat customers on both sides is the commercial default, Lumen included.
Real world

The ship gate, in one sentence. A spec whose every line survives these questions is a feature set you can hand over - to a colleague, to a platform team, to whoever inherits this in eighteen months. One that does not is a notebook with ambitions. The most common failure is not a wrong answer; it is a confident answer to field 3 that turns out, when someone finally checks the source system, to have been an assumption nobody ever tested.

Part 5 · self-study

Where this goes next ◐ read after class

Self-studyThe three courses on either side of this one3 min read▶
  • learn-model-evaluation-with-phoebe judges what these features produced: metrics, thresholds, and the cost of being wrong in each direction. This course owns the inputs; that one owns the verdict.
  • learn-intro-ml-with-phoebe is the modelling mechanics underneath - the estimator, the split, the fit. Worth a revisit now that you know what you were feeding it.
  • The leader track (a1-a6) is for whoever funds and reviews this work. a5 covers hand-crafted versus learned features for decision-makers, and a6 is the feature review checklist that the seven-field spec is designed to survive.
Self-studyThe honest frontier2 min read▶

Named, not taught, and you should know they exist:

  • Automated feature engineering - featuretools, autoML feature synthesis. They generate candidate features at volume. They do not answer field 3, which means everything in this course still has to happen afterwards, on a much larger pile.
  • Time-series features - lags, rolling windows, seasonality. That is learn-timeseries-forecasting-with-phoebe.
  • Representation learning - training the encoder as part of your model rather than calling a frozen one. That is learn-deep-learning-with-phoebe; b9 was the bridge, not the destination.
  • Feature stores - the concept and when it pays are in the leader track (a4); implementation and ops belong to the data-engineering bucket.
Homework

Try it yourself - this week ◐ 45-60 min total

Source material

Official sources covered

This capstone synthesizes the whole track. Certificates and graded exercises stay with the official providers (Kaggle Learn, Google MLCC). This page covers:

✓sklearn "Preprocessing data" - scalers, encoders, TargetEncoder in one assembled transformerPart 2 · every b2 and b3 transform in its production position
✓sklearn "Common pitfalls" - split first, never fit on test, use a PipelineParts 2-4 · the three rules as pipeline structure and as spec field 3
✓sklearn "Pipelines and composite estimators" - ColumnTransformer + PipelinePart 2 · the container's stated safety purpose, applied end to end
✓Google "Rules of Machine Learning" - rules 19, 21, 22 as spec fieldsParts 1 and 4 · specificity, weight budget, and cleaning up unused features
✓fe-live workbench canon - the ladder, the leak, the encoding demoPart 3 · 0.500 to 0.813 honest; 0.967/0.789 leaked; 0.931/0.493 naive encoding
◐sklearn "Feature selection" - SelectKBest inside the pipeline, not before itPart 2 · the selection craft itself is b8
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Of the seven spec fields, which one carries the most weight, and why?

The other six make a feature maintainable; field 3 makes it real. A feature written at or after the outcome is target leakage no matter how well it is owned, scaled, or documented - and no amount of cross-validation detects it, because the column is contaminated in every fold.

2 · Putting SelectKBest inside the Pipeline rather than before it prevents what, structurally?

sklearn's own words: pipelines "help avoid leaking statistics from your test data into the trained model in cross-validation, by ensuring that the same samples are used to train the transformers and predictors." Their worked example is the price tag: selection fitted before the split scored 0.76 accuracy on pure noise, where the honest score was 0.50 and cross-validation gave 0.43.

3 · What is the ship gate for a feature set leaving your hands?

Scores and clean runs are exactly what leakage produces, which is why neither can be the gate. The gate is that someone else can own this: every feature has a stated write-time, a stated fitting location, a named owner, and a deprecation status. A spec that survives is hand-overable; one that does not is a notebook with ambitions.

Practitioner session 10 cheat sheet · pin this

The seven fieldsName + definition · source table and column · write-time · transformation and where fitted · owner · refresh cadence · deprecation status.
Field 3 is the gate"When exactly is this written, and by which system?" The only field that disqualifies a feature outright. Cross-validation cannot substitute for it.
Field 4 is half a fieldNot "TargetEncoder" but "TargetEncoder, fit_transform inside the pipeline, cross-fitted." A transform is safe only in how it is fitted.
Pipeline ordersplit → ColumnTransformer (impute + scale · one-hot · target-encode) → SelectKBest → LogisticRegression → cross_val_score.
Stage to sessionb2 scaling · b3 encoding · b4 datetime derived upstream · b5 the container · b7 imputation with add_indicator · b8 selection inside.
The ladder, closedHeld-out 0.500 → 0.696 → 0.785 → 0.806 → 0.812 → 0.813. Leaked: train 0.967, held-out 0.789. Lever off, permanently.
The encoding checkNaive 0.931 train / 0.493 held-out. Cross-fitted 0.509 / 0.493. Same column, same data, different fitting.
The ship gateEvery line survives the hostile question. Hand-overable beats accurate: a score you cannot defend is a launch decision made on fiction.