walrusquant/sports-analytic-skills

feature-rules

Define, review, and document point-in-time legal sports-model features.

소스 보기
원본 Skill 문서

원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.

Feature Rules

Outcome

Produce a feature inventory reproducible from information available at the declared prediction decision time T. Every feature needs its source, grain, availability rule, transform, lookback, shift, null policy, legality verdict, and evidence. Both raw inputs and every transformation must be legal at T.

When to Use This Skill

Use when:

  • designing or reviewing predictors for a pre-event sports model;
  • building rolling form, rest, matchup, rating, roster, injury, or context features;
  • the user asks whether a column is legal at decision time T;
  • turning EDA findings into a documented feature inventory.

Do not use this skill as a substitute for:

  • formal leakage verdict after features exist → leakage-audit;
  • rolling/EWMA implementation detail only → time-series-sports;
  • Elo/strength systems as the primary object → ratings-strength-models.
NeedGo instead
Leakage auditleakage-audit
Form feature recipestime-series-sports
Ratings detailratings-strength-models

Prediction contract and legality test

Before construction, write the target, row grain, exact T (kickoff, lineup lock, first pitch, or instant before an in-game event), source publication and revision policy, eligible population, and historical reconstruction rule. “Game day” is too vague when lineups, injuries, weather, or prices change.

Ask of every historical value:

Could an analyst following the declared data policy compute this exact value from information published at or before T?
FindingAction
source and transform known by Tlegal candidate
source arrives with stable delayshift by delay; document it
only part is knownencode only known portion
availability unknownREVIEW REQUIRED; never pass
post-T source or transformillegal; drop or redefine T

Read the legality matrix while classifying feature families, the shift patterns while implementing temporal transforms, and the feature-card template while documenting the final inventory.

Workflow

  1. Declare target, grain, T, population, and publication policy.
  2. Inventory sources, stable IDs, event time, publication time, and revisions.
  3. Classify every source as known, delayed, conditional, unknown, or post-T.
  4. Specify transforms in plain language before coding.
  5. Construct history with explicit sort, shift, and lookback or as-of join.
  6. Construct opponent/context features from already time-safe values.
  7. Define early-history, missingness, staleness, and offseason behavior.
  8. Fit preprocessing, selection, encoding, and reduction inside each fold.
  9. Preview values/null rates and test boundary rows manually.
  10. Issue verdicts and remediate failures before modeling.

Feature-family decision table

FeaturePre-event statusRequired evidence
Home/venueusually legalschedule version known by T
Prior formlegal if shiftedevent ordering and finalization rule
Rest daysusually legalprior event and schedule snapshot
Rating differencelegal if updated after prior eventsupdate trace and initialization
Opponent strengthlegal if opponent value is pre-eventvalidated as-of opponent join
Injury/availabilityconditionalhistorical publication timestamp
Expected lineupconditionalsnapshot at/before T
Market priceconditionalquote timestamp and cutoff
Weatherconditionalforecast vintage, not final observation
Current score/statsillegal pre-eventtarget-only or redefine T
Final-season aggregateillegal for earlier rowsexpanding prior value
Full-data target encodingillegaltraining-fold fit only

Construction patterns

Shift, then aggregate

python
panel = panel.sort_values(["team", "event_time", "game_id"])
g = panel.groupby("team", sort=False)["won"]
panel["pre_win_pct"] = g.transform(
    lambda s: s.shift(1).expanding(min_periods=1).mean()
)
panel["roll5_win_pct"] = g.transform(
    lambda s: s.shift(1).rolling(5, min_periods=1).mean()
)
# Wrong: current label enters current feature.
panel["leaky_roll5"] = g.transform(lambda s: s.rolling(5).mean())

Make tie-break ordering explicit. Input row order is not a chronology.

Publication-time as-of joins

python
left = events.sort_values(["decision_time", "team"])
right = ratings.sort_values(["published_at", "team"])
joined = pd.merge_asof(
    left, right, left_on="decision_time", right_on="published_at",
    by="team", direction="backward", allow_exact_matches=True,
)
assert joined["published_at"].le(joined["decision_time"]).all()

Event date alone is insufficient. Never backfill a missing historical record with a later publication; define maximum staleness where old data should expire.

Opponent joins and differentials

Build team priors first, then join the opponent's already-safe same-event row.

python
opp = team_features[["game_id", "team", "pre_win_pct", "rating"]].rename(
    columns={"team": "opponent", "pre_win_pct": "opp_pre_win_pct",
             "rating": "opp_rating"}
)
feat = team_features.merge(opp, on=["game_id", "opponent"], validate="one_to_one")
feat["win_pct_diff"] = feat["pre_win_pct"] - feat["opp_pre_win_pct"]
feat["rating_diff"] = feat["rating"] - feat["opp_rating"]

Assert distinct entities, one-to-one cardinality, shared game identity, and no current outcome fields on either side.

Rest, minimum history, and priors

python
panel = panel.sort_values(["team", "event_time", "game_id"])
panel["rest_days"] = (
    panel.groupby("team")["event_time"].diff().dt.total_seconds() / 86400
)
panel["pre_games_played"] = panel.groupby("team").cumcount()
model_df = panel.loc[
    panel["pre_games_played"].ge(3)
    & panel["opp_pre_games_played"].ge(3)
].copy()

First events, offseason gaps, doubleheaders, postponements, and same-day events need explicit rules. Choose missing, league/hierarchical prior, carryover, or exclusion before evaluation. Never use zero silently when it has meaning.

Fold-fitted transformations

Imputation, scaling, categorical and target encoding, supervised selection, PCA, and embeddings are feature construction. Fit them on each chronological training fold only, then transform that fold's test rows. A shifted source can still leak through a full-data scaler or selector.

Diagnostics and invariants

  • First entity event has no empirical prior unless a documented prior supplies it.
  • Changing future outcomes cannot change earlier feature rows.
  • Shifted form equals a hand calculation from strictly prior rows.
  • Opponent features equal the opponent's same-event pre-event values.
  • Every as-of record has published_at <= T and acceptable staleness.
  • Current target and aliases are absent from candidate features.
  • Preprocessing is newly fit per training fold.
  • Null rates and eligibility match the early-history specification.

Use a future-perturbation test: alter outcomes after a cutoff, rebuild, and assert all feature values at or before the cutoff remain unchanged.

Anti-patterns

Anti-patternFailureRepair
roll without shiftcurrent label includedshift first
final-season value on early eventfuture bleeds backwardexpanding/as-of
display-name joinsduplicate/wrong opponentstable IDs + cardinality assertion
fill on unsorted rowsaccidental chronologysort and assert monotonicity
backfill historical gapslater values move backwardprior/missing/stale rule
full-data preprocessingtest distribution leaksfold-local fit
current participation as availabilityoften known post-Ttimestamped snapshot/drop
zero-filled early forminvented historyexplicit prior/threshold
shifted but late-published sourcesource remains illegalpublication-time policy

Standalone helpers

The helpers read user-owned CSV, Parquet, JSON, JSONL, or NDJSON and require pandas. Candidate features are explicit; there is no hidden feature list.

Build a portable team-game feature table

From a doubled team-game panel (exactly two rows per game_id), build shifted pre-game form features and opponent differentials without freehand pandas:

bash
python /path/to/feature-rules/scripts/build_team_game_features.py \
  --input team_games.parquet \
  --out features.csv \
  --manifest-out feature_manifest.json

Defaults expect columns game_id, team, opponent, is_home, won, point_diff, and sortable gameday (plus season when present). Generated modeling defaults:

  • is_home
  • feature_win_pct_diff
  • feature_diff_diff
  • feature_rest_diff (when dates parse)

All form fields use shift(1) before aggregation. Opponent values come from the opponent's own pre-event row joined on game_id. Current outcomes remain labels only. This does not replace leakage-audit.

Hand off next:

bash
python /path/to/leakage-audit/scripts/audit_pregame_features.py \
  --input features.csv --target won \
  --features is_home,feature_win_pct_diff,feature_diff_diff \
  --entity-col team --time-col gameday --out leakage.json

python /path/to/baseline-models/scripts/run_baselines.py \
  --input features.csv --target won --split-col season \
  --features is_home,feature_win_pct_diff,feature_diff_diff \
  --min-train-groups 1 \
  --out baseline-folds.json \
  --predictions-out baseline-predictions.csv

Preview and catalog helpers

bash
python /path/to/feature-rules/scripts/feature_preview.py \
  --input features.csv --features pre_win_rate,rest_days,rating_diff \
  --context season,event_id,team --rows 12

python /path/to/feature-rules/scripts/legality_report.py \
  --input feature_catalog.csv --feature-col feature \
  --available-at-col availability --out feature_legality.json

The legality input is a catalog with one row per feature—not a modeling matrix. It requires a unique feature-name column and a timing classification column. Canonical classifications are known_by_t, delayed, conditional, unknown, and post_t; common pregame/pre-decision spelling variants are normalized. Use --features only to select named catalog rows and --banned to extend the exact forbidden-name list. Exact forbidden names or post_t fields return ILLEGAL and status 2. Duplicate names, missing/unrecognized classifications, and unresolved timing return REVIEW REQUIRED and status 1. Even an otherwise passing catalog remains REVIEW REQUIRED, because strings cannot prove timestamps, sorting, joins, shifts, or fold-fitting. LEGAL is issued only after the manual evidence review below.

Minimum catalog:

csv
feature,availability
pre_win_rate,known_by_t
rest_days,known_by_t
projected_lineup,conditional

Feature card and output contract

text
Feature name/group and purpose:
Target / row grain / decision time T:
Raw source, version, source grain, and join keys:
Event time / publication time / revision policy:
Transform; sort, shift, lookback, minimum periods:
Opponent/as-of join rule:
Missing, staleness, offseason, and early-history policy:
Fold-fitted components:
Verdict: LEGAL | ILLEGAL | REVIEW REQUIRED
Evidence/tests and required remediation:
Used by models / owner / date:

Inventory every model input, not just engineered columns.

Worked example and integrity rules

For pre-game team form: sort by entity/time/stable event ID; shift outcomes; compute expanding and rolling form; define prior or three-game threshold; join the opponent's shifted row; derive differences; hand-check first and midseason rows; perturb future outcomes; run both helpers; then complete leakage-audit.

  1. Declare T before construction; unknown availability is not a pass.
  2. A shift does not repair a post-T source.
  3. Join by stable IDs with asserted cardinality and time bounds.
  4. Make null, prior, staleness, and minimum-history behavior explicit.
  5. Drop illegal features or redefine the problem; do not merely note them.
같은 저장소의 Skills

더 많은 Skills

모든 Skills