Feature Engineering skill for Codex

Features that only know the past. Codex writes down the prediction contract (entity, prediction time, available data) before building features, then uses as-of joins and windows that close before the prediction moment. It picks encodings by cardinality, keeps fitted encoders in the model pipeline, measures each feature group's gain against seed noise, and checks served values against offline recomputation.

  • AI & LLM Engineering
  • $feature-engineering
  • 169 lines
  • 2 files
  • ~1,800 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- feature-engineering

Installs to ~/.agents/skills/feature-engineering/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $feature-engineering, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Designs and builds ML features that are correct at prediction time - point-in-time joins with merge_asof and as-of SQL, windowed aggregates, categorical encodings (one-hot, ordinal, target, hashing), date and text features, missing-value signals, train/serve parity, and feature stores such as Feast for offline training sets and online serving. Use when asked to create features for a model, fix training-serving skew, build a training dataset from event tables, choose an encoding for high-cardinality categories, or set up a feature store.

Use it when

  • Turning event or warehouse tables into a model training set
  • Training-serving skew or leakage is suspected
  • Choosing encodings for high-cardinality categorical columns
  • Introducing a feature store such as Feast

Use something else when

How to check the result

Recompute features offline for a sample of served requests and get zero mismatches, and each kept feature group improves validation by more than the seed spread.

The mistake it prevents

Joining today's snapshot table onto historical labels, so training rows see values that did not exist at prediction time.

~/.agents/skills/feature-engineering/SKILL.md

--- name: feature-engineering description: Designs and builds ML features that are correct at prediction time - point-in-time joins with merge_asof and as-of SQL, windowed aggregates,… license: MIT ---

Done well means: every feature value in the training set is exactly what the system could have computed at that row’s prediction time, the same code (or the same definition) produces features for training and serving, and each feature earns its place by a measured gain on validation.

Start with the prediction contract

Write this down before creating any feature:

- Entity: customer_id            - Prediction time: order checkout (event_ts)
- Label: chargeback within 60 days of event_ts
- Data available at prediction time: profile, past orders, past sessions (lag <= 5 min)
- Not available: anything after event_ts, chargeback outcomes newer than 60 days before event_ts

Read AGENTS.md, the existing feature code, warehouse schemas and any feature store config. Note how fresh each source really is in production (batch nightly vs streaming).

Point-in-time correctness

The most common and most expensive bug: joining a snapshot table (current customer state) onto historical training rows. Every join must be “latest value as of the row’s timestamp, minus the source’s latency”.

import pandas as pd

labels = labels.sort_values("event_ts")
balances = balances.sort_values("updated_at")

train = pd.merge_asof(
    labels, balances,
    left_on="event_ts", right_on="updated_at",
    by="customer_id",
    direction="backward",                       # only past values
    tolerance=pd.Timedelta("30D"),              # stale beyond this -> NaN
    allow_exact_matches=False,                  # same-timestamp updates may be post-event
)
-- As-of join in SQL (DuckDB/Snowflake support ASOF JOIN; otherwise use a window)
SELECT l.*, b.balance
FROM labels l
ASOF LEFT JOIN balances b
  ON l.customer_id = b.customer_id AND l.event_ts > b.updated_at;

For aggregates, compute windows that end strictly before the prediction time:

orders = orders.sort_values("order_ts").set_index("order_ts")
roll = (orders.groupby("customer_id")["amount"]
        .rolling("30D", closed="left")          # excludes the current row
        .agg(["sum", "count"]).reset_index())

Feature menu by data type

Raw dataFeatures that usually helpWatch out
NumericRaw value, log1p for heavy tails, ratios (debt/income), clipped valuesTree models do not need scaling; linear models do
Categorical, low cardinality (< 30)One-hotUnseen categories at serving: handle_unknown
Categorical, high cardinality (IDs, zip codes)Target encoding with cross-fitting, frequency/count encoding, hashingPlain target mean on full data leaks the label
TimestampsHour, day of week, month, holiday flag, time since last event, age of accountTime zone of the user, not the server
Event logsCounts, sums and distinct counts over 1h/1d/7d/30d windows, recency, trend (7d vs 30d)Window must close before prediction time
TextLength, keyword flags, TF-IDF, or embeddings ($openai-embeddings)Embedding model versions must match train and serve
Missing valuesMissing indicator columns (missingness is often signal)Imputation statistics fitted on train folds only
GeographyDistance to store, region aggregates, geohash bucketsRegion aggregates computed with the row’s own label leak

Prefer a few strong, explainable features over hundreds of generated ones; add aggregation windows in a grid only when validation shows gains.

Encoding rules

from sklearn.preprocessing import TargetEncoder, OneHotEncoder

# TargetEncoder.fit_transform cross-fits internally (no row sees its own label);
# keep it inside the model Pipeline so CV refits it per fold.
te = TargetEncoder(target_type="binary", smooth="auto", cv=5, random_state=0)
  • Rare categories: group below a minimum count (min_frequency= on OneHotEncoder) into “other” so the model is not fitting noise.
  • Hashing (FeatureHasher) suits huge, open-ended vocabularies; collisions are acceptable noise, but the feature is not interpretable.
  • Keep encoders and imputers in the model pipeline ($scikit-learn-pipelines); the feature layer produces raw, point-in-time-correct columns.

Train/serve parity

ApproachWhen
Same Python function used in batch training and online servingSmall teams, one service
SQL/dbt definitions materialized to offline and online storesWarehouse-centric stacks
Feature store (Feast, Tecton, Databricks, Vertex)Many models share features; online low-latency lookup needed

Feast in brief:

from datetime import timedelta
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64

customer = Entity(name="customer", join_keys=["customer_id"])
stats = FeatureView(
    name="customer_30d_stats", entities=[customer], ttl=timedelta(days=30),
    schema=[Field(name="orders_30d", dtype=Int64), Field(name="spend_30d", dtype=Float32)],
    source=FileSource(path="data/customer_stats.parquet", timestamp_field="event_ts"),
)
feast apply                                        # register definitions
feast materialize-incremental $(date -u +%Y-%m-%dT%H:%M:%S)   # load online store

Training sets come from store.get_historical_features(entity_df=labels, features=[...]) (point-in-time join by event_timestamp), serving from store.get_online_features(...). feast apply and materialization change shared infrastructure: confirm the target environment with the user first.

Measuring a feature’s value

  1. Baseline model on the current feature set, fixed CV splits, 3 seeds.
  2. Add one feature group; rerun; keep it if the gain exceeds the seed/fold spread.
  3. Check permutation importance on validation and ablation (drop the group) for the final set.
  4. Inspect the top features’ definitions for leakage; a suspiciously dominant feature is usually computed with future information.

Skew and drift checks

  • Log served feature values for a sample of requests; compare distributions with the training set (population stability index, KS test, or simple quantile tables).
  • Recompute features offline for logged requests and diff against what was served: any mismatch is a parity bug, not drift.
  • Track null rates per feature in production; a jump usually means an upstream break.

Gotchas

  • groupby().transform("mean") over the whole dataset includes future rows and the row’s own label-correlated values.
  • Data refreshed late in production but on time in the warehouse makes training features fresher than serving features; simulate the real lag in the as-of join.
  • Deleting or backfilling source rows changes historical features; snapshot training sets and record their version.
  • Time zones: compute day-of-week and hour in the user’s local time.

Output contract

| Feature | Definition (as of prediction time) | Source / lag | Gain (val AP) |
|---|---|---|---|
| orders_30d | count of orders in [t-30d, t) | orders, 5 min | +0.021 |
| days_since_signup | t - signup_ts in days | accounts, nightly | +0.008 |
| merchant_te | cross-fitted target encoding of merchant_id | in model pipeline | +0.015 |

Parity check: 1,000 served requests recomputed offline, 0 mismatches.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Feature Engineering"
  short_description: "Features that only know the past"
  default_prompt: "$feature-engineering Build a training set for our fraud model from the orders and payments tables without leakage"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc