← AcesAPICONTENT HISTORY
WHAT CHANGED
Update to AcesAPI
Snapshot Oct 31, 2026 · 15:03 UTC · version 1.0.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save AcesAPI and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Building leak-free features for tennis match prediction from AcesAPI data: pre-match Elo (overall and surface), rating gaps, rolling form, days of rest and recent load, head-to-head with a time guard, surface and format flags, and a symmetric pair layout. Use when the user is building a model, asks what features to use, or suspects leakage. Do not use for model fitting or tuning: see walk-forward-validation. Do not use for calibration: see probability-calibration. Do not use for quick lookups: see match-lookup.",
"included_files": [],
"name": "feature-engineering",
"skill_md_contents": "---\nname: feature-engineering\ndescription: \"Building leak-free features for tennis match prediction from AcesAPI data: pre-match Elo (overall and surface), rating gaps, rolling form, days of rest and recent load, head-to-head with a time guard, surface and format flags, and a symmetric pair layout. Use when the user is building a model, asks what features to use, or suspects leakage. Do not use for model fitting or tuning: see walk-forward-validation. Do not use for calibration: see probability-calibration. Do not use for quick lookups: see match-lookup.\"\nmetadata:\n version: 1.0.0\n author: AcesAPI\n---\n\n# Feature Engineering\n\n> **Default data tool:** AcesAPI (`acesapi-tool`).\n> Bulk results: `get_matches` by `season` (2 credits per 100 matches, use `offset`). Per-match pre-match ratings: `get_match` (2 each) or `get_player_matches` (5 per 50). Ages, hand, height: `get_player` (5). Ranking on a date: `get_rankings` with `date` (2).\n> The cheapest design: pull seasons of results with `get_matches` and compute your own Elo and rolling features locally. About 90 credits per tour-season.\n\nYou turn match records into a table where each row is one match and every feature was knowable before the first ball. The single biggest risk is leakage, so every feature below states what it is allowed to see.\n\n## When to Use\n\n- \"What features should I use to predict tennis matches?\"\n- \"How do I build a rating feature without leakage?\"\n- \"My model is 80 percent accurate, why?\" (it is leaking)\n- \"Build a dataset from 2015 to 2025\"\n\n## When NOT to Use\n\n- Evaluating the model: see `walk-forward-validation`\n- Probability quality: see `probability-calibration`\n- Using a price as a feature: see `odds-analysis` first, and treat it separately (see below)\n\n## Commands Available\n\n| Command | What It Does | Credits |\n|---------|-------------|---------|\n| `get_matches` | Season of results with sets, surface, round, seeds, winner; paginate with `limit`, `offset` | 2 |\n| `get_player_matches` | One player's matches with `rating_before`, `rating_after` | 5 |\n| `get_match` | `players[].rating.before` and `surface_before`, `stats`, `odds` | 2 |\n| `get_player` | `date_of_birth`, `height_in`, `plays`, country, debut year via the profile | 5 |\n| `get_rankings` | List in force on a `date`, for rank and points as of before the match | 2 |\n| `get_head_to_head` | Prior meetings (only ever use meetings before the match date) | 3 |\n| `get_tournament` | Draw structure | 2 |\n\n## Commands That Do NOT Exist\n\n| Not Available | Use Instead |\n|--------------|-------------|\n| `get_features` | Build locally from the calls above |\n| `get_serve_stats` | Not available: no serve or break-point features |\n| `get_pre_match_ratings` | `rating.before` in `get_match`, `rating_before` in `get_player_matches` |\n| `get_injuries` | Not available: proxy with long gaps and retirements |\n\n## Row Design: One Match, Symmetric Pair\n\nEach match record has `player1` and `player2`. Do not treat position as meaningful. Two valid layouts:\n\n1. **Difference layout.** Pick the focal player at random per row (or alternate), compute `feature_focal - feature_opponent`, and the label is \"focal won\". Mirror the dataset (add the swapped row) so the model sees both orientations; evaluate on one orientation only.\n2. **Paired layout.** Keep both players' features and enforce symmetry: `P(A beats B) + P(B beats A) = 1`, by averaging the model's output across the swap.\n\nA model that learns \"player1 wins more often\" has learned data ordering, not tennis.\n\n## Feature Families\n\n**Strength (pre-match only)**\n\n| Feature | Source | Rule |\n|---------|--------|------|\n| `elo_gap` | `rating.before` for each player | Never `after` |\n| `surf_elo_gap` | `surface_before` | Same surface only; thin early in a career |\n| `elo_blend_gap` | shrink surface toward overall (see `surface-elo`) | Fit shrinkage by walk-forward |\n| `rank_gap_log` | `log(rank2) - log(rank1)` from the list in force before the match | Rank is null outside the top 100 to 150; use an indicator plus a cap (e.g. 200) |\n| `points_gap` | list points before the match | Same cap logic |\n\n**Form and load**\n\n| Feature | Definition | Rule |\n|---------|------------|------|\n| `form_10` | sum of `rating_after - rating_before` over the last 10 completed matches | Shift by one: exclude the current match |\n| `win_rate_surface_20` | wins in last 20 matches on this surface | Min 5, else missing |\n| `games_share_10` | games won divided by games played, last 10 | A serve and return proxy |\n| `days_rest` | days since the previous match | Cap at 60; flag returns after long gaps |\n| `sets_last_7d`, `games_last_7d` | sets or games played in the previous 7 days | Fatigue proxy |\n| `deciding_sets_14d` | three or five set matches in 14 days | Same |\n\n**Context**\n\n| Feature | Source |\n|---------|--------|\n| `surface`, `indoor` | match record (`indoor` may be null; encode as unknown) |\n| `best_of_5` | men's Grand Slam main draw; known in advance |\n| `round_order` | match record |\n| `seed_gap` | `seed1`, `seed2`; null for unseeded, encode as 33 or a missing flag |\n| `age_gap`, `height_gap`, `hand_matchup` | `get_player` bio (compute age at match date) |\n| `tourney_tier` | `major` flag from `list_tournaments`, or Masters/1000 by name |\n\n**Head to head with a time guard**\n\nCompute from `get_head_to_head` only meetings with `match_date` before the current match. Use `wins_a - wins_b` shrunk by `n / (n + 10)`; set to zero when `n < 3`. Do not call `get_head_to_head` per row of a large dataset (3 credits each). Build the history from your own results table instead.\n\n## Rebuilding Elo Locally\n\nIf you compute ratings yourself from `get_matches`, you can reproduce the published method: start 1500, update both players with `K = 250 / (matches + 5)^0.4` where `matches` is the player's own count of rated matches so far (separately for each surface rating), skip walkovers, defaults and retirements, and expected score `1 / (1 + 10^(-(Ra - Rb)/400))`. Your numbers will differ from `rating.before` where your match pool differs from AcesAPI's (qualifying is included in theirs, from 2022). Validate on a sample: compare your pre-match value with `rating_before` from `get_player_matches` for one player, and expect agreement to within a few points when pools match.\n\n```python\ndef update(ra, rb, na, nb, a_won):\n ea = 1 / (1 + 10 ** (-(ra - rb) / 400))\n ka = 250 / (na + 5) ** 0.4\n kb = 250 / (nb + 5) ** 0.4\n s = 1.0 if a_won else 0.0\n return ra + ka * (s - ea), rb + kb * ((1 - s) - (1 - ea))\n```\n\nProcess matches strictly in date order (`starts_at`), and write the pre-match rating into the row before applying the update.\n\n## Leakage Checklist\n\n| Leak | Why | Fix |\n|------|-----|-----|\n| `rating.after`, `surface_after` | Contains this match's result | Use `before` |\n| Score, `sets`, `games1`, `stats.total_games` of the target match | It is the outcome | Drop from features |\n| Rank from the list dated after the match | Includes the result | Take `get_rankings` with `date` set to the day before |\n| Rolling means that include the current row | Self-reference | Shift by 1 before windowing |\n| Career record from `get_player` pulled today | Includes later matches | Rebuild from results as of the match date |\n| Random train/test split | Future matches train the past | Time split, see `walk-forward-validation` |\n| Odds close as a feature, then reporting accuracy | The market already knows | Report it as a market-informed model; compare against the market baseline |\n| Round and draw knowledge | Fine, known in advance | Keep |\n| Final-round survivors | Winners reach late rounds | Do not condition on round reached by outcome |\n\nSanity test: a pre-match tennis model that beats the closing line by a wide margin is leaking. Rating-only models usually land in the mid-60s for accuracy on tour-level matches (run the Elo baseline on your own data to confirm); treat 75 percent as a bug to explain.\n\n## Missing Data\n\n- Walkovers, retirements: drop from training and from rating updates, keep as a \"retired recently\" flag for the next match.\n- Qualifying draw matches: available from 2022; the rating updates include them, so models trained before and after 2022 see different coverage.\n- Rank null: indicator plus cap. Never fill with 0.\n- First matches of a career: few prior matches means the rating is unsettled; include `matches_played_log` as a feature so the model can discount.\n\n## Credit Usage\n\n| Dataset | Calls | Credits |\n|---------|-------|---------|\n| One tour, one season of results | about 45 `get_matches` pages | about 90 |\n| Ten seasons, one tour | about 450 pages | about 900 |\n| Ratings from `get_match` per row | 1 per match | 2 per match: avoid, recompute locally |\n| Player bios for 300 players | 300 `get_player` | 1,500: sample or only fetch finalists |\n\nState the credit estimate before pulling more than one season.\n\n## Anti-patterns\n\n| What the Model Might Do | Why It's Wrong | Do This Instead |\n|---------------------|---------------|-----------------|\n| Use `rating.after` as a feature | Leak | `before` |\n| Let `player1` mean something | It is record order | Symmetrize |\n| Fill missing rank with 0 | 0 would be better than #1 | Indicator and cap |\n| Add career win rate from today's profile | Includes the future | Rebuild as-of |\n| Evaluate on a shuffled split | Leak | Walk forward |\n| Add 80 features for 20,000 rows | Overfit | Start with Elo gap, surface Elo gap, rest, form |\n\n## What to Do Next\n\n| What You Built | Next Action | Skill |\n|----------------|-------------|-------|\n| A feature table | Fit and evaluate in time order | `walk-forward-validation` |\n| Predictions | Check probabilities | `probability-calibration` |\n"
}SHA-256 of public snapshot: 0a2522c4c5e6197644a2c0b43032b9e04256f6f1c13176fbc9e74d76c6f97a6b