backcaster 0.3.4__tar.gz → 0.3.6__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.
- {backcaster-0.3.4/src/backcaster.egg-info → backcaster-0.3.6}/PKG-INFO +52 -16
- {backcaster-0.3.4 → backcaster-0.3.6}/README.md +51 -15
- {backcaster-0.3.4 → backcaster-0.3.6}/THIRD_PARTY_NOTICES.md +7 -5
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/__init__.py +1 -1
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/cli.py +9 -2
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/lahman.py +13 -10
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/marcel.py +46 -15
- {backcaster-0.3.4 → backcaster-0.3.6/src/backcaster.egg-info}/PKG-INFO +52 -16
- {backcaster-0.3.4 → backcaster-0.3.6}/tests/test_lahman.py +83 -2
- {backcaster-0.3.4 → backcaster-0.3.6}/tests/test_marcel_math.py +42 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/LICENSE +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/pyproject.toml +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/setup.cfg +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/batters.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/data.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/normalize.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/outputs.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster/schemas.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster.egg-info/SOURCES.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster.egg-info/dependency_links.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster.egg-info/entry_points.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster.egg-info/requires.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/src/backcaster.egg-info/top_level.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/tests/test_batters.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/tests/test_cli.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/tests/test_normalize.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.6}/tests/test_outputs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backcaster
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -36,7 +36,7 @@ Backcaster is a Python CLI that produces **Marcel-style MLB projections** from t
|
|
|
36
36
|
It supports:
|
|
37
37
|
- **Single player projections** (`player`)
|
|
38
38
|
- **Single team projections** (`team`)
|
|
39
|
-
- **League-wide
|
|
39
|
+
- **League-wide projections** (`hitters`, `pitchers`, or `batch`)
|
|
40
40
|
- Output as terminal table, CSV, or HTML
|
|
41
41
|
|
|
42
42
|
---
|
|
@@ -56,10 +56,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
56
56
|
python -m pip install --upgrade backcaster
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
To install a specific release, such as 0.3.
|
|
59
|
+
To install a specific release, such as 0.3.6:
|
|
60
60
|
|
|
61
61
|
```powershell
|
|
62
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
62
|
+
python -m pip install --upgrade backcaster==0.3.6
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -97,19 +97,52 @@ pip install dist/backcaster-*.whl
|
|
|
97
97
|
|
|
98
98
|
## Quickstart CLI usage
|
|
99
99
|
|
|
100
|
-
All commands require
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
All commands require `--year`, the target projection year.
|
|
101
|
+
The `player`, `team`, and `batch` commands also require `--kind batting|pitching`.
|
|
102
|
+
The `hitters` and `pitchers` commands choose their kind automatically.
|
|
103
103
|
|
|
104
104
|
Subcommand-specific required options:
|
|
105
105
|
- `player` requires `--name`
|
|
106
106
|
- `team` requires `--team`
|
|
107
|
-
- `batch`
|
|
107
|
+
- `batch`, `hitters`, and `pitchers` require no player or team option
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
backcaster <player|team|batch> [subcommand-options] --year <YEAR> --kind <batting|pitching> [--format cli|csv|html] [--out PATH]
|
|
111
|
+
backcaster <hitters|pitchers> --year <YEAR> [--data-dir PATH] [--format cli|csv|html] [--out PATH]
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
### Pre-offseason league projections
|
|
115
|
+
|
|
116
|
+
Once the latest regular season is present in your local Lahman files, generate
|
|
117
|
+
both populations without roster updates or team filters. For terminal output:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
backcaster hitters --year 2026
|
|
121
|
+
backcaster pitchers --year 2026
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
To save files for comparison with Baseball Reference:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
backcaster hitters --year 2026 --format csv --out hitters_2026.csv
|
|
128
|
+
backcaster pitchers --year 2026 --format csv --out pitchers_2026.csv
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`--year 2026` projects 2026 from 2023–2025; it never reads 2026 statistics.
|
|
132
|
+
Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
|
|
133
|
+
They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
|
|
134
|
+
For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
|
|
135
|
+
For HTML, use `--format html --out hitters_2026.html` (or a pitcher output filename).
|
|
136
|
+
File formats require `--out`; the CLI format prints to the terminal.
|
|
137
|
+
|
|
138
|
+
Players qualify for selection by meeting the existing PA/IP threshold in at least
|
|
139
|
+
one of those three seasons. This is a historical eligibility list, not an active
|
|
140
|
+
roster; pitchers who never reach the qualifying innings threshold (including most
|
|
141
|
+
relievers) remain excluded. Qualification selects players without removing their
|
|
142
|
+
other seasons from the calculation. Trades are combined into one player-season
|
|
143
|
+
before qualification, and all teams' statistics contribute once. The output is
|
|
144
|
+
one row per selected player, ready for comparison with reference projections.
|
|
145
|
+
|
|
113
146
|
### 1) Player projection
|
|
114
147
|
|
|
115
148
|
```bash
|
|
@@ -123,8 +156,8 @@ All projections use local Lahman CSVs. On Windows, the default directory is
|
|
|
123
156
|
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
124
157
|
`--data-dir` to override it for one command. No network requests or online
|
|
125
158
|
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
126
|
-
`Batting.csv` or `Pitching.csv` for the selected kind.
|
|
127
|
-
|
|
159
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Both kinds require valid
|
|
160
|
+
`birthYear` values in `People.csv`; batters additionally require `Appearances.csv`. Filenames are matched
|
|
128
161
|
without regard to case. The database is not bundled with Backcaster.
|
|
129
162
|
|
|
130
163
|
For the default Windows directory, no additional flag is needed:
|
|
@@ -166,11 +199,12 @@ ambiguous name lookups report the matching IDs.
|
|
|
166
199
|
|
|
167
200
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
168
201
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
169
|
-
baseline
|
|
202
|
+
baseline, with real-age performance adjustments documented in
|
|
203
|
+
[the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
170
204
|
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
205
|
+
Eligibility means qualifying in at least one of the three preceding seasons.
|
|
206
|
+
For every selected player, projections use all available seasons in that window,
|
|
207
|
+
including nonqualified seasons and seasons spent with other teams. For team selection, a traded player's full-season totals are assigned
|
|
174
208
|
to the team in their latest stint for that season; this is not a current-roster forecast.
|
|
175
209
|
|
|
176
210
|
### 2) Team projection
|
|
@@ -179,7 +213,7 @@ to the team in their latest stint for that season; this is not a current-roster
|
|
|
179
213
|
backcaster team --team LAN --year 2026 --kind pitching
|
|
180
214
|
```
|
|
181
215
|
|
|
182
|
-
### 3) Batch projection (all players
|
|
216
|
+
### 3) Batch projection (all eligible players)
|
|
183
217
|
|
|
184
218
|
```bash
|
|
185
219
|
backcaster batch --year 2026 --kind batting
|
|
@@ -208,7 +242,8 @@ Example shape (columns vary by batting vs pitching):
|
|
|
208
242
|
```
|
|
209
243
|
|
|
210
244
|
### CSV output (`--format csv`)
|
|
211
|
-
A single-row CSV for `player`; multi-row CSV for `team` and `
|
|
245
|
+
A single-row CSV for `player`; multi-row CSV for `team`, `batch`, `hitters`, and `pitchers`.
|
|
246
|
+
Both populations include `PlayerID` for matching records across files.
|
|
212
247
|
|
|
213
248
|
### HTML output (`--format html`)
|
|
214
249
|
A basic HTML table (no custom CSS) suitable for quick sharing or embedding.
|
|
@@ -227,6 +262,7 @@ Lahman distribution from the same edition. Existing online caches are ignored.
|
|
|
227
262
|
|
|
228
263
|
Runtime dependencies are intentionally minimal:
|
|
229
264
|
- `pandas` for tabular data processing and projection output
|
|
265
|
+
- `numpy` for numerical operations and rebaselining convergence checks
|
|
230
266
|
|
|
231
267
|
Development dependencies (`--extra dev`) include:
|
|
232
268
|
- `pytest`, `pytest-cov`
|
|
@@ -5,7 +5,7 @@ Backcaster is a Python CLI that produces **Marcel-style MLB projections** from t
|
|
|
5
5
|
It supports:
|
|
6
6
|
- **Single player projections** (`player`)
|
|
7
7
|
- **Single team projections** (`team`)
|
|
8
|
-
- **League-wide
|
|
8
|
+
- **League-wide projections** (`hitters`, `pitchers`, or `batch`)
|
|
9
9
|
- Output as terminal table, CSV, or HTML
|
|
10
10
|
|
|
11
11
|
---
|
|
@@ -25,10 +25,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
25
25
|
python -m pip install --upgrade backcaster
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
To install a specific release, such as 0.3.
|
|
28
|
+
To install a specific release, such as 0.3.6:
|
|
29
29
|
|
|
30
30
|
```powershell
|
|
31
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
31
|
+
python -m pip install --upgrade backcaster==0.3.6
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -66,19 +66,52 @@ pip install dist/backcaster-*.whl
|
|
|
66
66
|
|
|
67
67
|
## Quickstart CLI usage
|
|
68
68
|
|
|
69
|
-
All commands require
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
All commands require `--year`, the target projection year.
|
|
70
|
+
The `player`, `team`, and `batch` commands also require `--kind batting|pitching`.
|
|
71
|
+
The `hitters` and `pitchers` commands choose their kind automatically.
|
|
72
72
|
|
|
73
73
|
Subcommand-specific required options:
|
|
74
74
|
- `player` requires `--name`
|
|
75
75
|
- `team` requires `--team`
|
|
76
|
-
- `batch`
|
|
76
|
+
- `batch`, `hitters`, and `pitchers` require no player or team option
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
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
81
|
```
|
|
81
82
|
|
|
83
|
+
### Pre-offseason league projections
|
|
84
|
+
|
|
85
|
+
Once the latest regular season is present in your local Lahman files, generate
|
|
86
|
+
both populations without roster updates or team filters. For terminal output:
|
|
87
|
+
|
|
88
|
+
```powershell
|
|
89
|
+
backcaster hitters --year 2026
|
|
90
|
+
backcaster pitchers --year 2026
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To save files for comparison with Baseball Reference:
|
|
94
|
+
|
|
95
|
+
```powershell
|
|
96
|
+
backcaster hitters --year 2026 --format csv --out hitters_2026.csv
|
|
97
|
+
backcaster pitchers --year 2026 --format csv --out pitchers_2026.csv
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`--year 2026` projects 2026 from 2023–2025; it never reads 2026 statistics.
|
|
101
|
+
Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
|
|
102
|
+
They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
|
|
103
|
+
For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
|
|
104
|
+
For HTML, use `--format html --out hitters_2026.html` (or a pitcher output filename).
|
|
105
|
+
File formats require `--out`; the CLI format prints to the terminal.
|
|
106
|
+
|
|
107
|
+
Players qualify for selection by meeting the existing PA/IP threshold in at least
|
|
108
|
+
one of those three seasons. This is a historical eligibility list, not an active
|
|
109
|
+
roster; pitchers who never reach the qualifying innings threshold (including most
|
|
110
|
+
relievers) remain excluded. Qualification selects players without removing their
|
|
111
|
+
other seasons from the calculation. Trades are combined into one player-season
|
|
112
|
+
before qualification, and all teams' statistics contribute once. The output is
|
|
113
|
+
one row per selected player, ready for comparison with reference projections.
|
|
114
|
+
|
|
82
115
|
### 1) Player projection
|
|
83
116
|
|
|
84
117
|
```bash
|
|
@@ -92,8 +125,8 @@ All projections use local Lahman CSVs. On Windows, the default directory is
|
|
|
92
125
|
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
93
126
|
`--data-dir` to override it for one command. No network requests or online
|
|
94
127
|
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
95
|
-
`Batting.csv` or `Pitching.csv` for the selected kind.
|
|
96
|
-
|
|
128
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Both kinds require valid
|
|
129
|
+
`birthYear` values in `People.csv`; batters additionally require `Appearances.csv`. Filenames are matched
|
|
97
130
|
without regard to case. The database is not bundled with Backcaster.
|
|
98
131
|
|
|
99
132
|
For the default Windows directory, no additional flag is needed:
|
|
@@ -135,11 +168,12 @@ ambiguous name lookups report the matching IDs.
|
|
|
135
168
|
|
|
136
169
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
137
170
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
138
|
-
baseline
|
|
171
|
+
baseline, with real-age performance adjustments documented in
|
|
172
|
+
[the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
139
173
|
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
Eligibility means qualifying in at least one of the three preceding seasons.
|
|
175
|
+
For every selected player, projections use all available seasons in that window,
|
|
176
|
+
including nonqualified seasons and seasons spent with other teams. For team selection, a traded player's full-season totals are assigned
|
|
143
177
|
to the team in their latest stint for that season; this is not a current-roster forecast.
|
|
144
178
|
|
|
145
179
|
### 2) Team projection
|
|
@@ -148,7 +182,7 @@ to the team in their latest stint for that season; this is not a current-roster
|
|
|
148
182
|
backcaster team --team LAN --year 2026 --kind pitching
|
|
149
183
|
```
|
|
150
184
|
|
|
151
|
-
### 3) Batch projection (all players
|
|
185
|
+
### 3) Batch projection (all eligible players)
|
|
152
186
|
|
|
153
187
|
```bash
|
|
154
188
|
backcaster batch --year 2026 --kind batting
|
|
@@ -177,7 +211,8 @@ Example shape (columns vary by batting vs pitching):
|
|
|
177
211
|
```
|
|
178
212
|
|
|
179
213
|
### CSV output (`--format csv`)
|
|
180
|
-
A single-row CSV for `player`; multi-row CSV for `team` and `
|
|
214
|
+
A single-row CSV for `player`; multi-row CSV for `team`, `batch`, `hitters`, and `pitchers`.
|
|
215
|
+
Both populations include `PlayerID` for matching records across files.
|
|
181
216
|
|
|
182
217
|
### HTML output (`--format html`)
|
|
183
218
|
A basic HTML table (no custom CSS) suitable for quick sharing or embedding.
|
|
@@ -196,6 +231,7 @@ Lahman distribution from the same edition. Existing online caches are ignored.
|
|
|
196
231
|
|
|
197
232
|
Runtime dependencies are intentionally minimal:
|
|
198
233
|
- `pandas` for tabular data processing and projection output
|
|
234
|
+
- `numpy` for numerical operations and rebaselining convergence checks
|
|
199
235
|
|
|
200
236
|
Development dependencies (`--extra dev`) include:
|
|
201
237
|
- `pytest`, `pytest-cov`
|
|
@@ -12,11 +12,13 @@ Reference revision: `13a1c7943f5a7317c9c812a28e299c2184060046` (March 1, 2020).
|
|
|
12
12
|
See [reference and adaptation notes](docs/MARCELR_REFERENCE.md) for the files
|
|
13
13
|
reviewed, formulas examined, current differences, and attribution practices.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
Backcaster's pitcher-aging equations in `src/backcaster/marcel.py` are adapted
|
|
16
|
+
from the age-adjustment formulas in marcelR's `R/utilities.R` and their stat-specific
|
|
17
|
+
application in `inst/scripts/marcel_data_exporter.R` at the revision above.
|
|
18
|
+
The implementation is Python, with local birth-year validation and unchanged
|
|
19
|
+
pitching opportunities; it is not a complete port of marcelR. No marcelR datasets
|
|
20
|
+
are bundled. Backcaster does not depend on R or marcelR. Attribution does not
|
|
21
|
+
imply endorsement or verified numerical parity.
|
|
20
22
|
|
|
21
23
|
The following is marcelR's MIT license notice, reproduced without changes:
|
|
22
24
|
|
|
@@ -40,9 +40,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
40
40
|
p = argparse.ArgumentParser(prog="backcaster")
|
|
41
41
|
sub = p.add_subparsers(dest="command", required=True)
|
|
42
42
|
|
|
43
|
-
def add_common(sp: argparse.ArgumentParser) -> None:
|
|
43
|
+
def add_common(sp: argparse.ArgumentParser, kind: str | None = None) -> None:
|
|
44
44
|
sp.add_argument("--year", type=int, required=True)
|
|
45
|
-
|
|
45
|
+
if kind is None:
|
|
46
|
+
sp.add_argument("--kind", choices=["batting", "pitching"], required=True)
|
|
47
|
+
else:
|
|
48
|
+
sp.set_defaults(kind=kind)
|
|
46
49
|
sp.add_argument("--format", choices=_OUTPUT_FORMAT_CHOICES, default="cli")
|
|
47
50
|
sp.add_argument("--out")
|
|
48
51
|
sp.add_argument(
|
|
@@ -61,6 +64,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
61
64
|
|
|
62
65
|
bp = sub.add_parser("batch")
|
|
63
66
|
add_common(bp)
|
|
67
|
+
for command, kind in [("hitters", "batting"), ("pitchers", "pitching")]:
|
|
68
|
+
league = sub.add_parser(command, help=f"Project all eligible {command} across all teams")
|
|
69
|
+
add_common(league, kind)
|
|
70
|
+
league.set_defaults(command="batch")
|
|
64
71
|
return p
|
|
65
72
|
|
|
66
73
|
|
|
@@ -124,28 +124,33 @@ class LahmanSource:
|
|
|
124
124
|
if kind == "batting":
|
|
125
125
|
seasons, batting = self._batting_population(year)
|
|
126
126
|
else:
|
|
127
|
+
self.read("People", {"playerID", "nameFirst", "nameLast", "birthYear"})
|
|
127
128
|
seasons = pd.concat(
|
|
128
|
-
[self.season(y, kind) for y in (year - 1, year - 2, year - 3)],
|
|
129
|
+
[self.season(y, kind, qualified=False) for y in (year - 1, year - 2, year - 3)],
|
|
130
|
+
ignore_index=True,
|
|
129
131
|
)
|
|
132
|
+
eligible = seasons[seasons["Qualified"]]
|
|
130
133
|
if command == "player":
|
|
131
|
-
|
|
134
|
+
candidates = self._player_seasons(eligible, name)
|
|
135
|
+
matches = seasons[seasons["PlayerID"] == candidates["PlayerID"].iloc[0]]
|
|
132
136
|
if kind == "batting":
|
|
133
137
|
return batting[batting["PlayerID"] == matches["PlayerID"].iloc[0]].reset_index(
|
|
134
138
|
drop=True
|
|
135
139
|
)
|
|
136
140
|
return project_player(
|
|
137
|
-
str(matches["Name"].iloc[0]), matches, kind, year, league_df=
|
|
138
|
-
)
|
|
139
|
-
selected =
|
|
141
|
+
str(matches["Name"].iloc[0]), matches, kind, year, league_df=eligible
|
|
142
|
+
).assign(PlayerID=matches["PlayerID"].iloc[0])
|
|
143
|
+
selected = eligible
|
|
140
144
|
if command == "team":
|
|
141
|
-
selected =
|
|
145
|
+
selected = eligible[eligible["Team"].str.casefold() == (team or "").casefold()]
|
|
142
146
|
if selected.empty:
|
|
143
147
|
raise ProjectionError(
|
|
144
148
|
f"No qualified players for Lahman team ID '{team}' in the prior three seasons."
|
|
145
149
|
)
|
|
146
150
|
if kind == "batting":
|
|
147
151
|
return batting[batting["PlayerID"].isin(selected["PlayerID"])].reset_index(drop=True)
|
|
148
|
-
|
|
152
|
+
history = seasons[seasons["PlayerID"].isin(selected["PlayerID"])]
|
|
153
|
+
return project_team(history, kind, year, baseline_df=eligible)
|
|
149
154
|
|
|
150
155
|
def _batting_population(self, year: int) -> tuple[pd.DataFrame, pd.DataFrame]:
|
|
151
156
|
self.read("People", {"playerID", "nameFirst", "nameLast", "birthYear"})
|
|
@@ -178,7 +183,7 @@ class LahmanSource:
|
|
|
178
183
|
qualified = seasons[seasons["Qualified"]]
|
|
179
184
|
if qualified.empty:
|
|
180
185
|
raise ProjectionError("No qualified non-pitcher batters in the prior three seasons.")
|
|
181
|
-
return
|
|
186
|
+
return seasons, projected
|
|
182
187
|
|
|
183
188
|
def _player_seasons(self, seasons: pd.DataFrame, name: str | None) -> pd.DataFrame:
|
|
184
189
|
normalized = " ".join((name or "").casefold().split())
|
|
@@ -194,6 +199,4 @@ class LahmanSource:
|
|
|
194
199
|
raise PlayerLookupError(
|
|
195
200
|
f"Ambiguous Lahman name '{name}'; matching player IDs: {', '.join(ids)}."
|
|
196
201
|
)
|
|
197
|
-
if matches["Season"].nunique() != 3:
|
|
198
|
-
raise ProjectionError(f"Player '{name}' must qualify in all three prior seasons.")
|
|
199
202
|
return matches
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import math
|
|
4
|
+
|
|
3
5
|
import pandas as pd
|
|
4
6
|
|
|
5
7
|
from backcaster.normalize import safe_divide
|
|
@@ -14,9 +16,27 @@ BATTING_COMPONENTS = ["PA", "AB", "H", "2B", "3B", "HR", "BB", "SO", "HBP", "SF"
|
|
|
14
16
|
PITCHING_COMPONENTS = ["IP", "ER", "H", "HR", "BB", "SO", "BF"]
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
def _pitcher_age(prior: pd.DataFrame, year: int, fallback: float) -> float:
|
|
20
|
+
age = fallback
|
|
21
|
+
if "birthYear" in prior:
|
|
22
|
+
age = year - float(pd.to_numeric(prior["birthYear"].iloc[0], errors="coerce"))
|
|
23
|
+
if not math.isfinite(age) or not 15 <= age <= 70:
|
|
24
|
+
raise ProjectionError("A valid birthYear or explicit age is required for pitcher aging.")
|
|
25
|
+
return age
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _age_pitching(counts: pd.Series, age: float) -> pd.Series:
|
|
29
|
+
# Equations adapted from Ben Dilday's marcelR, utilities.R and
|
|
30
|
+
# inst/scripts/marcel_data_exporter.R, revision 13a1c7943f5a7317c9c812a28e299c2184060046.
|
|
31
|
+
# Copyright (c) 2017 Ben Dilday, MIT; see THIRD_PARTY_NOTICES.md.
|
|
32
|
+
benefit = 1 / (1 + 0.003 * (age - 29)) if age > 29 else 1 + 0.006 * (29 - age)
|
|
33
|
+
result = counts.copy()
|
|
34
|
+
result[["ER", "H", "HR", "BB"]] /= benefit
|
|
35
|
+
result["SO"] *= benefit
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _weighted_row(rows: list[pd.Series], weights: tuple[float, ...], cols: list[str]) -> pd.Series:
|
|
20
40
|
total_w = sum(weights[: len(rows)])
|
|
21
41
|
out = {}
|
|
22
42
|
for col in cols:
|
|
@@ -124,14 +144,20 @@ def project_player(
|
|
|
124
144
|
prior_df = _prepare_prior_df(prior_three, comps, optional_comps=optional_comps)
|
|
125
145
|
|
|
126
146
|
rows = [prior_df.iloc[i] for i in range(min(3, prior_df.shape[0]))]
|
|
127
|
-
|
|
147
|
+
weights: tuple[float, ...] = config.season_weights[: len(rows)]
|
|
148
|
+
if kind == "pitching":
|
|
149
|
+
calendar_weights: dict[float, float] = dict(
|
|
150
|
+
zip((year - 1, year - 2, year - 3), config.season_weights)
|
|
151
|
+
)
|
|
152
|
+
weights = tuple(calendar_weights.get(float(row["Season"]), 0.0) for row in rows)
|
|
153
|
+
weighted = _weighted_row(rows, weights, comps)
|
|
128
154
|
|
|
129
155
|
pt_col = comps[0]
|
|
130
156
|
weighted_pt = float(weighted[pt_col])
|
|
131
157
|
# Regression uses weighted evidence, not the per-season average used for
|
|
132
158
|
# playing time. Averaging first would multiply regression strength by the
|
|
133
159
|
# sum of the season weights (12 for three seasons with weights 5/4/3).
|
|
134
|
-
evidence = weighted * sum(
|
|
160
|
+
evidence = weighted * sum(weights)
|
|
135
161
|
evidence_pt = float(evidence[pt_col])
|
|
136
162
|
reliability = safe_divide(evidence_pt, evidence_pt + reg_pt)
|
|
137
163
|
|
|
@@ -148,7 +174,7 @@ def project_player(
|
|
|
148
174
|
regressed[c] = regressed_rate * weighted_pt
|
|
149
175
|
|
|
150
176
|
growth = config.default_pa_growth if kind == "batting" else config.default_ip_growth
|
|
151
|
-
age_adj = config.age_adjustment_fn(age)
|
|
177
|
+
age_adj = config.age_adjustment_fn(age) if kind == "batting" else 1.0
|
|
152
178
|
projected_pt = weighted_pt * growth * age_adj
|
|
153
179
|
pt_scale = safe_divide(projected_pt, weighted_pt)
|
|
154
180
|
regressed *= pt_scale
|
|
@@ -156,12 +182,16 @@ def project_player(
|
|
|
156
182
|
if kind == "batting":
|
|
157
183
|
rates = _derive_batting_rates(regressed)
|
|
158
184
|
else:
|
|
185
|
+
age = _pitcher_age(prior_df, year, age)
|
|
186
|
+
regressed = _age_pitching(regressed, age)
|
|
159
187
|
rates = _derive_pitching_rates(regressed)
|
|
160
188
|
|
|
161
189
|
output = pd.concat([regressed, rates])
|
|
162
190
|
output["Reliability"] = reliability
|
|
163
191
|
output["Name"] = player_name
|
|
164
192
|
output["Year"] = year
|
|
193
|
+
if kind == "pitching":
|
|
194
|
+
output["Age"] = age
|
|
165
195
|
return output.to_frame().T
|
|
166
196
|
|
|
167
197
|
|
|
@@ -195,16 +225,17 @@ def project_team(
|
|
|
195
225
|
display_name = sorted_grp["Name"].iloc[0]
|
|
196
226
|
if not isinstance(display_name, str):
|
|
197
227
|
raise ProjectionError("Encountered non-string player name in team input.")
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
league_rates=baseline_rates,
|
|
206
|
-
)
|
|
228
|
+
projection = project_player(
|
|
229
|
+
display_name,
|
|
230
|
+
sorted_grp.head(3),
|
|
231
|
+
kind,
|
|
232
|
+
year,
|
|
233
|
+
config,
|
|
234
|
+
league_rates=baseline_rates,
|
|
207
235
|
)
|
|
236
|
+
if "PlayerID" in sorted_grp:
|
|
237
|
+
projection["PlayerID"] = sorted_grp["PlayerID"].iloc[0]
|
|
238
|
+
projections.append(projection)
|
|
208
239
|
if not projections:
|
|
209
240
|
raise ProjectionError("No players available to project for this team.")
|
|
210
241
|
return pd.concat(projections, ignore_index=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backcaster
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -36,7 +36,7 @@ Backcaster is a Python CLI that produces **Marcel-style MLB projections** from t
|
|
|
36
36
|
It supports:
|
|
37
37
|
- **Single player projections** (`player`)
|
|
38
38
|
- **Single team projections** (`team`)
|
|
39
|
-
- **League-wide
|
|
39
|
+
- **League-wide projections** (`hitters`, `pitchers`, or `batch`)
|
|
40
40
|
- Output as terminal table, CSV, or HTML
|
|
41
41
|
|
|
42
42
|
---
|
|
@@ -56,10 +56,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
56
56
|
python -m pip install --upgrade backcaster
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
To install a specific release, such as 0.3.
|
|
59
|
+
To install a specific release, such as 0.3.6:
|
|
60
60
|
|
|
61
61
|
```powershell
|
|
62
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
62
|
+
python -m pip install --upgrade backcaster==0.3.6
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -97,19 +97,52 @@ pip install dist/backcaster-*.whl
|
|
|
97
97
|
|
|
98
98
|
## Quickstart CLI usage
|
|
99
99
|
|
|
100
|
-
All commands require
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
All commands require `--year`, the target projection year.
|
|
101
|
+
The `player`, `team`, and `batch` commands also require `--kind batting|pitching`.
|
|
102
|
+
The `hitters` and `pitchers` commands choose their kind automatically.
|
|
103
103
|
|
|
104
104
|
Subcommand-specific required options:
|
|
105
105
|
- `player` requires `--name`
|
|
106
106
|
- `team` requires `--team`
|
|
107
|
-
- `batch`
|
|
107
|
+
- `batch`, `hitters`, and `pitchers` require no player or team option
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
backcaster <player|team|batch> [subcommand-options] --year <YEAR> --kind <batting|pitching> [--format cli|csv|html] [--out PATH]
|
|
111
|
+
backcaster <hitters|pitchers> --year <YEAR> [--data-dir PATH] [--format cli|csv|html] [--out PATH]
|
|
111
112
|
```
|
|
112
113
|
|
|
114
|
+
### Pre-offseason league projections
|
|
115
|
+
|
|
116
|
+
Once the latest regular season is present in your local Lahman files, generate
|
|
117
|
+
both populations without roster updates or team filters. For terminal output:
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
backcaster hitters --year 2026
|
|
121
|
+
backcaster pitchers --year 2026
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
To save files for comparison with Baseball Reference:
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
backcaster hitters --year 2026 --format csv --out hitters_2026.csv
|
|
128
|
+
backcaster pitchers --year 2026 --format csv --out pitchers_2026.csv
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`--year 2026` projects 2026 from 2023–2025; it never reads 2026 statistics.
|
|
132
|
+
Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
|
|
133
|
+
They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
|
|
134
|
+
For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
|
|
135
|
+
For HTML, use `--format html --out hitters_2026.html` (or a pitcher output filename).
|
|
136
|
+
File formats require `--out`; the CLI format prints to the terminal.
|
|
137
|
+
|
|
138
|
+
Players qualify for selection by meeting the existing PA/IP threshold in at least
|
|
139
|
+
one of those three seasons. This is a historical eligibility list, not an active
|
|
140
|
+
roster; pitchers who never reach the qualifying innings threshold (including most
|
|
141
|
+
relievers) remain excluded. Qualification selects players without removing their
|
|
142
|
+
other seasons from the calculation. Trades are combined into one player-season
|
|
143
|
+
before qualification, and all teams' statistics contribute once. The output is
|
|
144
|
+
one row per selected player, ready for comparison with reference projections.
|
|
145
|
+
|
|
113
146
|
### 1) Player projection
|
|
114
147
|
|
|
115
148
|
```bash
|
|
@@ -123,8 +156,8 @@ All projections use local Lahman CSVs. On Windows, the default directory is
|
|
|
123
156
|
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
124
157
|
`--data-dir` to override it for one command. No network requests or online
|
|
125
158
|
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
126
|
-
`Batting.csv` or `Pitching.csv` for the selected kind.
|
|
127
|
-
|
|
159
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Both kinds require valid
|
|
160
|
+
`birthYear` values in `People.csv`; batters additionally require `Appearances.csv`. Filenames are matched
|
|
128
161
|
without regard to case. The database is not bundled with Backcaster.
|
|
129
162
|
|
|
130
163
|
For the default Windows directory, no additional flag is needed:
|
|
@@ -166,11 +199,12 @@ ambiguous name lookups report the matching IDs.
|
|
|
166
199
|
|
|
167
200
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
168
201
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
169
|
-
baseline
|
|
202
|
+
baseline, with real-age performance adjustments documented in
|
|
203
|
+
[the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
170
204
|
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
205
|
+
Eligibility means qualifying in at least one of the three preceding seasons.
|
|
206
|
+
For every selected player, projections use all available seasons in that window,
|
|
207
|
+
including nonqualified seasons and seasons spent with other teams. For team selection, a traded player's full-season totals are assigned
|
|
174
208
|
to the team in their latest stint for that season; this is not a current-roster forecast.
|
|
175
209
|
|
|
176
210
|
### 2) Team projection
|
|
@@ -179,7 +213,7 @@ to the team in their latest stint for that season; this is not a current-roster
|
|
|
179
213
|
backcaster team --team LAN --year 2026 --kind pitching
|
|
180
214
|
```
|
|
181
215
|
|
|
182
|
-
### 3) Batch projection (all players
|
|
216
|
+
### 3) Batch projection (all eligible players)
|
|
183
217
|
|
|
184
218
|
```bash
|
|
185
219
|
backcaster batch --year 2026 --kind batting
|
|
@@ -208,7 +242,8 @@ Example shape (columns vary by batting vs pitching):
|
|
|
208
242
|
```
|
|
209
243
|
|
|
210
244
|
### CSV output (`--format csv`)
|
|
211
|
-
A single-row CSV for `player`; multi-row CSV for `team` and `
|
|
245
|
+
A single-row CSV for `player`; multi-row CSV for `team`, `batch`, `hitters`, and `pitchers`.
|
|
246
|
+
Both populations include `PlayerID` for matching records across files.
|
|
212
247
|
|
|
213
248
|
### HTML output (`--format html`)
|
|
214
249
|
A basic HTML table (no custom CSS) suitable for quick sharing or embedding.
|
|
@@ -227,6 +262,7 @@ Lahman distribution from the same edition. Existing online caches are ignored.
|
|
|
227
262
|
|
|
228
263
|
Runtime dependencies are intentionally minimal:
|
|
229
264
|
- `pandas` for tabular data processing and projection output
|
|
265
|
+
- `numpy` for numerical operations and rebaselining convergence checks
|
|
230
266
|
|
|
231
267
|
Development dependencies (`--extra dev`) include:
|
|
232
268
|
- `pytest`, `pytest-cov`
|
|
@@ -102,8 +102,8 @@ def test_short_season_threshold(lahman: Path) -> None:
|
|
|
102
102
|
source = LahmanSource(str(lahman))
|
|
103
103
|
assert "star" in set(source.season(2020, "batting").PlayerID)
|
|
104
104
|
assert "star" not in set(source.season(2021, "batting").PlayerID)
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
result = source.project("player", 2022, "batting", name="Test Star")
|
|
106
|
+
assert result.loc[0, "PA"] == pytest.approx(0.5 * 190 + 0.1 * 190 + 200)
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
@pytest.mark.parametrize("command", ["player", "team", "batch"])
|
|
@@ -337,3 +337,84 @@ def test_missing_birth_year_column(lahman: Path) -> None:
|
|
|
337
337
|
source = LahmanSource(str(lahman))
|
|
338
338
|
with pytest.raises(DataFetchError, match="birthYear"):
|
|
339
339
|
source.project("batch", 2022, "batting")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_pitcher_aging_consistent_across_commands(lahman: Path) -> None:
|
|
343
|
+
source = LahmanSource(str(lahman))
|
|
344
|
+
player = source.project("player", 2022, "pitching", name="Test Star").iloc[0]
|
|
345
|
+
team = source.project("team", 2022, "pitching", team="BOS")
|
|
346
|
+
batch = source.project("batch", 2022, "pitching")
|
|
347
|
+
assert player["Age"] == 32
|
|
348
|
+
assert player["IP"] == pytest.approx(487 / 3)
|
|
349
|
+
for result in [team, batch]:
|
|
350
|
+
row = result[result.Name == "Test Star"].iloc[0]
|
|
351
|
+
for stat in ["IP", "SO", "ERA", "WHIP", "Age"]:
|
|
352
|
+
assert row[stat] == pytest.approx(player[stat])
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_pitcher_requires_birth_year(lahman: Path) -> None:
|
|
356
|
+
path = lahman / "People.csv"
|
|
357
|
+
pd.read_csv(path).drop(columns="birthYear").to_csv(path, index=False)
|
|
358
|
+
source = LahmanSource(str(lahman))
|
|
359
|
+
with pytest.raises(DataFetchError, match="birthYear"):
|
|
360
|
+
source.project("batch", 2022, "pitching")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@pytest.mark.parametrize("command, kind", [("hitters", "batting"), ("pitchers", "pitching")])
|
|
364
|
+
def test_league_commands_match_batch(lahman: Path, command: str, kind: str) -> None:
|
|
365
|
+
out = lahman / f"{command}.csv"
|
|
366
|
+
batch = lahman / "batch.csv"
|
|
367
|
+
common = ["--year", "2022", "--data-dir", str(lahman), "--format", "csv"]
|
|
368
|
+
assert main([command, *common, "--out", str(out)]) == 0
|
|
369
|
+
assert main(["batch", *common, "--kind", kind, "--out", str(batch)]) == 0
|
|
370
|
+
pd.testing.assert_frame_equal(pd.read_csv(out), pd.read_csv(batch))
|
|
371
|
+
assert len(pd.read_csv(out)) == 2
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_pitcher_full_history_survives_team_and_qualification_selection(lahman: Path) -> None:
|
|
375
|
+
path = lahman / "Pitching.csv"
|
|
376
|
+
frame = pd.read_csv(path)
|
|
377
|
+
frame.loc[(frame.playerID == "star") & (frame.yearID == 2019), "teamID"] = "LAN"
|
|
378
|
+
middle = (frame.playerID == "star") & (frame.yearID == 2020)
|
|
379
|
+
frame.loc[middle, ["teamID", "IPouts", "ER"]] = ["LAA", 90, 20]
|
|
380
|
+
frame.to_csv(path, index=False)
|
|
381
|
+
source = LahmanSource(str(lahman))
|
|
382
|
+
player = source.project("player", 2022, "pitching", name="Test Star").iloc[0]
|
|
383
|
+
team = source.project("team", 2022, "pitching", team="LAN").iloc[0]
|
|
384
|
+
batch = source.project("batch", 2022, "pitching")
|
|
385
|
+
row = batch[batch.Name == "Test Star"].iloc[0]
|
|
386
|
+
assert player["IP"] == pytest.approx((5 * 487 / 3 + 4 * 30 + 3 * 487 / 3) / 12)
|
|
387
|
+
for candidate in [team, row]:
|
|
388
|
+
for metric in ["IP", "ER", "ERA", "WHIP", "SO", "Reliability"]:
|
|
389
|
+
assert candidate[metric] == pytest.approx(player[metric])
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_pitcher_trade_stints_count_once_and_qualify_together(lahman: Path) -> None:
|
|
393
|
+
original = LahmanSource(str(lahman)).project("batch", 2022, "pitching")
|
|
394
|
+
path = lahman / "Pitching.csv"
|
|
395
|
+
frame = pd.read_csv(path)
|
|
396
|
+
mask = (frame.playerID == "star") & (frame.yearID == 2021)
|
|
397
|
+
row = frame[mask].copy()
|
|
398
|
+
frame = frame[~mask]
|
|
399
|
+
for column in ["IPouts", "ER", "H", "HR", "BB", "SO", "BFP"]:
|
|
400
|
+
row[column] = row[column] / 2
|
|
401
|
+
second = row.assign(stint=2, teamID="LAN")
|
|
402
|
+
pd.concat([frame, row, second]).to_csv(path, index=False)
|
|
403
|
+
source = LahmanSource(str(lahman))
|
|
404
|
+
season = source.season(2021, "pitching")
|
|
405
|
+
assert len(season[season.PlayerID == "star"]) == 1
|
|
406
|
+
actual = source.project("batch", 2022, "pitching")
|
|
407
|
+
pd.testing.assert_frame_equal(
|
|
408
|
+
actual.sort_values("PlayerID").reset_index(drop=True),
|
|
409
|
+
original.sort_values("PlayerID").reset_index(drop=True),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_pitcher_without_three_qualifying_seasons_can_be_selected(lahman: Path) -> None:
|
|
414
|
+
path = lahman / "Pitching.csv"
|
|
415
|
+
frame = pd.read_csv(path)
|
|
416
|
+
frame.loc[(frame.playerID == "star") & (frame.yearID != 2021), "IPouts"] = 90
|
|
417
|
+
frame.to_csv(path, index=False)
|
|
418
|
+
result = LahmanSource(str(lahman)).project("player", 2022, "pitching", name="Test Star")
|
|
419
|
+
assert len(result) == 1
|
|
420
|
+
assert result.loc[0, "IP"] == pytest.approx((5 * 487 / 3 + 4 * 30 + 3 * 30) / 12)
|
|
@@ -408,3 +408,45 @@ def test_one_season_regression_uses_only_available_weights() -> None:
|
|
|
408
408
|
result = project_player("One Season", prior, "batting", 2025, league_rates=rates)
|
|
409
409
|
assert float(result.loc[0, "HR"]) / float(result.loc[0, "PA"]) == pytest.approx(324 / 4200)
|
|
410
410
|
assert float(result.loc[0, "Reliability"]) == pytest.approx(3000 / 4200)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@pytest.mark.parametrize("age, factor", [(25, 1.024), (29, 1.0), (34, 1 / 1.015)])
|
|
414
|
+
def test_pitcher_age_curve_changes_rates_not_opportunities(age: int, factor: float) -> None:
|
|
415
|
+
neutral = project_player("Pitcher", _pitching_prior(), "pitching", 2026, age=29).iloc[0]
|
|
416
|
+
result = project_player("Pitcher", _pitching_prior(), "pitching", 2026, age=age).iloc[0]
|
|
417
|
+
assert result["IP"] == pytest.approx(neutral["IP"])
|
|
418
|
+
assert result["BF"] == pytest.approx(neutral["BF"])
|
|
419
|
+
assert result["Reliability"] == pytest.approx(neutral["Reliability"])
|
|
420
|
+
for stat in ["ER", "H", "HR", "BB", "ERA", "WHIP"]:
|
|
421
|
+
assert result[stat] == pytest.approx(neutral[stat] / factor)
|
|
422
|
+
assert result["SO"] == pytest.approx(neutral["SO"] * factor)
|
|
423
|
+
assert result["Age"] == age
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_pitcher_birth_year_overrides_default_age() -> None:
|
|
427
|
+
prior = _pitching_prior().assign(birthYear=1992)
|
|
428
|
+
actual = project_player("Pitcher", prior, "pitching", 2026)
|
|
429
|
+
expected = project_player("Pitcher", _pitching_prior(), "pitching", 2026, age=34)
|
|
430
|
+
pd.testing.assert_frame_equal(actual, expected)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@pytest.mark.parametrize("birth", [float("nan"), float("inf"), "unknown", 1800, 2025])
|
|
434
|
+
def test_pitcher_invalid_birth_year(birth: object) -> None:
|
|
435
|
+
prior = _pitching_prior().assign(birthYear=birth)
|
|
436
|
+
with pytest.raises(ProjectionError, match="valid birthYear"):
|
|
437
|
+
project_player("Pitcher", prior, "pitching", 2026)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def test_pitcher_missing_year_keeps_calendar_weights() -> None:
|
|
441
|
+
prior = _pitching_prior()
|
|
442
|
+
prior = prior[prior.Season != 2024]
|
|
443
|
+
result = project_player("Pitcher", prior, "pitching", 2026, age=29).iloc[0]
|
|
444
|
+
assert result["IP"] == pytest.approx((5 * 200 + 3 * 210) / 8)
|
|
445
|
+
assert result["Reliability"] == pytest.approx(1630 / 1830)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_pitcher_future_season_has_no_evidence() -> None:
|
|
449
|
+
prior = _pitching_prior().iloc[[0]].assign(Season=2026)
|
|
450
|
+
result = project_player("Pitcher", prior, "pitching", 2026, age=29).iloc[0]
|
|
451
|
+
assert result["IP"] == 0
|
|
452
|
+
assert result["Reliability"] == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|