backcaster 0.4.2__tar.gz → 0.4.3__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.
Files changed (32) hide show
  1. {backcaster-0.4.2/src/backcaster.egg-info → backcaster-0.4.3}/PKG-INFO +129 -34
  2. {backcaster-0.4.2 → backcaster-0.4.3}/README.md +128 -33
  3. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/__init__.py +1 -1
  4. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/batters.py +2 -1
  5. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/cli.py +49 -13
  6. backcaster-0.4.3/src/backcaster/eligibility.py +47 -0
  7. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/html_report.py +172 -41
  8. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/lahman.py +156 -35
  9. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/marcel.py +13 -14
  10. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/outputs.py +55 -4
  11. {backcaster-0.4.2 → backcaster-0.4.3/src/backcaster.egg-info}/PKG-INFO +129 -34
  12. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster.egg-info/SOURCES.txt +2 -0
  13. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_batters.py +25 -1
  14. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_cli.py +64 -6
  15. backcaster-0.4.3/tests/test_eligibility.py +65 -0
  16. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_html_report.py +66 -0
  17. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_lahman.py +457 -31
  18. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_marcel_math.py +81 -7
  19. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_outputs.py +92 -1
  20. {backcaster-0.4.2 → backcaster-0.4.3}/LICENSE +0 -0
  21. {backcaster-0.4.2 → backcaster-0.4.3}/THIRD_PARTY_NOTICES.md +0 -0
  22. {backcaster-0.4.2 → backcaster-0.4.3}/pyproject.toml +0 -0
  23. {backcaster-0.4.2 → backcaster-0.4.3}/setup.cfg +0 -0
  24. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/data.py +0 -0
  25. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/normalize.py +0 -0
  26. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/schemas.py +0 -0
  27. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster/stat_columns.py +0 -0
  28. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster.egg-info/dependency_links.txt +0 -0
  29. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster.egg-info/entry_points.txt +0 -0
  30. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster.egg-info/requires.txt +0 -0
  31. {backcaster-0.4.2 → backcaster-0.4.3}/src/backcaster.egg-info/top_level.txt +0 -0
  32. {backcaster-0.4.2 → backcaster-0.4.3}/tests/test_normalize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backcaster
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Marcel-style MLB projection CLI using local Lahman CSV data
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/jeffreywevans/backcaster
@@ -41,6 +41,7 @@ It supports:
41
41
  - **Single team projections** (`team`)
42
42
  - **League-wide projections** (`hitters`, `pitchers`, or `batch`)
43
43
  - Output as terminal table, CSV, or HTML
44
+ - Historical comparisons against actual results in HTML reports (`--compare-actuals`)
44
45
 
45
46
  ---
46
47
 
@@ -59,10 +60,10 @@ Upgrade to the latest release available on PyPI:
59
60
  python -m pip install --upgrade backcaster
60
61
  ```
61
62
 
62
- To install a specific release, such as 0.4.2:
63
+ To install a specific release, such as 0.4.3:
63
64
 
64
65
  ```powershell
65
- python -m pip install --upgrade backcaster==0.4.2
66
+ python -m pip install --upgrade backcaster==0.4.3
66
67
  ```
67
68
 
68
69
  Run the command using the same Python environment where you run Backcaster.
@@ -110,8 +111,8 @@ Subcommand-specific required options:
110
111
  - `batch`, `hitters`, and `pitchers` require no player or team option
111
112
 
112
113
  ```bash
113
- backcaster <player|team|batch> [subcommand-options] --year <YEAR> --kind <batting|pitching> [--format cli|csv|html] [--out PATH]
114
- backcaster <hitters|pitchers> --year <YEAR> [--data-dir PATH] [--format cli|csv|html] [--out PATH]
114
+ backcaster <player|team|batch> [subcommand-options] --year <YEAR> --kind <batting|pitching> [--format cli|csv|html] [--compare-actuals] [--out PATH]
115
+ backcaster <hitters|pitchers> --year <YEAR> [--data-dir PATH] [--format cli|csv|html] [--compare-actuals] [--out PATH]
115
116
  ```
116
117
 
117
118
  ### Pre-offseason league projections
@@ -131,7 +132,8 @@ backcaster hitters --year 2026 --format csv --out hitters_2026.csv
131
132
  backcaster pitchers --year 2026 --format csv --out pitchers_2026.csv
132
133
  ```
133
134
 
134
- `--year 2026` projects 2026 from 2023–2025; it never reads 2026 statistics.
135
+ `--year 2026` projects 2026 from 2023–2025. It reads 2026 statistics only when
136
+ `--compare-actuals` is selected for a historical HTML report.
135
137
  Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
136
138
  They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
137
139
  For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
@@ -140,13 +142,21 @@ File formats require `--out`; the CLI format prints to the terminal and rejects
140
142
  `--out`. Invalid output options fail before data is loaded. Unwritable output
141
143
  paths report an error on stderr and exit with status 2.
142
144
 
143
- Players qualify for selection by meeting the existing PA/IP threshold in at least
144
- one of those three seasons. This is a historical eligibility list, not an active
145
- roster; pitchers who never reach the qualifying innings threshold (including most
146
- relievers) remain excluded. Qualification selects players without removing their
147
- other seasons from the calculation. Trades are combined into one player-season
148
- before qualification, and all teams' statistics contribute once. The output is
149
- one row per selected player, ready for comparison with reference projections.
145
+ Exports must stay within the current working directory or its subdirectories.
146
+ Relative paths and absolute paths within that directory are accepted; paths that
147
+ escape it through `..` or symbolic links are rejected before data is loaded.
148
+ To export elsewhere, run Backcaster from the intended output directory.
149
+
150
+ Hitters receive a projection with any positive PA in the preceding three seasons,
151
+ subject to usable ages and the existing non-pitcher classification. There is no
152
+ minimum-PA or reliability gate. `ConfidenceTier` labels reliability at least 0.50
153
+ as `core` and lower values as `low-sample`; both tiers appear in every output.
154
+ Pitchers receive a projection with any recorded outs or batters faced in the
155
+ lookback window, including relievers and appearances without an out. They use
156
+ the same confidence labels, with no innings or reliability gate. All selected
157
+ players use their available three-year history.
158
+ Without a dated eligibility snapshot, this is a historical candidate population,
159
+ including potentially retired or otherwise unavailable players.
150
160
 
151
161
  ### 1) Player projection
152
162
 
@@ -186,18 +196,24 @@ FanGraphs abbreviation `LAD`). A projection year needs the preceding three seaso
186
196
  is unavailable. If the directory or required files are missing, the command
187
197
  reports the missing file and directory; it never attempts an online fetch.
188
198
 
189
- Offline qualification uses a consistent modern rule across historical seasons:
190
- at least **3.1 PA per game** for hitters and **one inning per game** for pitchers.
191
- The season's game count is the maximum `G` in that year's `Teams.csv` records,
192
- so shortened seasons have lower thresholds. The PA threshold is rounded to the
193
- nearest integer (502 PA for 162 games, 186 PA for 60 games). This is an analytical filter, not a
194
- reconstruction of historical award eligibility or batting-title exceptions.
195
- Relievers generally do not qualify under this pitching rule.
199
+ Batting inclusion requires positive lookback PA; pitching requires recorded outs
200
+ or batters faced. Neither has a minimum seasonal workload or reliability cutoff.
201
+ `season(..., qualified=True)` still exposes the historical PA/IP analytical filters
202
+ for Python callers; projection commands do not apply them.
196
203
 
197
- Player stints are summed before qualification. Plate appearances are calculated
204
+ Player stints are summed before projection. Plate appearances are calculated
198
205
  as `AB + BB + HBP + SF + SH`; missing/blank HBP, SF, and SH values count as zero.
199
206
  Blank numeric counting-stat cells are also treated as zero; use a dataset with
200
207
  complete records for the seasons you intend to project.
208
+
209
+ Historical batting and pitching records with missing or invalid birth years are excluded with
210
+ a stderr warning identifying the count and sample player IDs. Remaining players
211
+ are still projected; no default age is substituted. An entirely unusable
212
+ population produces a clear error. See [historical data caveats](docs/LAHMAN.md).
213
+ Pitcher playing time is `0.5 × last-year IP + 0.1 × two-years-ago IP + 60`,
214
+ with missing calendar years contributing zero. The third prior season still
215
+ contributes to regression and reliability. See [the pitcher model](docs/PITCHER_MODEL.md).
216
+
201
217
  Pitching `IPouts` is divided by three for decimal innings, and `BFP` supplies
202
218
  batters faced. Lahman player IDs keep different players with the same name separate;
203
219
  ambiguous name lookups report the matching IDs. Select one with `--player-id`
@@ -207,18 +223,56 @@ instead of `--name`, for example:
207
223
  backcaster player --player-id bettsmo01 --year 2026 --kind batting
208
224
  ```
209
225
 
210
- IDs match Lahman `People.csv.playerID` exactly and retain the same qualification
226
+ IDs match Lahman `People.csv.playerID` exactly and retain the same inclusion
211
227
  rules as name lookups.
212
228
 
213
- Batter league regression and rebaselining use all non-pitcher player-seasons;
214
- qualification selects the displayed players. Pitching retains its qualified-only
215
- baseline, with real-age performance adjustments documented in
216
- [the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
217
- real-age adjustment, PA formula, and explicit rebaselining choices.
218
- Eligibility means qualifying in at least one of the three preceding seasons.
219
- For every selected player, projections use all available seasons in that window,
220
- including nonqualified seasons and seasons spent with other teams. For team selection, a traded player's full-season totals are assigned
221
- to the team in their latest stint for that season; this is not a current-roster forecast.
229
+ Batter league regression and rebaselining use all positive-PA non-pitcher
230
+ player-seasons. Output selection, including status snapshots, does not change
231
+ that calculation. Pitcher regression likewise uses the full historical sample
232
+ population, independently of roster, player, or team selection. See [the batter model](docs/BATTER_MODEL.md) and
233
+ [the pitcher model](docs/PITCHER_MODEL.md).
234
+
235
+ For every selected player, projections use all available seasons in the window,
236
+ including seasons spent with other teams. Without a status snapshot, team
237
+ selection uses historical last-stint membership for each season.
238
+
239
+ ### Dated player eligibility
240
+
241
+ For a roster/status comparison, supply an explicit eligibility snapshot and date:
242
+
243
+ ```bash
244
+ backcaster hitters --year 2025 --eligibility-file eligibility_2025.csv --as-of 2025-01-14 --format csv --out mlb_hitting_2025.csv
245
+ ```
246
+
247
+ The CSV uses Lahman IDs and explicit inclusion decisions:
248
+
249
+ ```csv
250
+ PlayerID,Year,AsOf,Eligible,Team
251
+ troutmi01,2025,2025-01-14,true,ANA
252
+ bettsmo01,2025,2025-01-14,true,LAN
253
+ ```
254
+
255
+ Create these decisions from a roster/organization/free-agent/retirement source
256
+ as of the stated date. `Eligible` must be true or false. Unlisted players and
257
+ false rows are excluded; listed players still need a lookback sample (PA for
258
+ hitters, outs or batters faced for pitchers) and a usable birth year. Eligibility
259
+ never depends on reliability. The file can contain
260
+ multiple years and dates; only the exact `Year` and `AsOf` match is used, with
261
+ unique PlayerID values within that snapshot.
262
+
263
+ Both flags are required together and apply to batting and pitching. For pitchers,
264
+ use `backcaster pitchers` (or `--kind pitching`) with the same flags. Use separate
265
+ snapshot files if inclusion decisions differ between hitting and pitching roles.
266
+ `Team` is optional for league/player output and required for team output; it represents
267
+ membership at the snapshot date, with blanks allowed for unassigned players.
268
+ With a snapshot, team selection uses this membership rather than historical teams.
269
+ Exports include `EligibilityAsOf`. The snapshot filters output only; league
270
+ regression and rebaselining continue to use the full historical population.
271
+
272
+ For the January 14, 2025 Baseball Reference comparison, supply a snapshot actually
273
+ frozen to that date. Lahman history cannot reconstruct roster status on that date,
274
+ and later performance or final career dates are not used to guess it. Matching
275
+ 504 benchmark rows requires the right status population as well as model inputs.
222
276
 
223
277
  ### 2) Team projection
224
278
 
@@ -285,7 +339,7 @@ true innings rather than the displayed notation.
285
339
  ### Tracked statistics and order
286
340
 
287
341
  All outputs put available Name, Year, Player ID, and Age metadata first, followed
288
- by these statistics in order and then Reliability:
342
+ by these statistics in order and then Reliability and ConfidenceTier; snapshot-filtered output appends EligibilityAsOf:
289
343
 
290
344
  ```text
291
345
  Batters: PA,AB,R,H,2B,3B,HR,RBI,SB,CS,BB,SO,BA,OBP,SLG,OPS,TB,GDP,HBP,SH,SF,IBB
@@ -314,6 +368,7 @@ or share as one file. No server, CDN, fonts, or internet connection are required
314
368
  ```powershell
315
369
  backcaster hitters --year 2025 --format html --out hitters_2025.html
316
370
  backcaster pitchers --year 2025 --format html --out pitchers_2025.html
371
+ backcaster hitters --year 2025 --format html --compare-actuals --out hitters_accuracy_2025.html
317
372
  ```
318
373
 
319
374
  The first four columns are **Name, Year, Player ID, Age**, followed by counting
@@ -322,6 +377,7 @@ statistics and batting/pitching rates in a consistent order. The report includes
322
377
  - A player-count, projection-year, and projection-type summary.
323
378
  - Search across player names, IDs, and statistics.
324
379
  - Sortable column headings, with numeric sorting for statistics.
380
+ - Optional historical accuracy rows with a per-player expand/collapse control.
325
381
  - A sticky name column/header and horizontal scrolling on narrow screens.
326
382
  - A reset control and print layout for browser printing or saving as PDF.
327
383
 
@@ -331,6 +387,43 @@ Search filters the visible rows; printing respects that current view. Without
331
387
  JavaScript, the complete table is still readable. See
332
388
  [HTML report customization](docs/HTML_REPORTS.md) for theme and column constants.
333
389
 
390
+ ### Compare projections with actual results (new in 0.4.3)
391
+
392
+ Add `--compare-actuals` to a historical HTML report for either batters or pitchers:
393
+
394
+ ```powershell
395
+ backcaster hitters --year 2025 --format html --compare-actuals --out hitters_accuracy_2025.html
396
+ backcaster pitchers --year 2025 --format html --compare-actuals --out pitchers_accuracy_2025.html
397
+ ```
398
+
399
+ For these examples, the local Lahman files must cover **2022–2024** for the
400
+ projection and **2025** for the actual results, including target-year game counts
401
+ in `Teams.csv`. Use `--data-dir` to select the dataset. The 1871–2025 edition can
402
+ support a 2025 comparison; it cannot provide actual results for 2026.
403
+ The option also works with `player`, `team`, and `batch`; it requires
404
+ `--format html` and `--out` and is rejected for terminal or CSV output.
405
+
406
+ Projection calculations use only the three preceding seasons. Target-season
407
+ results are loaded afterward and matched by Lahman PlayerID, combining trade
408
+ stints without requiring the player to meet a seasonal qualification threshold.
409
+ Each projected player has two additional rows:
410
+
411
+ - **Actual** shows the player's recorded target-season results.
412
+ - **Variance** shows `actual - projected`, calculated before display rounding.
413
+ For example, 25 actual HR against 20 projected HR displays `+5`.
414
+
415
+ Positive differences are green and negative differences are red. These colors
416
+ indicate direction: a positive ERA variance means more earned runs per nine
417
+ innings than projected. IP differences use signed baseball innings notation;
418
+ `+1.2` means one inning and two outs.
419
+
420
+ Rows start expanded. Use the control beside each player name to collapse or
421
+ expand them; the rows stay with their player when sorting or filtering, and
422
+ printing preserves the current view. A projected player without a target-season
423
+ record is labeled `Actual (no recorded stats)` and has blank variance cells.
424
+ Players absent from the projection population are not added from the actuals.
425
+ ConfidenceTier and Reliability describe the projection, not the actual results.
426
+
334
427
  ---
335
428
 
336
429
  ## Local data updates
@@ -361,7 +454,9 @@ Development dependencies (`--extra dev`) include:
361
454
 
362
455
  ### Lockfile policy (`uv.lock`)
363
456
 
364
- `uv.lock` is committed and pins runtime, development, and build dependencies. CI uses
457
+ `uv.lock` is committed and pins runtime, development, and build dependencies. CI
458
+ installs uv 0.12.23 with a SHA-pinned `astral-sh/setup-uv` action, whose embedded
459
+ checksums verify the downloaded uv archive. CI uses
365
460
  `uv sync --locked --extra dev` and fails if the lockfile is out of date. CI first
366
461
  installs the locked tools with `--no-install-project`, then installs Backcaster
367
462
  with `--no-build-isolation` to keep build dependencies pinned. CI builds
@@ -408,7 +503,7 @@ pytest
408
503
  ### Coverage + JUnit reports
409
504
 
410
505
  ```bash
411
- pytest --junitxml=pytest.xml --cov=src --cov-report=xml:coverage.xml
506
+ pytest --junitxml=pytest.xml --cov=src --cov-branch --cov-report=xml:coverage.xml --cov-fail-under=100
412
507
  ```
413
508
 
414
509
  ### Lint and types
@@ -7,6 +7,7 @@ It supports:
7
7
  - **Single team projections** (`team`)
8
8
  - **League-wide projections** (`hitters`, `pitchers`, or `batch`)
9
9
  - Output as terminal table, CSV, or HTML
10
+ - Historical comparisons against actual results in HTML reports (`--compare-actuals`)
10
11
 
11
12
  ---
12
13
 
@@ -25,10 +26,10 @@ Upgrade to the latest release available on PyPI:
25
26
  python -m pip install --upgrade backcaster
26
27
  ```
27
28
 
28
- To install a specific release, such as 0.4.2:
29
+ To install a specific release, such as 0.4.3:
29
30
 
30
31
  ```powershell
31
- python -m pip install --upgrade backcaster==0.4.2
32
+ python -m pip install --upgrade backcaster==0.4.3
32
33
  ```
33
34
 
34
35
  Run the command using the same Python environment where you run Backcaster.
@@ -76,8 +77,8 @@ Subcommand-specific required options:
76
77
  - `batch`, `hitters`, and `pitchers` require no player or team option
77
78
 
78
79
  ```bash
79
- backcaster <player|team|batch> [subcommand-options] --year <YEAR> --kind <batting|pitching> [--format cli|csv|html] [--out PATH]
80
- backcaster <hitters|pitchers> --year <YEAR> [--data-dir PATH] [--format cli|csv|html] [--out PATH]
80
+ backcaster <player|team|batch> [subcommand-options] --year <YEAR> --kind <batting|pitching> [--format cli|csv|html] [--compare-actuals] [--out PATH]
81
+ backcaster <hitters|pitchers> --year <YEAR> [--data-dir PATH] [--format cli|csv|html] [--compare-actuals] [--out PATH]
81
82
  ```
82
83
 
83
84
  ### Pre-offseason league projections
@@ -97,7 +98,8 @@ backcaster hitters --year 2026 --format csv --out hitters_2026.csv
97
98
  backcaster pitchers --year 2026 --format csv --out pitchers_2026.csv
98
99
  ```
99
100
 
100
- `--year 2026` projects 2026 from 2023–2025; it never reads 2026 statistics.
101
+ `--year 2026` projects 2026 from 2023–2025. It reads 2026 statistics only when
102
+ `--compare-actuals` is selected for a historical HTML report.
101
103
  Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
102
104
  They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
103
105
  For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
@@ -106,13 +108,21 @@ File formats require `--out`; the CLI format prints to the terminal and rejects
106
108
  `--out`. Invalid output options fail before data is loaded. Unwritable output
107
109
  paths report an error on stderr and exit with status 2.
108
110
 
109
- Players qualify for selection by meeting the existing PA/IP threshold in at least
110
- one of those three seasons. This is a historical eligibility list, not an active
111
- roster; pitchers who never reach the qualifying innings threshold (including most
112
- relievers) remain excluded. Qualification selects players without removing their
113
- other seasons from the calculation. Trades are combined into one player-season
114
- before qualification, and all teams' statistics contribute once. The output is
115
- one row per selected player, ready for comparison with reference projections.
111
+ Exports must stay within the current working directory or its subdirectories.
112
+ Relative paths and absolute paths within that directory are accepted; paths that
113
+ escape it through `..` or symbolic links are rejected before data is loaded.
114
+ To export elsewhere, run Backcaster from the intended output directory.
115
+
116
+ Hitters receive a projection with any positive PA in the preceding three seasons,
117
+ subject to usable ages and the existing non-pitcher classification. There is no
118
+ minimum-PA or reliability gate. `ConfidenceTier` labels reliability at least 0.50
119
+ as `core` and lower values as `low-sample`; both tiers appear in every output.
120
+ Pitchers receive a projection with any recorded outs or batters faced in the
121
+ lookback window, including relievers and appearances without an out. They use
122
+ the same confidence labels, with no innings or reliability gate. All selected
123
+ players use their available three-year history.
124
+ Without a dated eligibility snapshot, this is a historical candidate population,
125
+ including potentially retired or otherwise unavailable players.
116
126
 
117
127
  ### 1) Player projection
118
128
 
@@ -152,18 +162,24 @@ FanGraphs abbreviation `LAD`). A projection year needs the preceding three seaso
152
162
  is unavailable. If the directory or required files are missing, the command
153
163
  reports the missing file and directory; it never attempts an online fetch.
154
164
 
155
- Offline qualification uses a consistent modern rule across historical seasons:
156
- at least **3.1 PA per game** for hitters and **one inning per game** for pitchers.
157
- The season's game count is the maximum `G` in that year's `Teams.csv` records,
158
- so shortened seasons have lower thresholds. The PA threshold is rounded to the
159
- nearest integer (502 PA for 162 games, 186 PA for 60 games). This is an analytical filter, not a
160
- reconstruction of historical award eligibility or batting-title exceptions.
161
- Relievers generally do not qualify under this pitching rule.
165
+ Batting inclusion requires positive lookback PA; pitching requires recorded outs
166
+ or batters faced. Neither has a minimum seasonal workload or reliability cutoff.
167
+ `season(..., qualified=True)` still exposes the historical PA/IP analytical filters
168
+ for Python callers; projection commands do not apply them.
162
169
 
163
- Player stints are summed before qualification. Plate appearances are calculated
170
+ Player stints are summed before projection. Plate appearances are calculated
164
171
  as `AB + BB + HBP + SF + SH`; missing/blank HBP, SF, and SH values count as zero.
165
172
  Blank numeric counting-stat cells are also treated as zero; use a dataset with
166
173
  complete records for the seasons you intend to project.
174
+
175
+ Historical batting and pitching records with missing or invalid birth years are excluded with
176
+ a stderr warning identifying the count and sample player IDs. Remaining players
177
+ are still projected; no default age is substituted. An entirely unusable
178
+ population produces a clear error. See [historical data caveats](docs/LAHMAN.md).
179
+ Pitcher playing time is `0.5 × last-year IP + 0.1 × two-years-ago IP + 60`,
180
+ with missing calendar years contributing zero. The third prior season still
181
+ contributes to regression and reliability. See [the pitcher model](docs/PITCHER_MODEL.md).
182
+
167
183
  Pitching `IPouts` is divided by three for decimal innings, and `BFP` supplies
168
184
  batters faced. Lahman player IDs keep different players with the same name separate;
169
185
  ambiguous name lookups report the matching IDs. Select one with `--player-id`
@@ -173,18 +189,56 @@ instead of `--name`, for example:
173
189
  backcaster player --player-id bettsmo01 --year 2026 --kind batting
174
190
  ```
175
191
 
176
- IDs match Lahman `People.csv.playerID` exactly and retain the same qualification
192
+ IDs match Lahman `People.csv.playerID` exactly and retain the same inclusion
177
193
  rules as name lookups.
178
194
 
179
- Batter league regression and rebaselining use all non-pitcher player-seasons;
180
- qualification selects the displayed players. Pitching retains its qualified-only
181
- baseline, with real-age performance adjustments documented in
182
- [the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
183
- real-age adjustment, PA formula, and explicit rebaselining choices.
184
- Eligibility means qualifying in at least one of the three preceding seasons.
185
- For every selected player, projections use all available seasons in that window,
186
- including nonqualified seasons and seasons spent with other teams. For team selection, a traded player's full-season totals are assigned
187
- to the team in their latest stint for that season; this is not a current-roster forecast.
195
+ Batter league regression and rebaselining use all positive-PA non-pitcher
196
+ player-seasons. Output selection, including status snapshots, does not change
197
+ that calculation. Pitcher regression likewise uses the full historical sample
198
+ population, independently of roster, player, or team selection. See [the batter model](docs/BATTER_MODEL.md) and
199
+ [the pitcher model](docs/PITCHER_MODEL.md).
200
+
201
+ For every selected player, projections use all available seasons in the window,
202
+ including seasons spent with other teams. Without a status snapshot, team
203
+ selection uses historical last-stint membership for each season.
204
+
205
+ ### Dated player eligibility
206
+
207
+ For a roster/status comparison, supply an explicit eligibility snapshot and date:
208
+
209
+ ```bash
210
+ backcaster hitters --year 2025 --eligibility-file eligibility_2025.csv --as-of 2025-01-14 --format csv --out mlb_hitting_2025.csv
211
+ ```
212
+
213
+ The CSV uses Lahman IDs and explicit inclusion decisions:
214
+
215
+ ```csv
216
+ PlayerID,Year,AsOf,Eligible,Team
217
+ troutmi01,2025,2025-01-14,true,ANA
218
+ bettsmo01,2025,2025-01-14,true,LAN
219
+ ```
220
+
221
+ Create these decisions from a roster/organization/free-agent/retirement source
222
+ as of the stated date. `Eligible` must be true or false. Unlisted players and
223
+ false rows are excluded; listed players still need a lookback sample (PA for
224
+ hitters, outs or batters faced for pitchers) and a usable birth year. Eligibility
225
+ never depends on reliability. The file can contain
226
+ multiple years and dates; only the exact `Year` and `AsOf` match is used, with
227
+ unique PlayerID values within that snapshot.
228
+
229
+ Both flags are required together and apply to batting and pitching. For pitchers,
230
+ use `backcaster pitchers` (or `--kind pitching`) with the same flags. Use separate
231
+ snapshot files if inclusion decisions differ between hitting and pitching roles.
232
+ `Team` is optional for league/player output and required for team output; it represents
233
+ membership at the snapshot date, with blanks allowed for unassigned players.
234
+ With a snapshot, team selection uses this membership rather than historical teams.
235
+ Exports include `EligibilityAsOf`. The snapshot filters output only; league
236
+ regression and rebaselining continue to use the full historical population.
237
+
238
+ For the January 14, 2025 Baseball Reference comparison, supply a snapshot actually
239
+ frozen to that date. Lahman history cannot reconstruct roster status on that date,
240
+ and later performance or final career dates are not used to guess it. Matching
241
+ 504 benchmark rows requires the right status population as well as model inputs.
188
242
 
189
243
  ### 2) Team projection
190
244
 
@@ -251,7 +305,7 @@ true innings rather than the displayed notation.
251
305
  ### Tracked statistics and order
252
306
 
253
307
  All outputs put available Name, Year, Player ID, and Age metadata first, followed
254
- by these statistics in order and then Reliability:
308
+ by these statistics in order and then Reliability and ConfidenceTier; snapshot-filtered output appends EligibilityAsOf:
255
309
 
256
310
  ```text
257
311
  Batters: PA,AB,R,H,2B,3B,HR,RBI,SB,CS,BB,SO,BA,OBP,SLG,OPS,TB,GDP,HBP,SH,SF,IBB
@@ -280,6 +334,7 @@ or share as one file. No server, CDN, fonts, or internet connection are required
280
334
  ```powershell
281
335
  backcaster hitters --year 2025 --format html --out hitters_2025.html
282
336
  backcaster pitchers --year 2025 --format html --out pitchers_2025.html
337
+ backcaster hitters --year 2025 --format html --compare-actuals --out hitters_accuracy_2025.html
283
338
  ```
284
339
 
285
340
  The first four columns are **Name, Year, Player ID, Age**, followed by counting
@@ -288,6 +343,7 @@ statistics and batting/pitching rates in a consistent order. The report includes
288
343
  - A player-count, projection-year, and projection-type summary.
289
344
  - Search across player names, IDs, and statistics.
290
345
  - Sortable column headings, with numeric sorting for statistics.
346
+ - Optional historical accuracy rows with a per-player expand/collapse control.
291
347
  - A sticky name column/header and horizontal scrolling on narrow screens.
292
348
  - A reset control and print layout for browser printing or saving as PDF.
293
349
 
@@ -297,6 +353,43 @@ Search filters the visible rows; printing respects that current view. Without
297
353
  JavaScript, the complete table is still readable. See
298
354
  [HTML report customization](docs/HTML_REPORTS.md) for theme and column constants.
299
355
 
356
+ ### Compare projections with actual results (new in 0.4.3)
357
+
358
+ Add `--compare-actuals` to a historical HTML report for either batters or pitchers:
359
+
360
+ ```powershell
361
+ backcaster hitters --year 2025 --format html --compare-actuals --out hitters_accuracy_2025.html
362
+ backcaster pitchers --year 2025 --format html --compare-actuals --out pitchers_accuracy_2025.html
363
+ ```
364
+
365
+ For these examples, the local Lahman files must cover **2022–2024** for the
366
+ projection and **2025** for the actual results, including target-year game counts
367
+ in `Teams.csv`. Use `--data-dir` to select the dataset. The 1871–2025 edition can
368
+ support a 2025 comparison; it cannot provide actual results for 2026.
369
+ The option also works with `player`, `team`, and `batch`; it requires
370
+ `--format html` and `--out` and is rejected for terminal or CSV output.
371
+
372
+ Projection calculations use only the three preceding seasons. Target-season
373
+ results are loaded afterward and matched by Lahman PlayerID, combining trade
374
+ stints without requiring the player to meet a seasonal qualification threshold.
375
+ Each projected player has two additional rows:
376
+
377
+ - **Actual** shows the player's recorded target-season results.
378
+ - **Variance** shows `actual - projected`, calculated before display rounding.
379
+ For example, 25 actual HR against 20 projected HR displays `+5`.
380
+
381
+ Positive differences are green and negative differences are red. These colors
382
+ indicate direction: a positive ERA variance means more earned runs per nine
383
+ innings than projected. IP differences use signed baseball innings notation;
384
+ `+1.2` means one inning and two outs.
385
+
386
+ Rows start expanded. Use the control beside each player name to collapse or
387
+ expand them; the rows stay with their player when sorting or filtering, and
388
+ printing preserves the current view. A projected player without a target-season
389
+ record is labeled `Actual (no recorded stats)` and has blank variance cells.
390
+ Players absent from the projection population are not added from the actuals.
391
+ ConfidenceTier and Reliability describe the projection, not the actual results.
392
+
300
393
  ---
301
394
 
302
395
  ## Local data updates
@@ -327,7 +420,9 @@ Development dependencies (`--extra dev`) include:
327
420
 
328
421
  ### Lockfile policy (`uv.lock`)
329
422
 
330
- `uv.lock` is committed and pins runtime, development, and build dependencies. CI uses
423
+ `uv.lock` is committed and pins runtime, development, and build dependencies. CI
424
+ installs uv 0.12.23 with a SHA-pinned `astral-sh/setup-uv` action, whose embedded
425
+ checksums verify the downloaded uv archive. CI uses
331
426
  `uv sync --locked --extra dev` and fails if the lockfile is out of date. CI first
332
427
  installs the locked tools with `--no-install-project`, then installs Backcaster
333
428
  with `--no-build-isolation` to keep build dependencies pinned. CI builds
@@ -374,7 +469,7 @@ pytest
374
469
  ### Coverage + JUnit reports
375
470
 
376
471
  ```bash
377
- pytest --junitxml=pytest.xml --cov=src --cov-report=xml:coverage.xml
472
+ pytest --junitxml=pytest.xml --cov=src --cov-branch --cov-report=xml:coverage.xml --cov-fail-under=100
378
473
  ```
379
474
 
380
475
  ### Lint and types
@@ -1,4 +1,4 @@
1
1
  """backcaster package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.4.2"
4
+ __version__ = "0.4.3"
@@ -170,6 +170,7 @@ def project_batters(seasons: pd.DataFrame, year: int) -> pd.DataFrame:
170
170
  context = _project_context(rates, pa, target, projected)
171
171
  out = _summarize(projected.join(context), pa)
172
172
  out["Reliability"] = evidence_pa / (evidence_pa + 1200)
173
+ out["ConfidenceTier"] = np.where(out["Reliability"] >= 0.5, "core", "low-sample")
173
174
  out["Age"], out["Name"], out["Year"] = age, people["Name"], year
174
175
  out = out.reset_index()
175
- return out[[*IDENTITY_COLUMNS, *BATTING_STAT_COLUMNS, "Reliability"]]
176
+ return out[[*IDENTITY_COLUMNS, *BATTING_STAT_COLUMNS, "Reliability", "ConfidenceTier"]]
@@ -10,7 +10,7 @@ import pandas as pd
10
10
  from backcaster.data import DataLoadError, PlayerLookupError
11
11
  from backcaster.lahman import LahmanSource
12
12
  from backcaster.marcel import ProjectionError
13
- from backcaster.outputs import to_cli_table, to_csv, to_html
13
+ from backcaster.outputs import to_cli_table, to_csv, to_html, validate_output_path
14
14
 
15
15
  _FILE_RENDERERS = {"csv": to_csv, "html": to_html}
16
16
  _OUTPUT_FORMAT_CHOICES = ("cli", *_FILE_RENDERERS.keys())
@@ -25,20 +25,34 @@ def _default_data_dir() -> str:
25
25
  return os.environ.get("LAHMAN_DATA_DIR", default)
26
26
 
27
27
 
28
- def _validate_output_options(output_format: str, out: str | None) -> None:
28
+ def _validate_output_options(
29
+ output_format: str, out: str | None, compare_actuals: bool = False
30
+ ) -> None:
29
31
  if output_format == "cli" and out is not None:
30
32
  raise ValueError("--out cannot be used with --format cli; choose --format csv or html")
31
33
  if output_format in _FILE_RENDERERS and not out:
32
34
  raise ValueError(f"--out is required for {output_format} output")
33
-
34
-
35
- def _render(df: pd.DataFrame, output_format: str, out: str | None) -> None:
36
- _validate_output_options(output_format, out)
35
+ if compare_actuals and output_format != "html":
36
+ raise ValueError("--compare-actuals requires --format html")
37
+ if output_format in _FILE_RENDERERS and out:
38
+ validate_output_path(out)
39
+
40
+
41
+ def _render(
42
+ df: pd.DataFrame,
43
+ output_format: str,
44
+ out: str | None,
45
+ actuals: pd.DataFrame | None = None,
46
+ ) -> None:
37
47
  if output_format == "cli":
38
48
  print(to_cli_table(df))
39
49
  elif output_format in _FILE_RENDERERS:
40
- assert out is not None
41
- _FILE_RENDERERS[output_format](df, out)
50
+ if not out:
51
+ raise ValueError(f"--out is required for {output_format} output")
52
+ if output_format == "html" and actuals is not None:
53
+ to_html(df, out, actuals)
54
+ else:
55
+ _FILE_RENDERERS[output_format](df, out)
42
56
  else:
43
57
  raise ValueError(f"Unsupported format: {output_format}")
44
58
 
@@ -54,7 +68,21 @@ def build_parser() -> argparse.ArgumentParser:
54
68
  else:
55
69
  sp.set_defaults(kind=kind)
56
70
  sp.add_argument("--format", choices=_OUTPUT_FORMAT_CHOICES, default="cli")
57
- sp.add_argument("--out", help="Output path (required for csv/html; invalid with cli)")
71
+ sp.add_argument(
72
+ "--eligibility-file", help="Target-season eligibility CSV keyed by Lahman PlayerID"
73
+ )
74
+ sp.add_argument(
75
+ "--as-of", help="Eligibility snapshot date (YYYY-MM-DD; requires --eligibility-file)"
76
+ )
77
+ sp.add_argument(
78
+ "--out",
79
+ help="Output path within the current working directory (required for csv/html; invalid with cli)",
80
+ )
81
+ sp.add_argument(
82
+ "--compare-actuals",
83
+ action="store_true",
84
+ help="Add expandable actual and variance rows to a historical HTML report",
85
+ )
58
86
  sp.add_argument(
59
87
  "--data-dir",
60
88
  default=_default_data_dir(),
@@ -64,7 +92,7 @@ def build_parser() -> argparse.ArgumentParser:
64
92
  pp = sub.add_parser("player")
65
93
  add_common(pp)
66
94
  player_selector = pp.add_mutually_exclusive_group(required=True)
67
- player_selector.add_argument("--name", help="Player name (must identify one qualified player)")
95
+ player_selector.add_argument("--name", help="Player name (must identify one eligible player)")
68
96
  player_selector.add_argument(
69
97
  "--player-id", help="Exact Lahman player ID (for example, bettsmo01)"
70
98
  )
@@ -86,16 +114,24 @@ def main(argv: list[str] | None = None) -> int:
86
114
  parser = build_parser()
87
115
  args = parser.parse_args(argv)
88
116
  try:
89
- _validate_output_options(args.format, args.out)
90
- result = LahmanSource(args.data_dir).project(
117
+ _validate_output_options(args.format, args.out, args.compare_actuals)
118
+ source = LahmanSource(args.data_dir)
119
+ result = source.project(
91
120
  args.command,
92
121
  args.year,
93
122
  args.kind,
94
123
  name=getattr(args, "name", None),
95
124
  team=getattr(args, "team", None),
96
125
  player_id=getattr(args, "player_id", None),
126
+ eligibility_file=args.eligibility_file,
127
+ as_of=args.as_of,
128
+ )
129
+ actuals = (
130
+ source.actuals(args.year, args.kind, result["PlayerID"])
131
+ if args.compare_actuals
132
+ else None
97
133
  )
98
- _render(result, args.format, args.out)
134
+ _render(result, args.format, args.out, actuals)
99
135
  return 0
100
136
  except (DataLoadError, PlayerLookupError, ProjectionError, ValueError, OSError) as exc:
101
137
  print(f"Error: {exc}", file=sys.stderr)