backcaster 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.
- {backcaster-0.3.2/src/backcaster.egg-info → backcaster-0.3.4}/PKG-INFO +48 -34
- {backcaster-0.3.2 → backcaster-0.3.4}/README.md +44 -31
- backcaster-0.3.4/THIRD_PARTY_NOTICES.md +52 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/pyproject.toml +4 -4
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/__init__.py +1 -1
- backcaster-0.3.4/src/backcaster/batters.py +118 -0
- backcaster-0.3.4/src/backcaster/cli.py +86 -0
- backcaster-0.3.4/src/backcaster/data.py +9 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/lahman.py +78 -22
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/marcel.py +7 -2
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/schemas.py +0 -1
- {backcaster-0.3.2 → backcaster-0.3.4/src/backcaster.egg-info}/PKG-INFO +48 -34
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/SOURCES.txt +4 -4
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/requires.txt +1 -1
- backcaster-0.3.4/tests/test_batters.py +120 -0
- backcaster-0.3.4/tests/test_cli.py +102 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_lahman.py +110 -10
- {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_marcel_math.py +49 -0
- backcaster-0.3.2/src/backcaster/cli.py +0 -176
- backcaster-0.3.2/src/backcaster/data.py +0 -246
- backcaster-0.3.2/tests/test_cli.py +0 -259
- backcaster-0.3.2/tests/test_cli_player_lookup.py +0 -60
- backcaster-0.3.2/tests/test_data.py +0 -412
- backcaster-0.3.2/tests/test_player_lookup.py +0 -89
- {backcaster-0.3.2 → backcaster-0.3.4}/LICENSE +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/setup.cfg +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/normalize.py +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/outputs.py +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/dependency_links.txt +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/entry_points.txt +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/top_level.txt +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_normalize.py +0 -0
- {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_outputs.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backcaster
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Marcel-style MLB projection CLI
|
|
3
|
+
Version: 0.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
|
|
7
7
|
Project-URL: Repository, https://github.com/jeffreywevans/backcaster
|
|
@@ -17,8 +17,9 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
|
17
17
|
Requires-Python: >=3.13
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
20
21
|
Requires-Dist: pandas>=2.0
|
|
21
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: numpy>=2.0
|
|
22
23
|
Provides-Extra: dev
|
|
23
24
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
24
25
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
@@ -30,7 +31,7 @@ Dynamic: license-file
|
|
|
30
31
|
|
|
31
32
|
# Backcaster
|
|
32
33
|
|
|
33
|
-
Backcaster is a Python CLI that produces **Marcel-style MLB projections** from the prior three seasons of data using
|
|
34
|
+
Backcaster is a Python CLI that produces **Marcel-style MLB projections** from the prior three seasons of data using local Lahman CSV files and `pandas`.
|
|
34
35
|
|
|
35
36
|
It supports:
|
|
36
37
|
- **Single player projections** (`player`)
|
|
@@ -44,7 +45,8 @@ It supports:
|
|
|
44
45
|
|
|
45
46
|
### Requirements
|
|
46
47
|
- Python **3.13+**
|
|
47
|
-
-
|
|
48
|
+
- An extracted Lahman CSV database covering the three prior seasons
|
|
49
|
+
- Internet access for package installation only; projections run offline
|
|
48
50
|
|
|
49
51
|
### Update an installed package
|
|
50
52
|
|
|
@@ -54,10 +56,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
54
56
|
python -m pip install --upgrade backcaster
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
To install a specific release, such as 0.3.
|
|
59
|
+
To install a specific release, such as 0.3.4:
|
|
58
60
|
|
|
59
61
|
```powershell
|
|
60
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
62
|
+
python -m pip install --upgrade backcaster==0.3.4
|
|
61
63
|
```
|
|
62
64
|
|
|
63
65
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -116,12 +118,21 @@ backcaster player --name "Mookie Betts" --year 2026 --kind batting
|
|
|
116
118
|
|
|
117
119
|
### Offline projections with Lahman CSVs
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`
|
|
121
|
+
All projections use local Lahman CSVs. On Windows, the default directory is
|
|
122
|
+
`C:\Baseball\lahman`; on other systems it is `~/Baseball/lahman`.
|
|
123
|
+
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
124
|
+
`--data-dir` to override it for one command. No network requests or online
|
|
125
|
+
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
126
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Batter projections also
|
|
127
|
+
require `Appearances.csv` and a valid `birthYear` in `People.csv`. Filenames are matched
|
|
123
128
|
without regard to case. The database is not bundled with Backcaster.
|
|
124
129
|
|
|
130
|
+
For the default Windows directory, no additional flag is needed:
|
|
131
|
+
|
|
132
|
+
```powershell
|
|
133
|
+
backcaster player --name "Mookie Betts" --year 2024 --kind batting
|
|
134
|
+
```
|
|
135
|
+
|
|
125
136
|
See [Lahman database notes](docs/LAHMAN.md) for the supplied 1871–2025 edition's
|
|
126
137
|
coverage, attribution, license, and historical-data caveats, with its original README.
|
|
127
138
|
|
|
@@ -134,8 +145,8 @@ backcaster batch --year 2022 --kind pitching --data-dir "C:\Baseball\lahman" --f
|
|
|
134
145
|
Use Lahman's `teamID` values, such as `LAN` for the Dodgers (rather than the
|
|
135
146
|
FanGraphs abbreviation `LAD`). A projection year needs the preceding three seasons:
|
|
136
147
|
2022 requires 2019–2021. Missing seasons produce an error explaining which year
|
|
137
|
-
is unavailable.
|
|
138
|
-
|
|
148
|
+
is unavailable. If the directory or required files are missing, the command
|
|
149
|
+
reports the missing file and directory; it never attempts an online fetch.
|
|
139
150
|
|
|
140
151
|
Offline qualification uses a consistent modern rule across historical seasons:
|
|
141
152
|
at least **3.1 PA per game** for hitters and **one inning per game** for pitchers.
|
|
@@ -153,7 +164,10 @@ Pitching `IPouts` is divided by three for decimal innings, and `BFP` supplies
|
|
|
153
164
|
batters faced. Lahman player IDs keep different players with the same name separate;
|
|
154
165
|
ambiguous name lookups report the matching IDs.
|
|
155
166
|
|
|
156
|
-
|
|
167
|
+
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
168
|
+
qualification selects the displayed players. Pitching retains its qualified-only
|
|
169
|
+
baseline. See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
170
|
+
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
157
171
|
Single-player projections require qualification in all three preceding seasons.
|
|
158
172
|
Team and batch projections use each player's available qualified seasons within
|
|
159
173
|
that window. For team selection, a traded player's full-season totals are assigned
|
|
@@ -162,7 +176,7 @@ to the team in their latest stint for that season; this is not a current-roster
|
|
|
162
176
|
### 2) Team projection
|
|
163
177
|
|
|
164
178
|
```bash
|
|
165
|
-
backcaster team --team
|
|
179
|
+
backcaster team --team LAN --year 2026 --kind pitching
|
|
166
180
|
```
|
|
167
181
|
|
|
168
182
|
### 3) Batch projection (all players returned by source data)
|
|
@@ -175,7 +189,7 @@ backcaster batch --year 2026 --kind batting
|
|
|
175
189
|
|
|
176
190
|
```bash
|
|
177
191
|
backcaster player --name "Mookie Betts" --year 2026 --kind batting --format csv --out ./out/mookie_2026.csv
|
|
178
|
-
backcaster team --team
|
|
192
|
+
backcaster team --team LAN --year 2026 --kind pitching --format html --out ./out/lan_pitching_2026.html
|
|
179
193
|
```
|
|
180
194
|
|
|
181
195
|
> `--out` is required for `csv` and `html` formats.
|
|
@@ -201,24 +215,11 @@ A basic HTML table (no custom CSS) suitable for quick sharing or embedding.
|
|
|
201
215
|
|
|
202
216
|
---
|
|
203
217
|
|
|
204
|
-
##
|
|
205
|
-
|
|
206
|
-
Backcaster caches season stat pulls under:
|
|
207
|
-
|
|
208
|
-
```text
|
|
209
|
-
~/.cache/backcaster/
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Cache details:
|
|
213
|
-
- Key pattern: `<kind>_<year>.parquet` (preferred) or `<kind>_<year>.csv` (fallback)
|
|
214
|
-
- Read order: Parquet first, then CSV
|
|
215
|
-
- Write behavior: atomic temp-file write + rename to reduce corruption risk
|
|
216
|
-
- Parquet write failures automatically fall back to CSV
|
|
218
|
+
## Local data updates
|
|
217
219
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
- If upstream data changes and you need a refresh, delete the relevant cache file(s) and rerun.
|
|
220
|
+
Backcaster reads the local CSV files for each command and does not use the old
|
|
221
|
+
FanGraphs season cache. To refresh the data, replace the CSVs with a complete
|
|
222
|
+
Lahman distribution from the same edition. Existing online caches are ignored.
|
|
222
223
|
|
|
223
224
|
---
|
|
224
225
|
|
|
@@ -226,7 +227,6 @@ Practical implications:
|
|
|
226
227
|
|
|
227
228
|
Runtime dependencies are intentionally minimal:
|
|
228
229
|
- `pandas` for tabular data processing and projection output
|
|
229
|
-
- `pybaseball` for MLB player/team seasonal stat retrieval and player ID lookup
|
|
230
230
|
|
|
231
231
|
Development dependencies (`--extra dev`) include:
|
|
232
232
|
- `pytest`, `pytest-cov`
|
|
@@ -298,6 +298,20 @@ Sonar consumes:
|
|
|
298
298
|
|
|
299
299
|
---
|
|
300
300
|
|
|
301
|
+
## Acknowledgments
|
|
302
|
+
|
|
303
|
+
Thank you to **Tom Tango** for developing and publishing the Marcel system, and
|
|
304
|
+
to **Ben Dilday** for [marcelR](https://github.com/bdilday/marcelR), whose open R
|
|
305
|
+
implementation provides a valuable methodological reference for our Python work.
|
|
306
|
+
We document the reviewed formulas, source revision, and implementation differences
|
|
307
|
+
in [our reference notes](docs/MARCELR_REFERENCE.md). The complete marcelR MIT
|
|
308
|
+
notice is preserved in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and shipped
|
|
309
|
+
with our distributions.
|
|
310
|
+
|
|
311
|
+
Thanks also to SABR, Sean Lahman, and the database contributors. See
|
|
312
|
+
[Lahman attribution and licensing](docs/LAHMAN.md). These acknowledgments do not
|
|
313
|
+
imply endorsement or exact agreement with another implementation's projections.
|
|
314
|
+
|
|
301
315
|
## Security and responsible use
|
|
302
316
|
|
|
303
317
|
Please review `SECURITY.md` for reporting guidance. Backcaster is an analytical utility and should be validated independently before use in production pipelines.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Backcaster
|
|
2
2
|
|
|
3
|
-
Backcaster is a Python CLI that produces **Marcel-style MLB projections** from the prior three seasons of data using
|
|
3
|
+
Backcaster is a Python CLI that produces **Marcel-style MLB projections** from the prior three seasons of data using local Lahman CSV files and `pandas`.
|
|
4
4
|
|
|
5
5
|
It supports:
|
|
6
6
|
- **Single player projections** (`player`)
|
|
@@ -14,7 +14,8 @@ It supports:
|
|
|
14
14
|
|
|
15
15
|
### Requirements
|
|
16
16
|
- Python **3.13+**
|
|
17
|
-
-
|
|
17
|
+
- An extracted Lahman CSV database covering the three prior seasons
|
|
18
|
+
- Internet access for package installation only; projections run offline
|
|
18
19
|
|
|
19
20
|
### Update an installed package
|
|
20
21
|
|
|
@@ -24,10 +25,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
24
25
|
python -m pip install --upgrade backcaster
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
To install a specific release, such as 0.3.
|
|
28
|
+
To install a specific release, such as 0.3.4:
|
|
28
29
|
|
|
29
30
|
```powershell
|
|
30
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
31
|
+
python -m pip install --upgrade backcaster==0.3.4
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -86,12 +87,21 @@ backcaster player --name "Mookie Betts" --year 2026 --kind batting
|
|
|
86
87
|
|
|
87
88
|
### Offline projections with Lahman CSVs
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
90
|
+
All projections use local Lahman CSVs. On Windows, the default directory is
|
|
91
|
+
`C:\Baseball\lahman`; on other systems it is `~/Baseball/lahman`.
|
|
92
|
+
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
93
|
+
`--data-dir` to override it for one command. No network requests or online
|
|
94
|
+
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
95
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Batter projections also
|
|
96
|
+
require `Appearances.csv` and a valid `birthYear` in `People.csv`. Filenames are matched
|
|
93
97
|
without regard to case. The database is not bundled with Backcaster.
|
|
94
98
|
|
|
99
|
+
For the default Windows directory, no additional flag is needed:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
backcaster player --name "Mookie Betts" --year 2024 --kind batting
|
|
103
|
+
```
|
|
104
|
+
|
|
95
105
|
See [Lahman database notes](docs/LAHMAN.md) for the supplied 1871–2025 edition's
|
|
96
106
|
coverage, attribution, license, and historical-data caveats, with its original README.
|
|
97
107
|
|
|
@@ -104,8 +114,8 @@ backcaster batch --year 2022 --kind pitching --data-dir "C:\Baseball\lahman" --f
|
|
|
104
114
|
Use Lahman's `teamID` values, such as `LAN` for the Dodgers (rather than the
|
|
105
115
|
FanGraphs abbreviation `LAD`). A projection year needs the preceding three seasons:
|
|
106
116
|
2022 requires 2019–2021. Missing seasons produce an error explaining which year
|
|
107
|
-
is unavailable.
|
|
108
|
-
|
|
117
|
+
is unavailable. If the directory or required files are missing, the command
|
|
118
|
+
reports the missing file and directory; it never attempts an online fetch.
|
|
109
119
|
|
|
110
120
|
Offline qualification uses a consistent modern rule across historical seasons:
|
|
111
121
|
at least **3.1 PA per game** for hitters and **one inning per game** for pitchers.
|
|
@@ -123,7 +133,10 @@ Pitching `IPouts` is divided by three for decimal innings, and `BFP` supplies
|
|
|
123
133
|
batters faced. Lahman player IDs keep different players with the same name separate;
|
|
124
134
|
ambiguous name lookups report the matching IDs.
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
137
|
+
qualification selects the displayed players. Pitching retains its qualified-only
|
|
138
|
+
baseline. See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
139
|
+
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
127
140
|
Single-player projections require qualification in all three preceding seasons.
|
|
128
141
|
Team and batch projections use each player's available qualified seasons within
|
|
129
142
|
that window. For team selection, a traded player's full-season totals are assigned
|
|
@@ -132,7 +145,7 @@ to the team in their latest stint for that season; this is not a current-roster
|
|
|
132
145
|
### 2) Team projection
|
|
133
146
|
|
|
134
147
|
```bash
|
|
135
|
-
backcaster team --team
|
|
148
|
+
backcaster team --team LAN --year 2026 --kind pitching
|
|
136
149
|
```
|
|
137
150
|
|
|
138
151
|
### 3) Batch projection (all players returned by source data)
|
|
@@ -145,7 +158,7 @@ backcaster batch --year 2026 --kind batting
|
|
|
145
158
|
|
|
146
159
|
```bash
|
|
147
160
|
backcaster player --name "Mookie Betts" --year 2026 --kind batting --format csv --out ./out/mookie_2026.csv
|
|
148
|
-
backcaster team --team
|
|
161
|
+
backcaster team --team LAN --year 2026 --kind pitching --format html --out ./out/lan_pitching_2026.html
|
|
149
162
|
```
|
|
150
163
|
|
|
151
164
|
> `--out` is required for `csv` and `html` formats.
|
|
@@ -171,24 +184,11 @@ A basic HTML table (no custom CSS) suitable for quick sharing or embedding.
|
|
|
171
184
|
|
|
172
185
|
---
|
|
173
186
|
|
|
174
|
-
##
|
|
175
|
-
|
|
176
|
-
Backcaster caches season stat pulls under:
|
|
177
|
-
|
|
178
|
-
```text
|
|
179
|
-
~/.cache/backcaster/
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Cache details:
|
|
183
|
-
- Key pattern: `<kind>_<year>.parquet` (preferred) or `<kind>_<year>.csv` (fallback)
|
|
184
|
-
- Read order: Parquet first, then CSV
|
|
185
|
-
- Write behavior: atomic temp-file write + rename to reduce corruption risk
|
|
186
|
-
- Parquet write failures automatically fall back to CSV
|
|
187
|
+
## Local data updates
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
- If upstream data changes and you need a refresh, delete the relevant cache file(s) and rerun.
|
|
189
|
+
Backcaster reads the local CSV files for each command and does not use the old
|
|
190
|
+
FanGraphs season cache. To refresh the data, replace the CSVs with a complete
|
|
191
|
+
Lahman distribution from the same edition. Existing online caches are ignored.
|
|
192
192
|
|
|
193
193
|
---
|
|
194
194
|
|
|
@@ -196,7 +196,6 @@ Practical implications:
|
|
|
196
196
|
|
|
197
197
|
Runtime dependencies are intentionally minimal:
|
|
198
198
|
- `pandas` for tabular data processing and projection output
|
|
199
|
-
- `pybaseball` for MLB player/team seasonal stat retrieval and player ID lookup
|
|
200
199
|
|
|
201
200
|
Development dependencies (`--extra dev`) include:
|
|
202
201
|
- `pytest`, `pytest-cov`
|
|
@@ -268,6 +267,20 @@ Sonar consumes:
|
|
|
268
267
|
|
|
269
268
|
---
|
|
270
269
|
|
|
270
|
+
## Acknowledgments
|
|
271
|
+
|
|
272
|
+
Thank you to **Tom Tango** for developing and publishing the Marcel system, and
|
|
273
|
+
to **Ben Dilday** for [marcelR](https://github.com/bdilday/marcelR), whose open R
|
|
274
|
+
implementation provides a valuable methodological reference for our Python work.
|
|
275
|
+
We document the reviewed formulas, source revision, and implementation differences
|
|
276
|
+
in [our reference notes](docs/MARCELR_REFERENCE.md). The complete marcelR MIT
|
|
277
|
+
notice is preserved in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and shipped
|
|
278
|
+
with our distributions.
|
|
279
|
+
|
|
280
|
+
Thanks also to SABR, Sean Lahman, and the database contributors. See
|
|
281
|
+
[Lahman attribution and licensing](docs/LAHMAN.md). These acknowledgments do not
|
|
282
|
+
imply endorsement or exact agreement with another implementation's projections.
|
|
283
|
+
|
|
271
284
|
## Security and responsible use
|
|
272
285
|
|
|
273
286
|
Please review `SECURITY.md` for reporting guidance. Backcaster is an analytical utility and should be validated independently before use in production pipelines.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Third-party acknowledgments and notices
|
|
2
|
+
|
|
3
|
+
## marcelR — Ben Dilday
|
|
4
|
+
|
|
5
|
+
Backcaster is a Python implementation of Marcel-style baseball projections.
|
|
6
|
+
[marcelR](https://github.com/bdilday/marcelR), by Ben Dilday, is an independent R
|
|
7
|
+
implementation we reviewed as a methodological reference and source of inspiration.
|
|
8
|
+
Thank you to Ben Dilday for sharing readable formulas, intermediate calculations,
|
|
9
|
+
and historical comparison examples that help make this work reproducible.
|
|
10
|
+
|
|
11
|
+
Reference revision: `13a1c7943f5a7317c9c812a28e299c2184060046` (March 1, 2020).
|
|
12
|
+
See [reference and adaptation notes](docs/MARCELR_REFERENCE.md) for the files
|
|
13
|
+
reviewed, formulas examined, current differences, and attribution practices.
|
|
14
|
+
|
|
15
|
+
At the time this notice was added, marcelR source had been inspected, but no R
|
|
16
|
+
source had been copied or translated into Backcaster, and its datasets had not
|
|
17
|
+
been bundled. Backcaster does not depend on R or marcelR. The notice is retained
|
|
18
|
+
for acknowledgment and to accompany future attributed adaptations. Neither the
|
|
19
|
+
review nor the acknowledgment implies endorsement or verified numerical parity.
|
|
20
|
+
|
|
21
|
+
The following is marcelR's MIT license notice, reproduced without changes:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
MIT License
|
|
25
|
+
|
|
26
|
+
Copyright (c) 2017 Ben Dilday
|
|
27
|
+
|
|
28
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
29
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
30
|
+
in the Software without restriction, including without limitation the rights
|
|
31
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
32
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
33
|
+
furnished to do so, subject to the following conditions:
|
|
34
|
+
|
|
35
|
+
The above copyright notice and this permission notice shall be included in all
|
|
36
|
+
copies or substantial portions of the Software.
|
|
37
|
+
|
|
38
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
39
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
40
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
41
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
42
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
43
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
44
|
+
SOFTWARE.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Method and data credits
|
|
48
|
+
|
|
49
|
+
Tom Tango developed the Marcel projection system. We thank him for publishing
|
|
50
|
+
its methodology. SABR, Sean Lahman, and the contributors to the Lahman database
|
|
51
|
+
provide the historical data on which local projections rely. The Lahman data
|
|
52
|
+
license is separate; see [database attribution and caveats](docs/LAHMAN.md).
|
|
@@ -5,10 +5,10 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "backcaster"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "Marcel-style MLB projection CLI
|
|
8
|
+
description = "Marcel-style MLB projection CLI using local Lahman CSV data"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
|
-
license-files = ["LICENSE"]
|
|
11
|
+
license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"]
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
14
|
"Programming Language :: Python :: 3.13",
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
requires-python = ">=3.13"
|
|
23
23
|
dependencies = [
|
|
24
24
|
"pandas>=2.0",
|
|
25
|
-
"
|
|
25
|
+
"numpy>=2.0",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
@@ -85,5 +85,5 @@ no_implicit_optional = true
|
|
|
85
85
|
check_untyped_defs = true
|
|
86
86
|
|
|
87
87
|
[[tool.mypy.overrides]]
|
|
88
|
-
module = ["pandas", "pandas.*"
|
|
88
|
+
module = ["pandas", "pandas.*"]
|
|
89
89
|
ignore_missing_imports = true
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Population-level batter projections; no projected-year statistics are read."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from backcaster.marcel import BATTING_COMPONENTS, ProjectionError
|
|
9
|
+
|
|
10
|
+
EVENTS = ["1B", "2B", "3B", "HR", "BB", "HBP", "SO", "SH", "SF", "OUT"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _events(frame: pd.DataFrame) -> pd.DataFrame:
|
|
14
|
+
events = frame.reindex(columns=EVENTS, fill_value=0).astype(float)
|
|
15
|
+
events["1B"] = frame["H"] - frame["2B"] - frame["3B"] - frame["HR"]
|
|
16
|
+
events["OUT"] = frame["AB"] - frame["H"] - frame["SO"]
|
|
17
|
+
if not np.isfinite(events.to_numpy()).all() or (events < 0).any().any():
|
|
18
|
+
raise ProjectionError(
|
|
19
|
+
"Batter counts must be finite and consistent (H <= AB, SO <= AB - H)."
|
|
20
|
+
)
|
|
21
|
+
return events
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _rebaseline(events: pd.DataFrame, pa: pd.Series, target: pd.Series) -> pd.DataFrame:
|
|
25
|
+
"""Rake mutually exclusive events to fixed player PA and league event totals."""
|
|
26
|
+
desired = target * float(pa.sum())
|
|
27
|
+
# Positive league outcomes must be reachable, including a historically absent
|
|
28
|
+
# event in the projected population. Seed only those cells, then rake.
|
|
29
|
+
values = events.clip(lower=1e-12).mul(desired.gt(0), axis=1)
|
|
30
|
+
for _ in range(200):
|
|
31
|
+
values = values.mul(desired.div(values.sum()).fillna(0), axis=1)
|
|
32
|
+
values = values.mul(pa.div(values.sum(axis=1)).fillna(0), axis=0)
|
|
33
|
+
if np.allclose(values.sum(), desired, rtol=1e-10, atol=1e-8):
|
|
34
|
+
return values
|
|
35
|
+
raise ProjectionError("Batter league rebaselining did not converge.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def project_batters(seasons: pd.DataFrame, year: int) -> pd.DataFrame:
|
|
39
|
+
"""Use all non-pitcher seasons for both regression and rebaselining."""
|
|
40
|
+
history = seasons[seasons["Season"].isin([year - 1, year - 2, year - 3])].copy()
|
|
41
|
+
if history.empty:
|
|
42
|
+
raise ProjectionError("No non-pitcher batting history in the three prior seasons.")
|
|
43
|
+
history = history[history["PA"] > 0].copy()
|
|
44
|
+
counts = _events(history)
|
|
45
|
+
weights = history["Season"].map({year - 1: 5.0, year - 2: 4.0, year - 3: 3.0})
|
|
46
|
+
history["EvidencePA"] = history["PA"] * weights
|
|
47
|
+
league_counts = counts.groupby(history["Season"]).sum()
|
|
48
|
+
league_pa = history.groupby("Season")["PA"].sum()
|
|
49
|
+
if year - 1 not in league_pa:
|
|
50
|
+
raise ProjectionError("Latest completed season has no non-pitcher batting PA.")
|
|
51
|
+
league_rates = league_counts.div(league_pa, axis=0)
|
|
52
|
+
# Each player's exposure to each season determines their league average.
|
|
53
|
+
expected = (
|
|
54
|
+
league_rates.reindex(history["Season"])
|
|
55
|
+
.set_axis(history.index)
|
|
56
|
+
.mul(history["EvidencePA"], axis=0)
|
|
57
|
+
)
|
|
58
|
+
evidence = counts.mul(weights, axis=0).groupby(history["PlayerID"]).sum()
|
|
59
|
+
evidence_pa = history.groupby("PlayerID")["EvidencePA"].sum()
|
|
60
|
+
baseline = expected.groupby(history["PlayerID"]).sum().div(evidence_pa, axis=0)
|
|
61
|
+
rates = (evidence + 1200 * baseline).div(evidence_pa + 1200, axis=0)
|
|
62
|
+
prior_pa = (
|
|
63
|
+
history.pivot_table(index="PlayerID", columns="Season", values="PA", aggfunc="sum")
|
|
64
|
+
.reindex(columns=[year - 1, year - 2], fill_value=0)
|
|
65
|
+
.fillna(0)
|
|
66
|
+
)
|
|
67
|
+
pa = 0.5 * prior_pa[year - 1] + 0.1 * prior_pa[year - 2] + 200
|
|
68
|
+
people = (
|
|
69
|
+
history.sort_values("Season")
|
|
70
|
+
.drop_duplicates("PlayerID", keep="last")
|
|
71
|
+
.set_index("PlayerID")
|
|
72
|
+
.reindex(rates.index)
|
|
73
|
+
)
|
|
74
|
+
birth = pd.to_numeric(people["birthYear"], errors="coerce")
|
|
75
|
+
age = year - birth
|
|
76
|
+
if birth.isna().any() or not np.isfinite(age).all() or ((age < 15) | (age > 70)).any():
|
|
77
|
+
raise ProjectionError(
|
|
78
|
+
"Valid birthYear is required for every batter in the projection population."
|
|
79
|
+
)
|
|
80
|
+
adjustment = (age - 29) * np.where(age > 29, 0.003, 0.006)
|
|
81
|
+
rates[["1B", "2B", "3B", "HR", "BB", "HBP"]] = rates[["1B", "2B", "3B", "HR", "BB", "HBP"]].mul(
|
|
82
|
+
1 - adjustment, axis=0
|
|
83
|
+
)
|
|
84
|
+
rates[["SO", "OUT"]] = rates[["SO", "OUT"]].mul(1 + adjustment, axis=0)
|
|
85
|
+
rates = rates.div(rates.sum(axis=1), axis=0)
|
|
86
|
+
projected = _rebaseline(rates.mul(pa, axis=0), pa, league_rates.loc[[year - 1]].iloc[0])
|
|
87
|
+
out = projected.copy()
|
|
88
|
+
out["H"] = projected[["1B", "2B", "3B", "HR"]].sum(axis=1)
|
|
89
|
+
out["AB"] = out["H"] + projected["SO"] + projected["OUT"]
|
|
90
|
+
out["PA"] = pa
|
|
91
|
+
out["AVG"] = out["H"] / out["AB"].replace(0, np.nan)
|
|
92
|
+
out["OBP"] = (out["H"] + out["BB"] + out["HBP"]) / (
|
|
93
|
+
out["AB"] + out["BB"] + out["HBP"] + out["SF"]
|
|
94
|
+
).replace(0, np.nan)
|
|
95
|
+
out["SLG"] = (projected["1B"] + 2 * out["2B"] + 3 * out["3B"] + 4 * out["HR"]) / out[
|
|
96
|
+
"AB"
|
|
97
|
+
].replace(0, np.nan)
|
|
98
|
+
out["OPS"] = out["OBP"] + out["SLG"]
|
|
99
|
+
out["Reliability"] = evidence_pa / (evidence_pa + 1200)
|
|
100
|
+
out["Age"] = age
|
|
101
|
+
out["Name"] = people["Name"]
|
|
102
|
+
out["Year"] = year
|
|
103
|
+
out = out.fillna(0).drop(columns=["1B", "OUT"]).reset_index()
|
|
104
|
+
return out[
|
|
105
|
+
[
|
|
106
|
+
"PlayerID",
|
|
107
|
+
*BATTING_COMPONENTS,
|
|
108
|
+
"SH",
|
|
109
|
+
"AVG",
|
|
110
|
+
"OBP",
|
|
111
|
+
"SLG",
|
|
112
|
+
"OPS",
|
|
113
|
+
"Reliability",
|
|
114
|
+
"Age",
|
|
115
|
+
"Name",
|
|
116
|
+
"Year",
|
|
117
|
+
]
|
|
118
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from backcaster.data import DataFetchError, PlayerLookupError
|
|
11
|
+
from backcaster.lahman import LahmanSource
|
|
12
|
+
from backcaster.marcel import ProjectionError
|
|
13
|
+
from backcaster.outputs import to_cli_table, to_csv, to_html
|
|
14
|
+
|
|
15
|
+
_FILE_RENDERERS = {"csv": to_csv, "html": to_html}
|
|
16
|
+
_OUTPUT_FORMAT_CHOICES = ("cli", *_FILE_RENDERERS.keys())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_data_dir() -> str:
|
|
20
|
+
default = (
|
|
21
|
+
"C:/Baseball/lahman"
|
|
22
|
+
if sys.platform == "win32"
|
|
23
|
+
else str(Path.home() / "Baseball" / "lahman")
|
|
24
|
+
)
|
|
25
|
+
return os.environ.get("LAHMAN_DATA_DIR", default)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _render(df: pd.DataFrame, output_format: str, out: str | None) -> None:
|
|
29
|
+
if output_format == "cli":
|
|
30
|
+
print(to_cli_table(df))
|
|
31
|
+
elif output_format in _FILE_RENDERERS:
|
|
32
|
+
if not out:
|
|
33
|
+
raise ValueError(f"--out is required for {output_format} output")
|
|
34
|
+
_FILE_RENDERERS[output_format](df, out)
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"Unsupported format: {output_format}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
40
|
+
p = argparse.ArgumentParser(prog="backcaster")
|
|
41
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
42
|
+
|
|
43
|
+
def add_common(sp: argparse.ArgumentParser) -> None:
|
|
44
|
+
sp.add_argument("--year", type=int, required=True)
|
|
45
|
+
sp.add_argument("--kind", choices=["batting", "pitching"], required=True)
|
|
46
|
+
sp.add_argument("--format", choices=_OUTPUT_FORMAT_CHOICES, default="cli")
|
|
47
|
+
sp.add_argument("--out")
|
|
48
|
+
sp.add_argument(
|
|
49
|
+
"--data-dir",
|
|
50
|
+
default=_default_data_dir(),
|
|
51
|
+
help="Lahman CSV directory (default: LAHMAN_DATA_DIR, C:/Baseball/lahman on Windows, or ~/Baseball/lahman)",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
pp = sub.add_parser("player")
|
|
55
|
+
add_common(pp)
|
|
56
|
+
pp.add_argument("--name", required=True)
|
|
57
|
+
|
|
58
|
+
tp = sub.add_parser("team")
|
|
59
|
+
add_common(tp)
|
|
60
|
+
tp.add_argument("--team", required=True)
|
|
61
|
+
|
|
62
|
+
bp = sub.add_parser("batch")
|
|
63
|
+
add_common(bp)
|
|
64
|
+
return p
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv: list[str] | None = None) -> int:
|
|
68
|
+
parser = build_parser()
|
|
69
|
+
args = parser.parse_args(argv)
|
|
70
|
+
try:
|
|
71
|
+
result = LahmanSource(args.data_dir).project(
|
|
72
|
+
args.command,
|
|
73
|
+
args.year,
|
|
74
|
+
args.kind,
|
|
75
|
+
name=getattr(args, "name", None),
|
|
76
|
+
team=getattr(args, "team", None),
|
|
77
|
+
)
|
|
78
|
+
_render(result, args.format, args.out)
|
|
79
|
+
return 0
|
|
80
|
+
except (DataFetchError, PlayerLookupError, ProjectionError, ValueError) as exc:
|
|
81
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
82
|
+
return 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
raise SystemExit(main())
|