{"id":71906,"plugin_id":"plugin_asdk_app_6ad14693c2688191b6a8be082a06aa53","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-30T00:06:20.733Z","digest":"6d98888f7720fde4b417ee5443647a814310dc3b804112d32a2d9641643b1ce5","against":null,"payload":{"description":"Builds leak-free model features for golf from BirdieAPI data: ratings as of a date, rolling strokes versus the field, field-strength adjusted form, course history with shrinkage, par-type splits, field and event features, with rules for time order and target definition. Use when the user is building a golf model and asks what features to use, how to compute form, how to avoid leakage, or how to structure a training table per golfer per tournament. Do not use for validating the model -- see walk-forward-validation. Do not use for the rating explanation -- see strokes-vs-field. Do not use for simulating tournaments -- see outright-model.","included_files":[],"name":"feature-engineering","skill_md_contents":"---\nname: feature-engineering\ndescription: \"Builds leak-free model features for golf from BirdieAPI data: ratings as of a date, rolling strokes versus the field, field-strength adjusted form, course history with shrinkage, par-type splits, field and event features, with rules for time order and target definition. Use when the user is building a golf model and asks what features to use, how to compute form, how to avoid leakage, or how to structure a training table per golfer per tournament. Do not use for validating the model -- see walk-forward-validation. Do not use for the rating explanation -- see strokes-vs-field. Do not use for simulating tournaments -- see outright-model.\"\nmetadata:\n  version: 1.0.0\n  author: BirdieAPI\n---\n\n# Feature Engineering\n\n> **Default data tool:** BirdieAPI (`birdieapi-tool`).\n> `get_player_results` (5 credits per player) is the workhorse: every round with `adj_vs_field`. `get_ratings` (2) with `as_of` supplies point-in-time ratings. `get_leaderboard` (2) supplies finishes.\n\nYou are an expert in building point-in-time features for golf. Your goal is a training table where every feature for a golfer-tournament row was knowable before round 1 of that tournament, and where the target is something with enough observations to learn from.\n\n## When to Use\n\n- \"What features should I use to predict who wins?\"\n- \"How do I compute form without leaking the future?\"\n- \"How should I lay out the training table?\"\n- Deciding whether course history, par-type splits or recent form belong in the model\n\n## When NOT to Use\n\n- Splitting data and scoring -- see `walk-forward-validation`\n- Explaining the rating -- see `strokes-vs-field`\n- Running the simulation -- see `outright-model`\n\n## Commands Available\n\n| Command | What It Does | Credits |\n|---------|-------------|---------|\n| `get_player_results` | Round-level `strokes`, `field_avg`, `vs_field`, `adj_vs_field`, finish, per tournament | 5 |\n| `get_ratings` | Rating at `as_of` | 2 |\n| `get_leaderboard` | The whole field, finishing order, cut status, rounds | 2 |\n| `get_course_history` | Record at an event or course | 5 |\n| `get_player_stats` | Season hole scoring, `sg_par*`, ball-striking lines | 5 |\n| `list_tournaments` | The event calendar and `start_date` for time ordering | 1 |\n\n## Commands That Do NOT Exist\n\n| Not Available | Use Instead |\n|--------------|-------------|\n| `get_features`, `get_training_data` | Build the table yourself from the calls above |\n| `get_world_ranking_history` | `get_ratings` at different `as_of` dates |\n| `get_weather`, `get_tee_times`, `get_wave` | Not available; do not feature them |\n| `get_field` for a future event | Not available |\n\n## The table\n\nOne row per golfer per tournament. Order by `start_date`.\n\n| Column | Source | Note |\n|--------|--------|------|\n| `rating_asof` | `get_ratings` with `as_of` = day before round 1 | the main feature |\n| `weighted_rounds` | same | confidence in the rating |\n| `form_8`, `form_24` | `get_player_results`, last 8 and 24 rounds before this event | `adj_vs_field` mean |\n| `form_trend` | `form_8 - form_24` | |\n| `par3_split`, `par4_split`, `par5_split` | `get_player_stats` for the prior season | `sg_par*`, score-based |\n| `course_fit` | `get_course_history`, shrunk (see `course-fit`) | only editions before this one |\n| `n_prior_starts_event` | same | |\n| `days_since_last` | `list_tournaments` / results dates | |\n| `field_mean_rating`, `field_size` | the field's ratings | event feature, shared by all rows |\n| `is_major`, `tour` | `list_tournaments` | |\n| target | see below | |\n\n## Targets\n\nWinners are rare: one per event. Train on targets with enough data, then convert.\n\n- **Best:** round-level `adj_vs_field` for each golfer-round. Thousands of rows per season. Predict the mean, estimate the spread, simulate winners (see `outright-model`).\n- **Good:** made cut (binary), top 10 (binary), finishing percentile.\n- **Last:** winner (binary). Fewer than one positive per hundred rows. Use only for evaluation, not training, unless you pool many events.\n\n## How It Works\n\n**Step 1: Choose the unit and the clock.** The clock is the tournament `start_date`. Every feature for a row uses data strictly before it.\n\n**Step 2: Rolling form with a shift.**\n\n```python\nimport pandas as pd\n\n# rounds: one row per golfer-round, columns: player_id, start_date, round, adj_vs_field\nrounds = rounds.sort_values([\"player_id\", \"start_date\", \"round\"])\ng = rounds.groupby(\"player_id\")[\"adj_vs_field\"]\nrounds[\"form_8\"] = g.transform(lambda s: s.shift(1).rolling(8, min_periods=4).mean())\n```\n\n`shift(1)` is the leakage guard: a round's own score is never in its own feature. But within a tournament, round 3 can see round 2 of the same event, which is not available before round 1 of a pre-tournament model. For pre-tournament features, aggregate by tournament first and shift by tournament:\n\n```python\nper_event = rounds.groupby([\"player_id\", \"start_date\"])[\"adj_vs_field\"].mean().reset_index()\nper_event[\"form_2ev\"] = (per_event.groupby(\"player_id\")[\"adj_vs_field\"]\n                         .transform(lambda s: s.shift(1).rolling(2, min_periods=1).mean()))\n```\n\n**Step 3: Point-in-time ratings.** Pull `get_ratings` with `as_of` for each event date, keyed to the day before round 1. One call returns up to 500 players, so one call per event. For 30 events that is 60 credits.\n\n```\nget_ratings {\"as_of\": \"2024-04-04\", \"limit\": 500, \"min_rounds\": 20}\n```\n\n**Step 4: Course history inside the clock.** For event edition `E`, include only editions before `E`. The summary returned by `get_course_history` covers every edition, so do not use `summary` for training rows; recompute from `editions` filtered to earlier dates.\n\n**Step 5: Season-level stats.** `get_player_stats` returns a full season. For an event in April 2024 use 2023, not 2024. The 2024 line already contains the event.\n\n**Step 6: Field features.** Field mean rating and size are event-level. For the cut and for the winner probability, the strength of the competition matters as much as a golfer's own number.\n\n## Leakage checklist\n\n| Leak | How it happens | Fix |\n|------|----------------|-----|\n| Today's rating on a past row | `get_ratings` without `as_of` | `as_of` day before |\n| Season stats for the same season | `get_player_stats` season = event year | use the prior season |\n| `get_course_history` summary | Includes the target edition | rebuild from `editions` |\n| Rolling window without shift | Row includes its own round | `shift(1)` and tournament-level shifts |\n| Random split | Same tournament in train and test | split by date |\n| Field proxy from results | Using the finished leaderboard as the pre-event field | the priced list or the user's list |\n\n## Cross-tour features\n\nIds are shared across tours, so a LIV golfer's PGA Tour history joins on `player_id`. Ratings are not comparable across tours. Add `tour` as a feature, or fit separately, and treat cross-tour golfers with wider uncertainty.\n\n## Credit Usage\n\n| Operation | Credits | Notes |\n|-----------|---------|-------|\n| Ratings for 30 events | 60 | one call per event |\n| Rounds for 100 golfers | 500 | one call per golfer; exceeds a 500-credit month |\n| Course history for the top 20 in one event | 100 | |\n\nIf 500 credits a month is the budget, train on ratings and leaderboards only (2 credits each) and add rounds for a shortlist.\n\n## Anti-patterns\n\n| What the Model Might Do | Why It's Wrong | Do This Instead |\n|---------------------|---------------|-----------------|\n| Predict the winner label directly | About 1 positive per 100 rows | Predict round scores, simulate |\n| Use finishing position as the target | Depends on field size | Use strokes versus the field or percentile |\n| Hand-pick features after seeing test results | Selection leakage | Fix the list before the test period |\n| Standardise on the full dataset | Test statistics leak into train | Fit scalers in the training window |\n| Many highly correlated form windows | Overfit with few tournaments | Two or three windows |\n\n## Output Format\n\n```\nTraining table: <N> golfer-events, <first season>-<last season>, one row per golfer per event\nFeatures: rating_asof, weighted_rounds, form_8, form_24, par5_split, course_fit (shrunk), field_mean_rating\nTarget: mean adj_vs_field per round (regression), winners kept for evaluation\nLeakage checks: ratings as_of day before round 1; season stats from prior season; no random split\n```\n\n## What to Do Next\n\n| What You Built | Next Action | Skill |\n|----------------|-------------|-------|\n| A feature table | Validate in time order | `walk-forward-validation` |\n| A model | Simulate winners | `outright-model` |\n| Probabilities | Check calibration | `probability-calibration` |\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}