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.
Files changed (33) hide show
  1. {backcaster-0.3.2/src/backcaster.egg-info → backcaster-0.3.4}/PKG-INFO +48 -34
  2. {backcaster-0.3.2 → backcaster-0.3.4}/README.md +44 -31
  3. backcaster-0.3.4/THIRD_PARTY_NOTICES.md +52 -0
  4. {backcaster-0.3.2 → backcaster-0.3.4}/pyproject.toml +4 -4
  5. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/__init__.py +1 -1
  6. backcaster-0.3.4/src/backcaster/batters.py +118 -0
  7. backcaster-0.3.4/src/backcaster/cli.py +86 -0
  8. backcaster-0.3.4/src/backcaster/data.py +9 -0
  9. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/lahman.py +78 -22
  10. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/marcel.py +7 -2
  11. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/schemas.py +0 -1
  12. {backcaster-0.3.2 → backcaster-0.3.4/src/backcaster.egg-info}/PKG-INFO +48 -34
  13. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/SOURCES.txt +4 -4
  14. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/requires.txt +1 -1
  15. backcaster-0.3.4/tests/test_batters.py +120 -0
  16. backcaster-0.3.4/tests/test_cli.py +102 -0
  17. {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_lahman.py +110 -10
  18. {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_marcel_math.py +49 -0
  19. backcaster-0.3.2/src/backcaster/cli.py +0 -176
  20. backcaster-0.3.2/src/backcaster/data.py +0 -246
  21. backcaster-0.3.2/tests/test_cli.py +0 -259
  22. backcaster-0.3.2/tests/test_cli_player_lookup.py +0 -60
  23. backcaster-0.3.2/tests/test_data.py +0 -412
  24. backcaster-0.3.2/tests/test_player_lookup.py +0 -89
  25. {backcaster-0.3.2 → backcaster-0.3.4}/LICENSE +0 -0
  26. {backcaster-0.3.2 → backcaster-0.3.4}/setup.cfg +0 -0
  27. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/normalize.py +0 -0
  28. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster/outputs.py +0 -0
  29. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/dependency_links.txt +0 -0
  30. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/entry_points.txt +0 -0
  31. {backcaster-0.3.2 → backcaster-0.3.4}/src/backcaster.egg-info/top_level.txt +0 -0
  32. {backcaster-0.3.2 → backcaster-0.3.4}/tests/test_normalize.py +0 -0
  33. {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.2
4
- Summary: Marcel-style MLB projection CLI built on pybaseball
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: pybaseball>=2.2
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 [`pybaseball`](https://github.com/jldbc/pybaseball) and `pandas`.
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
- - Internet access for first-time stat fetches (data is cached locally afterward)
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.2:
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.2
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
- Extract your Lahman CSV distribution into a local directory. Pass `--data-dir`
120
- to use it without any network requests, player-register downloads, or FanGraphs
121
- cache reads. The directory must contain `People.csv`, `Teams.csv`, and
122
- `Batting.csv` or `Pitching.csv` for the selected kind. Filenames are matched
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. Without `--data-dir`, the existing pybaseball source is used;
138
- FanGraphs may reject its automated requests with HTTP 403.
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
- Only qualified player-seasons enter projections and league regression baselines.
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 LAD --year 2026 --kind pitching
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 LAD --year 2026 --kind pitching --format html --out ./out/lad_pitching_2026.html
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
- ## Cache behavior
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
- Practical implications:
219
- - First run for a season may be slower (network fetch).
220
- - Re-runs are typically faster (local cache hit).
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 [`pybaseball`](https://github.com/jldbc/pybaseball) and `pandas`.
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
- - Internet access for first-time stat fetches (data is cached locally afterward)
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.2:
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.2
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
- Extract your Lahman CSV distribution into a local directory. Pass `--data-dir`
90
- to use it without any network requests, player-register downloads, or FanGraphs
91
- cache reads. The directory must contain `People.csv`, `Teams.csv`, and
92
- `Batting.csv` or `Pitching.csv` for the selected kind. Filenames are matched
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. Without `--data-dir`, the existing pybaseball source is used;
108
- FanGraphs may reject its automated requests with HTTP 403.
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
- Only qualified player-seasons enter projections and league regression baselines.
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 LAD --year 2026 --kind pitching
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 LAD --year 2026 --kind pitching --format html --out ./out/lad_pitching_2026.html
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
- ## Cache behavior
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
- Practical implications:
189
- - First run for a season may be slower (network fetch).
190
- - Re-runs are typically faster (local cache hit).
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 built on pybaseball"
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
- "pybaseball>=2.2",
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.*", "pybaseball", "pybaseball.*"]
88
+ module = ["pandas", "pandas.*"]
89
89
  ignore_missing_imports = true
@@ -1,4 +1,4 @@
1
1
  """backcaster package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.3.2"
4
+ __version__ = "0.3.4"
@@ -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())
@@ -0,0 +1,9 @@
1
+ """Errors reported when local source data cannot be loaded or resolved."""
2
+
3
+
4
+ class DataFetchError(RuntimeError):
5
+ pass
6
+
7
+
8
+ class PlayerLookupError(RuntimeError):
9
+ pass