stathead 0.3.2__tar.gz → 0.3.4__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.
@@ -18,6 +18,7 @@ public/data/clay-projections-*.json
18
18
  public/data/clay-unit-grades-*.json
19
19
  public/data/clay-matchups-*.json
20
20
  public/data/clay-team-projections-*.json
21
+ !public/data/tankathon-2027.json
21
22
  !public/data/training-rows-cache-*.json
22
23
  !public/data/trained-models-cache-*.json
23
24
  !public/data/model-cache-*.json
@@ -42,6 +43,20 @@ public/data/clay-team-projections-*.json
42
43
  !public/data/redraft-projections.json
43
44
  !public/data/projection-base-*.json
44
45
  !public/data/weekly-projections-*.json
46
+ !public/data/kicker-projections-*.json
47
+ !public/data/dst-projections-*.json
48
+ !public/data/idp-projections-*.json
49
+ !public/data/return-projections-*.json
50
+ !public/data/fb-projections-*.json
51
+ !public/data/punter-projections-*.json
52
+ !public/data/hc-projections-*.json
53
+ !public/data/coach-overrides.json
54
+ # Market lines, derived from nflverse's schedule (scripts/build-odds-from-schedule.mjs)
55
+ !public/data/odds_nfl_lines.json
56
+ !public/data/schedule-strength-*.json
57
+ # Weekly archive of what we projected, for next offseason's backtests
58
+ # (refresh-data.yml, Tuesdays). Small, gzipped, append-only.
59
+ !public/data/projection-history/
45
60
  !public/data/model-eval-*.json
46
61
  !public/data/team-metrics-*.json
47
62
  !public/data/coach-tendencies.json
@@ -119,7 +134,25 @@ public/data/sfb16-cheatsheet.json
119
134
  # Never committed — absent in the public deploy so Clay's numbers ship nowhere.
120
135
  /public/data/clay/
121
136
 
137
+ # Workbooks written by the MCP's export_excel tool. With no `path` argument it
138
+ # writes stathead-<kind>-<tag>-<date>.xlsx into process.cwd(), which is the repo
139
+ # root whenever the server runs from a checkout (`npm run mcp`) — so ordinary
140
+ # use litters untracked exports here. They're generated output, never sources.
141
+ /stathead-*.xlsx
142
+
122
143
  # Downloaded headshot images
123
144
  headshots.zip
124
145
  headshots_part*.zip
125
146
  !public/data/depth-starters-*.json
147
+ # Fitted abandonment-hazard weights. StatHead's own model output — no
148
+ # per-manager data — so it ships with the app. The population it was fitted on
149
+ # does not (see docs/sleeper-engagement-model.md).
150
+ !public/data/abandonment-model-v*.json
151
+ !public/data/dynasty-departure-*.json
152
+
153
+ # Engagement reporting suite — generated audit reports are reproducible from
154
+ # their input, and crawled Sleeper populations contain per-manager behavioural
155
+ # data on named real people (see docs/sleeper-engagement-model.md). Neither is
156
+ # committed: reports go up as CI artifacts, populations stay local.
157
+ reports/
158
+ sleeper-population*.json
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: stathead
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -154,7 +154,7 @@ available here as a pandas DataFrame.
154
154
  | Function | Returns | Shape |
155
155
  |---|---|---|
156
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 |
157
+ | `load_weekly_projections()` | Per-week matchup-adjusted projections incl. kickers + team DST, one row per player-game (opp, home, matchup mult, PPR pts, gsis/sleeper ids, depth-chart rank, roster `status`/`active` — IR/exempt/practice-squad rows are zeroed from the current week with the conditional number in `proj_ppr_if_active` — and a `backup` flag for 1–3 game lines; `df.attrs['def_vs_pos']`, `df.attrs['meta']['currentWeek']`) | ~780 × 17 rows/player |
158
158
  | `load_ppg_projections()` | Model-predicted PPG for established players | ~250 × 4 |
159
159
  | `load_adp_value_model()` | VOR vs ADP, hit probability, confidence interval | ~153 × 10 |
160
160
  | `load_volume_projections()` | Team pass/rush/target volumes with low/high bands | ~153 × ~14 |
@@ -126,7 +126,7 @@ available here as a pandas DataFrame.
126
126
  | Function | Returns | Shape |
127
127
  |---|---|---|
128
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 |
129
+ | `load_weekly_projections()` | Per-week matchup-adjusted projections incl. kickers + team DST, one row per player-game (opp, home, matchup mult, PPR pts, gsis/sleeper ids, depth-chart rank, roster `status`/`active` — IR/exempt/practice-squad rows are zeroed from the current week with the conditional number in `proj_ppr_if_active` — and a `backup` flag for 1–3 game lines; `df.attrs['def_vs_pos']`, `df.attrs['meta']['currentWeek']`) | ~780 × 17 rows/player |
130
130
  | `load_ppg_projections()` | Model-predicted PPG for established players | ~250 × 4 |
131
131
  | `load_adp_value_model()` | VOR vs ADP, hit probability, confidence interval | ~153 × 10 |
132
132
  | `load_volume_projections()` | Team pass/rush/target volumes with low/high bands | ~153 × ~14 |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "stathead"
7
- version = "0.3.2"
7
+ version = "0.3.4"
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" }
@@ -42,7 +42,7 @@ from .prospects import load_prospect_grades
42
42
  from .sql import list_tables, query, register
43
43
  from .stats import load_player_stats
44
44
 
45
- __version__ = "0.3.2"
45
+ __version__ = "0.3.4"
46
46
 
47
47
  __all__ = [
48
48
  "__version__",
@@ -48,7 +48,9 @@ def load_redraft_projections() -> pd.DataFrame:
48
48
  def load_weekly_projections() -> pd.DataFrame:
49
49
  """Per-week 2026 projections — the season projection split across the
50
50
  schedule, one row per player per scheduled game (17 rows/player; byes
51
- omitted).
51
+ omitted). Covers QB/RB/WR/TE plus kickers (position ``K``, the current
52
+ depth-chart PK1 per team) and team defenses (position ``DST``, name
53
+ ``"<TEAM> DST"``, ``sleeper_id`` = team code).
52
54
 
53
55
  Weekly points = season PPG x opponent defense-vs-position multiplier
54
56
  (prior-season PPR allowed per game vs league average, heavily regressed)
@@ -57,12 +59,28 @@ def load_weekly_projections() -> pd.DataFrame:
57
59
  games (health) discount. Half/Std conversion: weekly receptions scale
58
60
  with the same multiplier, so ``rec_w = recPG * proj_ppr / ppg``.
59
61
 
60
- Columns: ``player_key``, ``name``, ``position``, ``team``, ``week``,
61
- ``opp``, ``home``, ``matchup_mult``, ``proj_ppr``, ``ppg``, ``recPG``,
62
- ``gp``, ``season``.
63
-
64
- Metadata on ``df.attrs``: ``meta`` (generatedAt + method note) and
65
- ``def_vs_pos`` (per-team defense-vs-position multiplier table).
62
+ Roster status (QB/RB/WR/TE, from the nflverse roster at build time):
63
+ ``status`` is ``ACT`` / ``RES`` (IR, PUP, NFI) / ``EXE`` (commissioner
64
+ exempt) / ``DEV`` (practice squad) / ``INA`` (game-day inactive) /
65
+ ``FA`` (unrostered), or None for K/DST/IDP rows. ``active`` is False for
66
+ RES/EXE/DEV/FA rows, whose ``proj_ppr`` is 0 from the current week on;
67
+ ``proj_ppr_if_active`` on those rows is the un-zeroed conditional
68
+ number (NaN elsewhere). Retired and cut players carry no rows.
69
+ ``backup`` is True for a 1-3 game season line on a depth-2+ player:
70
+ ``proj_ppr`` is then a per-game rate conditional on playing, not an
71
+ expectation of starting — rank those below starters. ``depth`` is the
72
+ position rank on the newest nflverse depth chart (1 = starter).
73
+
74
+ Columns: ``player_key``, ``name``, ``position``, ``team``, ``gsis_id``,
75
+ ``sleeper_id``, ``depth``, ``status``, ``active``, ``backup``, ``week``,
76
+ ``opp``, ``home``, ``matchup_mult``, ``proj_ppr``, ``proj_ppr_if_active``,
77
+ ``ppg``, ``recPG``, ``gp``, ``season``. The gsis/sleeper ids are stamped
78
+ at build time from the player crosswalk (None for pre-NFL rookies).
79
+
80
+ Metadata on ``df.attrs``: ``meta`` (generatedAt, method note,
81
+ ``playedThrough`` = last week with every game final, ``currentWeek``,
82
+ ``statusNote``) and ``def_vs_pos`` (per-team defense-vs-position
83
+ multiplier table).
66
84
  """
67
85
  data = fetch_json("public/data/weekly-projections-2026.json")
68
86
  team_weeks = {
@@ -73,6 +91,7 @@ def load_weekly_projections() -> pd.DataFrame:
73
91
  rows = []
74
92
  for p in data.get("players") or []:
75
93
  sched = team_weeks.get(p["team"], {})
94
+ if_active = p.get("wkIfActive")
76
95
  for i, pts in enumerate(p["wk"]):
77
96
  week = i + 1
78
97
  game = sched.get(week)
@@ -82,11 +101,18 @@ def load_weekly_projections() -> pd.DataFrame:
82
101
  "name": p["name"],
83
102
  "position": p["pos"],
84
103
  "team": p["team"],
104
+ "gsis_id": p.get("gsis"),
105
+ "sleeper_id": p.get("sleeper"),
106
+ "depth": p.get("depth"),
107
+ "status": p.get("status"),
108
+ "active": p.get("active", True),
109
+ "backup": bool(p.get("backup", False)),
85
110
  "week": week,
86
111
  "opp": game["opp"],
87
112
  "home": game["home"],
88
113
  "matchup_mult": def_vs_pos.get(game["opp"], {}).get(p["pos"]),
89
114
  "proj_ppr": pts,
115
+ "proj_ppr_if_active": if_active[i] if if_active else None,
90
116
  "ppg": p["ppg"],
91
117
  "recPG": p["recPG"],
92
118
  "gp": p["gp"],
@@ -97,6 +123,9 @@ def load_weekly_projections() -> pd.DataFrame:
97
123
  df.attrs["meta"] = {
98
124
  "generatedAt": data.get("generatedAt"),
99
125
  "note": data.get("note"),
126
+ "playedThrough": data.get("playedThrough"),
127
+ "currentWeek": data.get("currentWeek"),
128
+ "statusNote": data.get("statusNote"),
100
129
  }
101
130
  df.attrs["def_vs_pos"] = def_vs_pos
102
131
  return df
@@ -135,7 +135,20 @@ def test_weekly_projections():
135
135
  df = stathead.load_weekly_projections()
136
136
  assert not df.empty
137
137
  assert {"player_key", "name", "position", "team", "week", "opp", "home",
138
- "matchup_mult", "proj_ppr", "ppg", "gp"}.issubset(df.columns)
138
+ "matchup_mult", "proj_ppr", "ppg", "gp",
139
+ "depth", "status", "active", "backup", "proj_ppr_if_active"}.issubset(df.columns)
140
+ # Roster status only applies to skill positions; K/DST/IDP rows are None.
141
+ assert df.loc[df["position"].isin(["QB", "RB", "WR", "TE"]) & df["status"].notna(), "status"].isin(
142
+ ["ACT", "RES", "EXE", "DEV", "INA", "FA"]).all()
143
+ assert df["active"].dtype == bool and df["backup"].dtype == bool
144
+ # Inactive rows are zeroed from the current week on; the conditional strip
145
+ # rides along so a consumer can redistribute what was vacated.
146
+ inactive = df[~df["active"]]
147
+ if not inactive.empty:
148
+ assert (inactive["proj_ppr"] == 0).any()
149
+ assert inactive["proj_ppr_if_active"].notna().all()
150
+ # Normalization below only holds for rows the builder did not zero.
151
+ df = df[df["active"]]
139
152
  # 17 scheduled games per player (byes omitted), weeks within 1-18.
140
153
  assert (df.groupby("name")["week"].count() == 17).all()
141
154
  assert df["week"].between(1, 18).all()
@@ -143,6 +156,11 @@ def test_weekly_projections():
143
156
  sums = df.groupby("name").agg(total=("proj_ppr", "sum"), ppg=("ppg", "first"))
144
157
  assert ((sums["total"] - sums["ppg"] * 17).abs() < 1.0).all()
145
158
  assert isinstance(df.attrs.get("def_vs_pos"), dict)
159
+ # K + DST: one per team, 32 each.
160
+ assert (df[df["position"] == "K"]["team"].nunique()) == 32
161
+ dst = df[df["position"] == "DST"]
162
+ assert dst["team"].nunique() == 32
163
+ assert (dst["sleeper_id"] == dst["team"]).all()
146
164
 
147
165
 
148
166
  def test_ppg_and_adp_value_model():
File without changes
File without changes
File without changes