stathead 0.2.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ node_modules
2
+ dist
3
+ # ...except the published MCP server bundle (the distributable artifact).
4
+ !mcp/dist/
5
+ !mcp/dist/server.mjs
6
+ .DS_Store
7
+ *.local
8
+ .env
9
+ !.env.example
10
+ public/data/*
11
+ !public/data/schedule-*.json
12
+ !public/data/redraft-projections-presets.json
13
+ # Mike Clay's NFL Projection Guide (ESPN+) is a paid product — its raw extracts
14
+ # are LOCAL-ONLY (regenerate via scripts/extract_clay_*.py from a PDF you own).
15
+ # Never commit clay-*.json; only the derived/anonymized blends ship. See
16
+ # DATA_SOURCES.md ("Paid products — never redistribute the source").
17
+ public/data/clay-projections-*.json
18
+ public/data/clay-unit-grades-*.json
19
+ public/data/clay-matchups-*.json
20
+ public/data/clay-team-projections-*.json
21
+ !public/data/training-rows-cache-*.json
22
+ !public/data/trained-models-cache-*.json
23
+ !public/data/model-cache-*.json
24
+ !public/data/feature-store/
25
+ !public/data/score-store/
26
+ !public/data/ktc_rankings_*.json
27
+ !public/data/ktc_history.json
28
+ !public/data/nflverse_weekly_2025.json
29
+ !public/data/ppg-feature-ablation.json
30
+ !public/data/ppg-hyperparam-sweep.json
31
+ !public/data/residual-feature-ablation.json
32
+ !public/data/residual-hyperparam-sweep.json
33
+ !public/data/ktc-feature-ablation.json
34
+ !public/data/ktc-hyperparam-sweep.json
35
+ !public/data/pdf-career-ablation-predraft.json
36
+ !public/data/pdf-career-ablation-postdraft.json
37
+ !public/data/rsp-career-ablation-predraft.json
38
+ !public/data/rsp-career-ablation-postdraft.json
39
+ !public/data/pdf-only-career-test.json
40
+ !public/data/qb-beast-ablation.json
41
+ !public/data/ktc-forecasts-*.json
42
+ !public/data/redraft-projections.json
43
+ !public/data/projection-base-*.json
44
+ !public/data/weekly-projections-*.json
45
+ !public/data/model-eval-*.json
46
+ !public/data/team-metrics-*.json
47
+ !public/data/coach-tendencies.json
48
+ !public/data/player-metrics-*.json
49
+ !public/data/pbp-slim-*.json.gz
50
+ !public/data/prospect-boom-bust.json
51
+ !public/data/prospect-grades-*.json
52
+ !public/data/depth-order-2026.json
53
+ !public/data/career-2027.json
54
+ !public/data/cfbd-college-stats.json
55
+ !public/data/cfbd-sp-ratings.json
56
+ !public/data/cfbd-recruiting.json
57
+ !public/data/cfbd-games.json
58
+ !public/data/cfbd-team-talent.json
59
+ !public/data/cfbd-player-usage.json
60
+ !public/data/cfbd/
61
+ !public/data/cfbd/*.json
62
+ !public/data/pdf-prospect-features.json
63
+ !public/data/pdf-prospect-features-merged.json
64
+ !public/data/rsp-historical-rankings.json
65
+ !public/data/rsp-qb-features.json
66
+ !public/data/manual-cfbd-overrides.json
67
+ !public/data/player-crosswalk.json
68
+ !public/data/player-id-map.json
69
+ !public/data/player-aliases.json
70
+ !public/data/player-promotions.json
71
+ !public/data/player-conflicts.json
72
+ !public/data/player-buzz.json
73
+ # Raw upstream sources committed as .csv.gz (~35 MB vs ~200 MB raw).
74
+ # TS readLocalFile auto-decompresses; Python runs `bash scripts/extract-data.sh`
75
+ # once to materialize the .csv. See scripts/pull-all-data-sources.sh.
76
+ !public/data/*.csv.gz
77
+ !public/data/ffc_adp_ppr_*.json
78
+ !public/data/ffc_adp_2qb_*.json
79
+ !public/data/espn_adp_*.json
80
+ !public/data/espn-nfl-ids.json
81
+ !public/data/fantasycalc_*.json
82
+ !public/data/fantasycalc_history_*.json
83
+ !public/data/sleeper-players.json
84
+ !public/data/sleeper-adp-*.json
85
+ !public/data/ktc-fc-rescale.json
86
+ # Aggregated, anonymized expert activity (no usernames) — committed/deployed.
87
+ # Built by scripts/build_expert_data.py from the gitignored private/ list.
88
+ !public/data/expert-ownership.json
89
+ !public/data/expert-adds.json
90
+ !public/data/expert-trades.json
91
+ !public/data/expert-graph.json
92
+ !public/data/expert-league-graph.json
93
+ !public/data/expert-candidates.json
94
+ # expert-names.json (index -> username) is intentionally NOT un-ignored: it stays
95
+ # local-only so the social graph / rankings can show names in dev but never ship.
96
+ # Its passphrase-encrypted form (scripts/encrypt-expert-names.mjs) IS committed:
97
+ # the deployed Social Graph decrypts it in-browser after an "unlock" passphrase.
98
+ # The ciphertext is public, so the passphrase is the only protection — keep it strong.
99
+ !public/data/expert-names.enc.json
100
+ .claude/*
101
+ !.claude/commands/
102
+ !.claude/commands/**
103
+ CLAUDE.local.md
104
+ __pycache__/
105
+ .nflverse-cache/
106
+ .cache/
107
+ .venv/
108
+ pdfs/
109
+
110
+ # Footballguys SFB cheatsheet (paid product) — LOCAL-ONLY, never committed.
111
+ # Regenerate from a PDF you own:
112
+ # pdftotext -layout <cheatsheet.pdf> - | npx tsx scripts/parse-sfb-cheatsheet.ts > public/data/sfb16-cheatsheet.json
113
+ # (already covered by the public/data/* ignore; listed here for discoverability)
114
+ public/data/sfb16-cheatsheet.json
115
+
116
+ # Proprietary inputs (Clay PDFs + parsed dataset) — never committed.
117
+ /private/
118
+ # Local-only Clay projection set consumed at runtime by the "Consensus" preset.
119
+ # Never committed — absent in the public deploy so Clay's numbers ship nowhere.
120
+ /public/data/clay/
121
+
122
+ # Downloaded headshot images
123
+ headshots.zip
124
+ headshots_part*.zip
125
+ !public/data/depth-starters-*.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stathead
3
- Version: 0.2.1
3
+ Version: 0.3.2
4
4
  Summary: Python client for the StatHead fantasy football model — rookie career predictions, historical ADP, and flattened feature matrices.
5
5
  Project-URL: Homepage, https://github.com/dachhack/stathead
6
6
  Project-URL: Source, https://github.com/dachhack/stathead
@@ -39,8 +39,8 @@ pip install stathead
39
39
  Optional extras:
40
40
 
41
41
  ```bash
42
- pip install "stathead[polars]" # for .to_polars() helpers
43
- pip install "stathead[duckdb]" # for local SQL querying
42
+ pip install "stathead[polars]" # for sh.to_polars() / sh.load_polars() helpers
43
+ pip install "stathead[duckdb]" # for local SQL querying (sh.query)
44
44
  ```
45
45
 
46
46
  ## Quick start
@@ -62,6 +62,58 @@ adp = sh.load_adp_historical()
62
62
  adp[(adp.season == 2023) & (adp.adp <= 24)]
63
63
  ```
64
64
 
65
+ ## SQL querying
66
+
67
+ With the `duckdb` extra, `sh.query()` runs SQL over the loaders — the same
68
+ surface as the site's Data Query tab, in Python. Every table joins on
69
+ `player_key`.
70
+
71
+ ```bash
72
+ pip install "stathead[duckdb]"
73
+ ```
74
+
75
+ ```python
76
+ import stathead as sh
77
+
78
+ sh.query("""
79
+ SELECT c.name, c.position, c.predictedCareerPPG, d.value_1qb
80
+ FROM career_2026 c
81
+ JOIN dynasty_values d USING (player_key)
82
+ WHERE c.percentile >= 80
83
+ ORDER BY d.value_1qb DESC
84
+ """)
85
+
86
+ sh.list_tables() # every queryable table name
87
+ ```
88
+
89
+ Tables load lazily — only the ones a query references are materialized, so a
90
+ query that never touches `player_stats` (~400k rows) doesn't pay for it.
91
+ Register your own DataFrame (a roster, a league export) to join against the
92
+ model tables:
93
+
94
+ ```python
95
+ sh.register("my_roster", roster_df)
96
+ sh.query("SELECT * FROM career_2026 c JOIN my_roster r USING (player_key)")
97
+ ```
98
+
99
+ ## Polars
100
+
101
+ Loaders return pandas. With the `polars` extra, convert any of them — handy on
102
+ the big tables like `load_player_stats` (~400k rows) where polars is faster.
103
+
104
+ ```bash
105
+ pip install "stathead[polars]"
106
+ ```
107
+
108
+ ```python
109
+ import stathead as sh
110
+
111
+ pl_df = sh.to_polars(sh.load_player_stats(2024))
112
+
113
+ # Or convert a loader by reference, without calling it yourself:
114
+ pl_df = sh.load_polars(sh.load_player_stats, 2024)
115
+ ```
116
+
65
117
  ## Pinning to a specific version
66
118
 
67
119
  Loaders resolve against the upstream GitHub repo. Pin to a commit SHA, tag,
@@ -85,16 +137,61 @@ cache directory or call `clear_cache()` to force a refresh.
85
137
 
86
138
  ## Available loaders
87
139
 
140
+ Every table on the [StatHead site](https://github.com/dachhack/stathead) is
141
+ available here as a pandas DataFrame.
142
+
143
+ **Predictions & prospects**
144
+
88
145
  | Function | Returns | Shape |
89
146
  |---|---|---|
90
147
  | `load_career_predictions_2026()` | 2026 rookie predictions | ~77 × ~80 cols |
91
148
  | `load_career_backtest()` | Historical rookies with pred + actual PPG | ~1087 × ~100 cols |
149
+ | `load_prospect_grades(year=2026)` | Scouting-report grades | ~200 × 7 |
150
+ | `load_career_2027()` | 2027 draft-class early board + college aggregates | ~200 × ~30 |
151
+
152
+ **Projections (model outputs)**
153
+
154
+ | Function | Returns | Shape |
155
+ |---|---|---|
156
+ | `load_redraft_projections()` | Seasonal redraft PPG (PPR) + receptions/game | ~250 × 7 |
157
+ | `load_weekly_projections()` | Per-week matchup-adjusted projections, one row per player-game (opp, home, matchup mult, PPR pts; `df.attrs['def_vs_pos']`) | ~445 × 17 rows/player |
158
+ | `load_ppg_projections()` | Model-predicted PPG for established players | ~250 × 4 |
159
+ | `load_adp_value_model()` | VOR vs ADP, hit probability, confidence interval | ~153 × 10 |
160
+ | `load_volume_projections()` | Team pass/rush/target volumes with low/high bands | ~153 × ~14 |
161
+ | `load_share_projections()` | Predicted target + rush share | ~153 × 6 |
162
+ | `load_taxi_predictions()` | Taxi-squad roster/drop probabilities (+ `df.attrs['meta']`) | ~96 × 6 |
163
+
164
+ **Market & stats**
165
+
166
+ | Function | Returns | Shape |
167
+ |---|---|---|
168
+ | `load_player_stats(season=None)` | Per-player per-week NFL box scores 2010-present | ~400k × ~50 |
169
+ | `load_dynasty_values()` | In-house blended dynasty value (1QB + Superflex) | ~500 × 10 |
170
+ | `load_dynasty_value_history()` | Blended daily dynasty value history | variable |
92
171
  | `load_adp_historical()` | Model-training ADP 2010-2025 | 4507 × 10 |
93
172
  | `load_adp_ffc(season=None)` | FFC PPR raw ADP (per season as fetched) — data via [Fantasy Football Calculator](https://fantasyfootballcalculator.com/adp/ppr) | variable |
94
- | `load_prospect_grades(year=2026)` | Scouting-report grades | ~200 × 7 |
173
+
174
+ **Identity & raw**
175
+
176
+ | Function | Returns | Shape |
177
+ |---|---|---|
178
+ | `load_player_crosswalk()` | Canonical cross-source player IDs | ~10k × ~20 |
179
+ | `resolve_player(name, position=None)` / `get_player(key)` / `load_player_profile(key)` | Name → `player_key` helpers | — |
95
180
  | `load_feature_matrix()` | Raw `feature-matrix.json` (dict) | — |
96
181
  | `load_manual_overrides()` | Manual CFBD usage overrides (dict) | — |
97
182
 
183
+ Every row-shaped loader carries a `player_key` column that joins to
184
+ `load_player_crosswalk()`.
185
+
186
+ ### Dynasty values are the app's in-house blend
187
+
188
+ `load_dynasty_values()` returns the same blended value the web app displays:
189
+ KTC rankings rescaled into FantasyCalc's scale via a per-player ratio
190
+ (`fc_value / ktc_value`, with a positional-median fallback below a value
191
+ floor). The ratio snapshot is built offline and committed as
192
+ `public/data/dynasty-fc-rescale.json`; the loader just applies it. No raw KTC
193
+ value is exposed — only the rescaled blend.
194
+
98
195
  ## Feature columns
99
196
 
100
197
  Career-prediction and backtest rows include flattened model features
@@ -11,8 +11,8 @@ pip install stathead
11
11
  Optional extras:
12
12
 
13
13
  ```bash
14
- pip install "stathead[polars]" # for .to_polars() helpers
15
- pip install "stathead[duckdb]" # for local SQL querying
14
+ pip install "stathead[polars]" # for sh.to_polars() / sh.load_polars() helpers
15
+ pip install "stathead[duckdb]" # for local SQL querying (sh.query)
16
16
  ```
17
17
 
18
18
  ## Quick start
@@ -34,6 +34,58 @@ adp = sh.load_adp_historical()
34
34
  adp[(adp.season == 2023) & (adp.adp <= 24)]
35
35
  ```
36
36
 
37
+ ## SQL querying
38
+
39
+ With the `duckdb` extra, `sh.query()` runs SQL over the loaders — the same
40
+ surface as the site's Data Query tab, in Python. Every table joins on
41
+ `player_key`.
42
+
43
+ ```bash
44
+ pip install "stathead[duckdb]"
45
+ ```
46
+
47
+ ```python
48
+ import stathead as sh
49
+
50
+ sh.query("""
51
+ SELECT c.name, c.position, c.predictedCareerPPG, d.value_1qb
52
+ FROM career_2026 c
53
+ JOIN dynasty_values d USING (player_key)
54
+ WHERE c.percentile >= 80
55
+ ORDER BY d.value_1qb DESC
56
+ """)
57
+
58
+ sh.list_tables() # every queryable table name
59
+ ```
60
+
61
+ Tables load lazily — only the ones a query references are materialized, so a
62
+ query that never touches `player_stats` (~400k rows) doesn't pay for it.
63
+ Register your own DataFrame (a roster, a league export) to join against the
64
+ model tables:
65
+
66
+ ```python
67
+ sh.register("my_roster", roster_df)
68
+ sh.query("SELECT * FROM career_2026 c JOIN my_roster r USING (player_key)")
69
+ ```
70
+
71
+ ## Polars
72
+
73
+ Loaders return pandas. With the `polars` extra, convert any of them — handy on
74
+ the big tables like `load_player_stats` (~400k rows) where polars is faster.
75
+
76
+ ```bash
77
+ pip install "stathead[polars]"
78
+ ```
79
+
80
+ ```python
81
+ import stathead as sh
82
+
83
+ pl_df = sh.to_polars(sh.load_player_stats(2024))
84
+
85
+ # Or convert a loader by reference, without calling it yourself:
86
+ pl_df = sh.load_polars(sh.load_player_stats, 2024)
87
+ ```
88
+
37
89
  ## Pinning to a specific version
38
90
 
39
91
  Loaders resolve against the upstream GitHub repo. Pin to a commit SHA, tag,
@@ -57,16 +109,61 @@ cache directory or call `clear_cache()` to force a refresh.
57
109
 
58
110
  ## Available loaders
59
111
 
112
+ Every table on the [StatHead site](https://github.com/dachhack/stathead) is
113
+ available here as a pandas DataFrame.
114
+
115
+ **Predictions & prospects**
116
+
60
117
  | Function | Returns | Shape |
61
118
  |---|---|---|
62
119
  | `load_career_predictions_2026()` | 2026 rookie predictions | ~77 × ~80 cols |
63
120
  | `load_career_backtest()` | Historical rookies with pred + actual PPG | ~1087 × ~100 cols |
121
+ | `load_prospect_grades(year=2026)` | Scouting-report grades | ~200 × 7 |
122
+ | `load_career_2027()` | 2027 draft-class early board + college aggregates | ~200 × ~30 |
123
+
124
+ **Projections (model outputs)**
125
+
126
+ | Function | Returns | Shape |
127
+ |---|---|---|
128
+ | `load_redraft_projections()` | Seasonal redraft PPG (PPR) + receptions/game | ~250 × 7 |
129
+ | `load_weekly_projections()` | Per-week matchup-adjusted projections, one row per player-game (opp, home, matchup mult, PPR pts; `df.attrs['def_vs_pos']`) | ~445 × 17 rows/player |
130
+ | `load_ppg_projections()` | Model-predicted PPG for established players | ~250 × 4 |
131
+ | `load_adp_value_model()` | VOR vs ADP, hit probability, confidence interval | ~153 × 10 |
132
+ | `load_volume_projections()` | Team pass/rush/target volumes with low/high bands | ~153 × ~14 |
133
+ | `load_share_projections()` | Predicted target + rush share | ~153 × 6 |
134
+ | `load_taxi_predictions()` | Taxi-squad roster/drop probabilities (+ `df.attrs['meta']`) | ~96 × 6 |
135
+
136
+ **Market & stats**
137
+
138
+ | Function | Returns | Shape |
139
+ |---|---|---|
140
+ | `load_player_stats(season=None)` | Per-player per-week NFL box scores 2010-present | ~400k × ~50 |
141
+ | `load_dynasty_values()` | In-house blended dynasty value (1QB + Superflex) | ~500 × 10 |
142
+ | `load_dynasty_value_history()` | Blended daily dynasty value history | variable |
64
143
  | `load_adp_historical()` | Model-training ADP 2010-2025 | 4507 × 10 |
65
144
  | `load_adp_ffc(season=None)` | FFC PPR raw ADP (per season as fetched) — data via [Fantasy Football Calculator](https://fantasyfootballcalculator.com/adp/ppr) | variable |
66
- | `load_prospect_grades(year=2026)` | Scouting-report grades | ~200 × 7 |
145
+
146
+ **Identity & raw**
147
+
148
+ | Function | Returns | Shape |
149
+ |---|---|---|
150
+ | `load_player_crosswalk()` | Canonical cross-source player IDs | ~10k × ~20 |
151
+ | `resolve_player(name, position=None)` / `get_player(key)` / `load_player_profile(key)` | Name → `player_key` helpers | — |
67
152
  | `load_feature_matrix()` | Raw `feature-matrix.json` (dict) | — |
68
153
  | `load_manual_overrides()` | Manual CFBD usage overrides (dict) | — |
69
154
 
155
+ Every row-shaped loader carries a `player_key` column that joins to
156
+ `load_player_crosswalk()`.
157
+
158
+ ### Dynasty values are the app's in-house blend
159
+
160
+ `load_dynasty_values()` returns the same blended value the web app displays:
161
+ KTC rankings rescaled into FantasyCalc's scale via a per-player ratio
162
+ (`fc_value / ktc_value`, with a positional-median fallback below a value
163
+ floor). The ratio snapshot is built offline and committed as
164
+ `public/data/dynasty-fc-rescale.json`; the loader just applies it. No raw KTC
165
+ value is exposed — only the rescaled blend.
166
+
70
167
  ## Feature columns
71
168
 
72
169
  Career-prediction and backtest rows include flattened model features
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "stathead"
7
- version = "0.2.1"
7
+ version = "0.3.2"
8
8
  description = "Python client for the StatHead fantasy football model — rookie career predictions, historical ADP, and flattened feature matrices."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -24,11 +24,25 @@ from .crosswalk import (
24
24
  load_player_profile,
25
25
  resolve_player,
26
26
  )
27
+ from .dynasty import load_dynasty_value_history, load_dynasty_values
27
28
  from .features import load_feature_matrix, load_manual_overrides
28
29
  from .predictions import load_career_backtest, load_career_predictions_2026
30
+ from .projections import (
31
+ load_adp_value_model,
32
+ load_career_2027,
33
+ load_ppg_projections,
34
+ load_redraft_projections,
35
+ load_share_projections,
36
+ load_taxi_predictions,
37
+ load_volume_projections,
38
+ load_weekly_projections,
39
+ )
40
+ from .polars import load_polars, to_polars
29
41
  from .prospects import load_prospect_grades
42
+ from .sql import list_tables, query, register
43
+ from .stats import load_player_stats
30
44
 
31
- __version__ = "0.2.1"
45
+ __version__ = "0.3.2"
32
46
 
33
47
  __all__ = [
34
48
  "__version__",
@@ -37,13 +51,29 @@ __all__ = [
37
51
  "set_ref",
38
52
  "load_adp_ffc",
39
53
  "load_adp_historical",
54
+ "load_adp_value_model",
55
+ "load_career_2027",
40
56
  "load_career_backtest",
41
57
  "load_career_predictions_2026",
58
+ "load_dynasty_value_history",
59
+ "load_dynasty_values",
42
60
  "load_feature_matrix",
43
61
  "load_manual_overrides",
44
62
  "load_player_crosswalk",
45
63
  "load_player_profile",
64
+ "load_player_stats",
65
+ "load_ppg_projections",
46
66
  "load_prospect_grades",
67
+ "load_redraft_projections",
68
+ "load_share_projections",
69
+ "load_taxi_predictions",
70
+ "load_volume_projections",
71
+ "load_weekly_projections",
47
72
  "get_player",
48
73
  "resolve_player",
74
+ "query",
75
+ "register",
76
+ "list_tables",
77
+ "to_polars",
78
+ "load_polars",
49
79
  ]
@@ -72,6 +72,42 @@ def _crosswalk_cached() -> list[dict[str, Any]]:
72
72
  return fetch_json("public/data/player-crosswalk.json").get("players") or []
73
73
 
74
74
 
75
+ @lru_cache(maxsize=1)
76
+ def key_by_name_pos() -> dict[tuple[str, str], str]:
77
+ """``(_norm(name), position) -> player_key`` for every name a player has
78
+ gone by (display name, ``all_names``, aliases) crossed with every
79
+ position they've held. First writer wins so the canonical record is
80
+ preferred over an alias. Used by loaders that only carry name+position
81
+ (dynasty blend, projections) to stamp the canonical ``player_key``."""
82
+ out: dict[tuple[str, str], str] = {}
83
+ for rec in _crosswalk_cached():
84
+ key = rec.get("player_key")
85
+ if not key:
86
+ continue
87
+ names = {rec.get("display_name", "")}
88
+ names.update(rec.get("all_names") or [])
89
+ names.update(a.get("name", "") for a in rec.get("aliases") or [])
90
+ positions = set(rec.get("all_positions") or []) | {rec.get("position") or ""}
91
+ for nm in names:
92
+ n = _norm(nm)
93
+ if not n:
94
+ continue
95
+ for pos in positions:
96
+ out.setdefault((n, pos), key)
97
+ return out
98
+
99
+
100
+ @lru_cache(maxsize=1)
101
+ def key_by_gsis() -> dict[str, str]:
102
+ """``gsis_id -> player_key``. Used to stamp the canonical key onto
103
+ weekly ``player_stats`` rows (whose ``player_id`` is the GSIS ID)."""
104
+ return {
105
+ str(rec["gsis_id"]): rec["player_key"]
106
+ for rec in _crosswalk_cached()
107
+ if rec.get("gsis_id") and rec.get("player_key")
108
+ }
109
+
110
+
75
111
  def resolve_player(name: str, position: str | None = None) -> str:
76
112
  """Return the canonical ``player_key`` for a player name.
77
113