backcaster 0.4.1__tar.gz → 0.4.2__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.4.1 → backcaster-0.4.2}/PKG-INFO +42 -14
- backcaster-0.4.1/src/backcaster.egg-info/PKG-INFO → backcaster-0.4.2/README.md +37 -43
- {backcaster-0.4.1 → backcaster-0.4.2}/pyproject.toml +4 -1
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/__init__.py +1 -1
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/batters.py +1 -1
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/cli.py +19 -6
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/data.py +2 -2
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/lahman.py +61 -34
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/marcel.py +93 -71
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/outputs.py +16 -2
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/schemas.py +4 -5
- backcaster-0.4.1/README.md → backcaster-0.4.2/src/backcaster.egg-info/PKG-INFO +71 -12
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster.egg-info/requires.txt +4 -1
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_batters.py +15 -2
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_cli.py +58 -2
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_lahman.py +129 -10
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_marcel_math.py +159 -152
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_outputs.py +51 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/LICENSE +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/THIRD_PARTY_NOTICES.md +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/setup.cfg +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/html_report.py +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/normalize.py +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster/stat_columns.py +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster.egg-info/SOURCES.txt +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster.egg-info/dependency_links.txt +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster.egg-info/entry_points.txt +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/src/backcaster.egg-info/top_level.txt +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_html_report.py +0 -0
- {backcaster-0.4.1 → backcaster-0.4.2}/tests/test_normalize.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backcaster
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -18,7 +18,7 @@ Requires-Python: >=3.13
|
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
License-File: LICENSE
|
|
20
20
|
License-File: THIRD_PARTY_NOTICES.md
|
|
21
|
-
Requires-Dist: pandas>=2.
|
|
21
|
+
Requires-Dist: pandas>=2.2.2
|
|
22
22
|
Requires-Dist: numpy>=2.0
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -27,6 +27,9 @@ Requires-Dist: ruff>=0.11.0; extra == "dev"
|
|
|
27
27
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
28
28
|
Requires-Dist: pandas-stubs>=2.2; extra == "dev"
|
|
29
29
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
30
|
+
Requires-Dist: setuptools>=77; extra == "dev"
|
|
31
|
+
Requires-Dist: wheel; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
30
33
|
Dynamic: license-file
|
|
31
34
|
|
|
32
35
|
# Backcaster
|
|
@@ -56,10 +59,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
56
59
|
python -m pip install --upgrade backcaster
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
To install a specific release, such as 0.4.
|
|
62
|
+
To install a specific release, such as 0.4.2:
|
|
60
63
|
|
|
61
64
|
```powershell
|
|
62
|
-
python -m pip install --upgrade backcaster==0.4.
|
|
65
|
+
python -m pip install --upgrade backcaster==0.4.2
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -73,7 +76,7 @@ python -m pip show backcaster
|
|
|
73
76
|
|
|
74
77
|
```bash
|
|
75
78
|
# from repo root
|
|
76
|
-
uv sync
|
|
79
|
+
uv sync --locked
|
|
77
80
|
```
|
|
78
81
|
|
|
79
82
|
This installs runtime dependencies and the `backcaster` CLI entrypoint defined in `pyproject.toml`.
|
|
@@ -81,10 +84,10 @@ This installs runtime dependencies and the `backcaster` CLI entrypoint defined i
|
|
|
81
84
|
### Install in editable/dev mode
|
|
82
85
|
|
|
83
86
|
```bash
|
|
84
|
-
uv sync --extra dev
|
|
87
|
+
uv sync --locked --extra dev
|
|
85
88
|
```
|
|
86
89
|
|
|
87
|
-
This adds test/lint/type-check/build tooling (`pytest`, `pytest-cov`, `ruff`, `mypy`, `pandas-stubs`, `build`).
|
|
90
|
+
This adds test/lint/type-check/build tooling (`pytest`, `pytest-cov`, `ruff`, `mypy`, `pandas-stubs`, `build`, `setuptools`, `wheel`, `twine`).
|
|
88
91
|
|
|
89
92
|
### Build/install as a package
|
|
90
93
|
|
|
@@ -102,7 +105,7 @@ The `player`, `team`, and `batch` commands also require `--kind batting|pitching
|
|
|
102
105
|
The `hitters` and `pitchers` commands choose their kind automatically.
|
|
103
106
|
|
|
104
107
|
Subcommand-specific required options:
|
|
105
|
-
- `player` requires `--name`
|
|
108
|
+
- `player` requires exactly one of `--name` or `--player-id`
|
|
106
109
|
- `team` requires `--team`
|
|
107
110
|
- `batch`, `hitters`, and `pitchers` require no player or team option
|
|
108
111
|
|
|
@@ -133,7 +136,9 @@ Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
|
|
|
133
136
|
They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
|
|
134
137
|
For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
|
|
135
138
|
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
|
|
139
|
+
File formats require `--out`; the CLI format prints to the terminal and rejects
|
|
140
|
+
`--out`. Invalid output options fail before data is loaded. Unwritable output
|
|
141
|
+
paths report an error on stderr and exit with status 2.
|
|
137
142
|
|
|
138
143
|
Players qualify for selection by meeting the existing PA/IP threshold in at least
|
|
139
144
|
one of those three seasons. This is a historical eligibility list, not an active
|
|
@@ -195,7 +200,15 @@ Blank numeric counting-stat cells are also treated as zero; use a dataset with
|
|
|
195
200
|
complete records for the seasons you intend to project.
|
|
196
201
|
Pitching `IPouts` is divided by three for decimal innings, and `BFP` supplies
|
|
197
202
|
batters faced. Lahman player IDs keep different players with the same name separate;
|
|
198
|
-
ambiguous name lookups report the matching IDs.
|
|
203
|
+
ambiguous name lookups report the matching IDs. Select one with `--player-id`
|
|
204
|
+
instead of `--name`, for example:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
backcaster player --player-id bettsmo01 --year 2026 --kind batting
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
IDs match Lahman `People.csv.playerID` exactly and retain the same qualification
|
|
211
|
+
rules as name lookups.
|
|
199
212
|
|
|
200
213
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
201
214
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
@@ -226,7 +239,7 @@ backcaster player --name "Mookie Betts" --year 2026 --kind batting --format csv
|
|
|
226
239
|
backcaster team --team LAN --year 2026 --kind pitching --format html --out ./out/lan_pitching_2026.html
|
|
227
240
|
```
|
|
228
241
|
|
|
229
|
-
> `--out` is required for `csv` and `html` formats
|
|
242
|
+
> `--out` is required for `csv` and `html` formats and cannot be used with `cli`.
|
|
230
243
|
|
|
231
244
|
---
|
|
232
245
|
|
|
@@ -250,6 +263,8 @@ Terminal, CSV, and HTML outputs use the same display rules:
|
|
|
250
263
|
up. Age and year are displayed as whole numbers too.
|
|
251
264
|
- BA, OBP, SLG, and OPS always show three decimal places, including `1.048` OPS.
|
|
252
265
|
BA is the canonical batter output label (previously AVG).
|
|
266
|
+
Rates with zero denominators remain undefined (NaN) and display blank;
|
|
267
|
+
OPS is undefined when OBP or SLG is undefined.
|
|
253
268
|
- ERA shows two decimals; WHIP shows three. W-L% shows a three-decimal
|
|
254
269
|
fraction, such as `0.524`. H9, HR9, BB9, and SO9 show one decimal; SO/W shows two.
|
|
255
270
|
W-L% without any decisions and SO/W without walks are undefined and display blank.
|
|
@@ -260,7 +275,7 @@ Terminal, CSV, and HTML outputs use the same display rules:
|
|
|
260
275
|
Calculations and Python projection DataFrames retain full precision. Rates are
|
|
261
276
|
computed from unrounded totals, then formatted; they are not recalculated from
|
|
262
277
|
rounded displayed counts. Independently rounded totals may therefore not add up
|
|
263
|
-
exactly. Reliability
|
|
278
|
+
exactly. Reliability shows six decimal places in CLI, CSV, and HTML output.
|
|
264
279
|
|
|
265
280
|
**CSV IP uses baseball notation too.** Convert `161.1` to `161 + 1/3` before doing
|
|
266
281
|
arithmetic; treating it as 161.1 decimal innings gives incorrect results. Internally,
|
|
@@ -326,6 +341,13 @@ Lahman distribution from the same edition. Existing online caches are ignored.
|
|
|
326
341
|
|
|
327
342
|
---
|
|
328
343
|
|
|
344
|
+
### Python data-loading errors
|
|
345
|
+
|
|
346
|
+
Local data-loading and validation failures raise
|
|
347
|
+
`backcaster.data.DataLoadError` (renamed from `DataFetchError`). Player lookup
|
|
348
|
+
failures continue to raise `PlayerLookupError`. The CLI reports these failures
|
|
349
|
+
and filesystem errors on stderr with exit status 2.
|
|
350
|
+
|
|
329
351
|
## Dependency notes
|
|
330
352
|
|
|
331
353
|
Runtime dependencies are intentionally minimal:
|
|
@@ -339,7 +361,12 @@ Development dependencies (`--extra dev`) include:
|
|
|
339
361
|
|
|
340
362
|
### Lockfile policy (`uv.lock`)
|
|
341
363
|
|
|
342
|
-
`uv.lock`
|
|
364
|
+
`uv.lock` is committed and pins runtime, development, and build dependencies. CI uses
|
|
365
|
+
`uv sync --locked --extra dev` and fails if the lockfile is out of date. CI first
|
|
366
|
+
installs the locked tools with `--no-install-project`, then installs Backcaster
|
|
367
|
+
with `--no-build-isolation` to keep build dependencies pinned. CI builds
|
|
368
|
+
without build isolation using the locked build tools; wheel smoke tests install
|
|
369
|
+
hash-checked runtime dependencies exported from the same lockfile.
|
|
343
370
|
|
|
344
371
|
Release/source ZIP archives may not include `uv.lock`. If it is missing in your environment, regenerate it locally:
|
|
345
372
|
|
|
@@ -347,7 +374,8 @@ Release/source ZIP archives may not include `uv.lock`. If it is missing in your
|
|
|
347
374
|
uv lock
|
|
348
375
|
```
|
|
349
376
|
|
|
350
|
-
Do not hand-edit `uv.lock`;
|
|
377
|
+
Do not hand-edit `uv.lock`; run `uv lock` and commit the updated lockfile when
|
|
378
|
+
dependencies change. To deliberately refresh all pins, run `uv lock --upgrade`.
|
|
351
379
|
|
|
352
380
|
---
|
|
353
381
|
|
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: backcaster
|
|
3
|
-
Version: 0.4.1
|
|
4
|
-
Summary: Marcel-style MLB projection CLI using local Lahman CSV data
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Project-URL: Homepage, https://github.com/jeffreywevans/backcaster
|
|
7
|
-
Project-URL: Repository, https://github.com/jeffreywevans/backcaster
|
|
8
|
-
Project-URL: Issues, https://github.com/jeffreywevans/backcaster/issues
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Development Status :: 4 - Beta
|
|
15
|
-
Classifier: Intended Audience :: Science/Research
|
|
16
|
-
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
17
|
-
Requires-Python: >=3.13
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
License-File: LICENSE
|
|
20
|
-
License-File: THIRD_PARTY_NOTICES.md
|
|
21
|
-
Requires-Dist: pandas>=2.0
|
|
22
|
-
Requires-Dist: numpy>=2.0
|
|
23
|
-
Provides-Extra: dev
|
|
24
|
-
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
-
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
26
|
-
Requires-Dist: ruff>=0.11.0; extra == "dev"
|
|
27
|
-
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
28
|
-
Requires-Dist: pandas-stubs>=2.2; extra == "dev"
|
|
29
|
-
Requires-Dist: build>=1.2; extra == "dev"
|
|
30
|
-
Dynamic: license-file
|
|
31
|
-
|
|
32
1
|
# Backcaster
|
|
33
2
|
|
|
34
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`.
|
|
@@ -56,10 +25,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
56
25
|
python -m pip install --upgrade backcaster
|
|
57
26
|
```
|
|
58
27
|
|
|
59
|
-
To install a specific release, such as 0.4.
|
|
28
|
+
To install a specific release, such as 0.4.2:
|
|
60
29
|
|
|
61
30
|
```powershell
|
|
62
|
-
python -m pip install --upgrade backcaster==0.4.
|
|
31
|
+
python -m pip install --upgrade backcaster==0.4.2
|
|
63
32
|
```
|
|
64
33
|
|
|
65
34
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -73,7 +42,7 @@ python -m pip show backcaster
|
|
|
73
42
|
|
|
74
43
|
```bash
|
|
75
44
|
# from repo root
|
|
76
|
-
uv sync
|
|
45
|
+
uv sync --locked
|
|
77
46
|
```
|
|
78
47
|
|
|
79
48
|
This installs runtime dependencies and the `backcaster` CLI entrypoint defined in `pyproject.toml`.
|
|
@@ -81,10 +50,10 @@ This installs runtime dependencies and the `backcaster` CLI entrypoint defined i
|
|
|
81
50
|
### Install in editable/dev mode
|
|
82
51
|
|
|
83
52
|
```bash
|
|
84
|
-
uv sync --extra dev
|
|
53
|
+
uv sync --locked --extra dev
|
|
85
54
|
```
|
|
86
55
|
|
|
87
|
-
This adds test/lint/type-check/build tooling (`pytest`, `pytest-cov`, `ruff`, `mypy`, `pandas-stubs`, `build`).
|
|
56
|
+
This adds test/lint/type-check/build tooling (`pytest`, `pytest-cov`, `ruff`, `mypy`, `pandas-stubs`, `build`, `setuptools`, `wheel`, `twine`).
|
|
88
57
|
|
|
89
58
|
### Build/install as a package
|
|
90
59
|
|
|
@@ -102,7 +71,7 @@ The `player`, `team`, and `batch` commands also require `--kind batting|pitching
|
|
|
102
71
|
The `hitters` and `pitchers` commands choose their kind automatically.
|
|
103
72
|
|
|
104
73
|
Subcommand-specific required options:
|
|
105
|
-
- `player` requires `--name`
|
|
74
|
+
- `player` requires exactly one of `--name` or `--player-id`
|
|
106
75
|
- `team` requires `--team`
|
|
107
76
|
- `batch`, `hitters`, and `pitchers` require no player or team option
|
|
108
77
|
|
|
@@ -133,7 +102,9 @@ Both commands support `--data-dir`, `--format cli|csv|html`, and `--out`.
|
|
|
133
102
|
They are shortcuts for `batch --kind batting` and `batch --kind pitching`.
|
|
134
103
|
For a different data directory, add `--data-dir "C:\Baseball\lahman"`.
|
|
135
104
|
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
|
|
105
|
+
File formats require `--out`; the CLI format prints to the terminal and rejects
|
|
106
|
+
`--out`. Invalid output options fail before data is loaded. Unwritable output
|
|
107
|
+
paths report an error on stderr and exit with status 2.
|
|
137
108
|
|
|
138
109
|
Players qualify for selection by meeting the existing PA/IP threshold in at least
|
|
139
110
|
one of those three seasons. This is a historical eligibility list, not an active
|
|
@@ -195,7 +166,15 @@ Blank numeric counting-stat cells are also treated as zero; use a dataset with
|
|
|
195
166
|
complete records for the seasons you intend to project.
|
|
196
167
|
Pitching `IPouts` is divided by three for decimal innings, and `BFP` supplies
|
|
197
168
|
batters faced. Lahman player IDs keep different players with the same name separate;
|
|
198
|
-
ambiguous name lookups report the matching IDs.
|
|
169
|
+
ambiguous name lookups report the matching IDs. Select one with `--player-id`
|
|
170
|
+
instead of `--name`, for example:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
backcaster player --player-id bettsmo01 --year 2026 --kind batting
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
IDs match Lahman `People.csv.playerID` exactly and retain the same qualification
|
|
177
|
+
rules as name lookups.
|
|
199
178
|
|
|
200
179
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
201
180
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
@@ -226,7 +205,7 @@ backcaster player --name "Mookie Betts" --year 2026 --kind batting --format csv
|
|
|
226
205
|
backcaster team --team LAN --year 2026 --kind pitching --format html --out ./out/lan_pitching_2026.html
|
|
227
206
|
```
|
|
228
207
|
|
|
229
|
-
> `--out` is required for `csv` and `html` formats
|
|
208
|
+
> `--out` is required for `csv` and `html` formats and cannot be used with `cli`.
|
|
230
209
|
|
|
231
210
|
---
|
|
232
211
|
|
|
@@ -250,6 +229,8 @@ Terminal, CSV, and HTML outputs use the same display rules:
|
|
|
250
229
|
up. Age and year are displayed as whole numbers too.
|
|
251
230
|
- BA, OBP, SLG, and OPS always show three decimal places, including `1.048` OPS.
|
|
252
231
|
BA is the canonical batter output label (previously AVG).
|
|
232
|
+
Rates with zero denominators remain undefined (NaN) and display blank;
|
|
233
|
+
OPS is undefined when OBP or SLG is undefined.
|
|
253
234
|
- ERA shows two decimals; WHIP shows three. W-L% shows a three-decimal
|
|
254
235
|
fraction, such as `0.524`. H9, HR9, BB9, and SO9 show one decimal; SO/W shows two.
|
|
255
236
|
W-L% without any decisions and SO/W without walks are undefined and display blank.
|
|
@@ -260,7 +241,7 @@ Terminal, CSV, and HTML outputs use the same display rules:
|
|
|
260
241
|
Calculations and Python projection DataFrames retain full precision. Rates are
|
|
261
242
|
computed from unrounded totals, then formatted; they are not recalculated from
|
|
262
243
|
rounded displayed counts. Independently rounded totals may therefore not add up
|
|
263
|
-
exactly. Reliability
|
|
244
|
+
exactly. Reliability shows six decimal places in CLI, CSV, and HTML output.
|
|
264
245
|
|
|
265
246
|
**CSV IP uses baseball notation too.** Convert `161.1` to `161 + 1/3` before doing
|
|
266
247
|
arithmetic; treating it as 161.1 decimal innings gives incorrect results. Internally,
|
|
@@ -326,6 +307,13 @@ Lahman distribution from the same edition. Existing online caches are ignored.
|
|
|
326
307
|
|
|
327
308
|
---
|
|
328
309
|
|
|
310
|
+
### Python data-loading errors
|
|
311
|
+
|
|
312
|
+
Local data-loading and validation failures raise
|
|
313
|
+
`backcaster.data.DataLoadError` (renamed from `DataFetchError`). Player lookup
|
|
314
|
+
failures continue to raise `PlayerLookupError`. The CLI reports these failures
|
|
315
|
+
and filesystem errors on stderr with exit status 2.
|
|
316
|
+
|
|
329
317
|
## Dependency notes
|
|
330
318
|
|
|
331
319
|
Runtime dependencies are intentionally minimal:
|
|
@@ -339,7 +327,12 @@ Development dependencies (`--extra dev`) include:
|
|
|
339
327
|
|
|
340
328
|
### Lockfile policy (`uv.lock`)
|
|
341
329
|
|
|
342
|
-
`uv.lock`
|
|
330
|
+
`uv.lock` is committed and pins runtime, development, and build dependencies. CI uses
|
|
331
|
+
`uv sync --locked --extra dev` and fails if the lockfile is out of date. CI first
|
|
332
|
+
installs the locked tools with `--no-install-project`, then installs Backcaster
|
|
333
|
+
with `--no-build-isolation` to keep build dependencies pinned. CI builds
|
|
334
|
+
without build isolation using the locked build tools; wheel smoke tests install
|
|
335
|
+
hash-checked runtime dependencies exported from the same lockfile.
|
|
343
336
|
|
|
344
337
|
Release/source ZIP archives may not include `uv.lock`. If it is missing in your environment, regenerate it locally:
|
|
345
338
|
|
|
@@ -347,7 +340,8 @@ Release/source ZIP archives may not include `uv.lock`. If it is missing in your
|
|
|
347
340
|
uv lock
|
|
348
341
|
```
|
|
349
342
|
|
|
350
|
-
Do not hand-edit `uv.lock`;
|
|
343
|
+
Do not hand-edit `uv.lock`; run `uv lock` and commit the updated lockfile when
|
|
344
|
+
dependencies change. To deliberately refresh all pins, run `uv lock --upgrade`.
|
|
351
345
|
|
|
352
346
|
---
|
|
353
347
|
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
requires-python = ">=3.13"
|
|
23
23
|
dependencies = [
|
|
24
|
-
"pandas>=2.
|
|
24
|
+
"pandas>=2.2.2",
|
|
25
25
|
"numpy>=2.0",
|
|
26
26
|
]
|
|
27
27
|
|
|
@@ -38,6 +38,9 @@ dev = [
|
|
|
38
38
|
"mypy>=1.10",
|
|
39
39
|
"pandas-stubs>=2.2",
|
|
40
40
|
"build>=1.2",
|
|
41
|
+
"setuptools>=77",
|
|
42
|
+
"wheel",
|
|
43
|
+
"twine",
|
|
41
44
|
]
|
|
42
45
|
|
|
43
46
|
[project.scripts]
|
|
@@ -144,7 +144,7 @@ def _summarize(projected: pd.DataFrame, pa: pd.Series) -> pd.DataFrame:
|
|
|
144
144
|
).replace(0, np.nan)
|
|
145
145
|
out["SLG"] = out["TB"] / out["AB"].replace(0, np.nan)
|
|
146
146
|
out["OPS"] = out["OBP"] + out["SLG"]
|
|
147
|
-
return out
|
|
147
|
+
return out
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def project_batters(seasons: pd.DataFrame, year: int) -> pd.DataFrame:
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
|
-
from backcaster.data import
|
|
10
|
+
from backcaster.data import DataLoadError, PlayerLookupError
|
|
11
11
|
from backcaster.lahman import LahmanSource
|
|
12
12
|
from backcaster.marcel import ProjectionError
|
|
13
13
|
from backcaster.outputs import to_cli_table, to_csv, to_html
|
|
@@ -25,12 +25,19 @@ def _default_data_dir() -> str:
|
|
|
25
25
|
return os.environ.get("LAHMAN_DATA_DIR", default)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _validate_output_options(output_format: str, out: str | None) -> None:
|
|
29
|
+
if output_format == "cli" and out is not None:
|
|
30
|
+
raise ValueError("--out cannot be used with --format cli; choose --format csv or html")
|
|
31
|
+
if output_format in _FILE_RENDERERS and not out:
|
|
32
|
+
raise ValueError(f"--out is required for {output_format} output")
|
|
33
|
+
|
|
34
|
+
|
|
28
35
|
def _render(df: pd.DataFrame, output_format: str, out: str | None) -> None:
|
|
36
|
+
_validate_output_options(output_format, out)
|
|
29
37
|
if output_format == "cli":
|
|
30
38
|
print(to_cli_table(df))
|
|
31
39
|
elif output_format in _FILE_RENDERERS:
|
|
32
|
-
|
|
33
|
-
raise ValueError(f"--out is required for {output_format} output")
|
|
40
|
+
assert out is not None
|
|
34
41
|
_FILE_RENDERERS[output_format](df, out)
|
|
35
42
|
else:
|
|
36
43
|
raise ValueError(f"Unsupported format: {output_format}")
|
|
@@ -47,7 +54,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
47
54
|
else:
|
|
48
55
|
sp.set_defaults(kind=kind)
|
|
49
56
|
sp.add_argument("--format", choices=_OUTPUT_FORMAT_CHOICES, default="cli")
|
|
50
|
-
sp.add_argument("--out")
|
|
57
|
+
sp.add_argument("--out", help="Output path (required for csv/html; invalid with cli)")
|
|
51
58
|
sp.add_argument(
|
|
52
59
|
"--data-dir",
|
|
53
60
|
default=_default_data_dir(),
|
|
@@ -56,7 +63,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
56
63
|
|
|
57
64
|
pp = sub.add_parser("player")
|
|
58
65
|
add_common(pp)
|
|
59
|
-
pp.
|
|
66
|
+
player_selector = pp.add_mutually_exclusive_group(required=True)
|
|
67
|
+
player_selector.add_argument("--name", help="Player name (must identify one qualified player)")
|
|
68
|
+
player_selector.add_argument(
|
|
69
|
+
"--player-id", help="Exact Lahman player ID (for example, bettsmo01)"
|
|
70
|
+
)
|
|
60
71
|
|
|
61
72
|
tp = sub.add_parser("team")
|
|
62
73
|
add_common(tp)
|
|
@@ -75,16 +86,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
75
86
|
parser = build_parser()
|
|
76
87
|
args = parser.parse_args(argv)
|
|
77
88
|
try:
|
|
89
|
+
_validate_output_options(args.format, args.out)
|
|
78
90
|
result = LahmanSource(args.data_dir).project(
|
|
79
91
|
args.command,
|
|
80
92
|
args.year,
|
|
81
93
|
args.kind,
|
|
82
94
|
name=getattr(args, "name", None),
|
|
83
95
|
team=getattr(args, "team", None),
|
|
96
|
+
player_id=getattr(args, "player_id", None),
|
|
84
97
|
)
|
|
85
98
|
_render(result, args.format, args.out)
|
|
86
99
|
return 0
|
|
87
|
-
except (
|
|
100
|
+
except (DataLoadError, PlayerLookupError, ProjectionError, ValueError, OSError) as exc:
|
|
88
101
|
print(f"Error: {exc}", file=sys.stderr)
|
|
89
102
|
return 2
|
|
90
103
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Errors reported when local source data cannot be loaded or resolved."""
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
|
|
4
|
+
class DataLoadError(RuntimeError):
|
|
5
|
+
"""Local source files are missing, unreadable, or invalid."""
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class PlayerLookupError(RuntimeError):
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
9
|
from backcaster.batters import project_batters
|
|
10
|
-
from backcaster.data import
|
|
10
|
+
from backcaster.data import DataLoadError, PlayerLookupError
|
|
11
11
|
from backcaster.marcel import ProjectionError, project_player, project_team
|
|
12
12
|
|
|
13
13
|
|
|
@@ -15,32 +15,41 @@ class LahmanSource:
|
|
|
15
15
|
def __init__(self, directory: str) -> None:
|
|
16
16
|
self.directory = Path(directory)
|
|
17
17
|
self.tables: dict[str, pd.DataFrame] = {}
|
|
18
|
+
self._requested_columns: dict[str, set[str]] = {}
|
|
19
|
+
self._people: pd.DataFrame | None = None
|
|
18
20
|
|
|
19
21
|
def read(self, name: str, required: set[str]) -> pd.DataFrame:
|
|
20
|
-
|
|
22
|
+
optional = {"Batting": {"HBP", "SF", "SH"}, "People": {"birthYear"}}.get(name, set())
|
|
23
|
+
columns = required | optional
|
|
24
|
+
if name not in self.tables or columns - self._requested_columns[name]:
|
|
25
|
+
columns |= self._requested_columns.get(name, set())
|
|
21
26
|
paths = [
|
|
22
27
|
p for p in self.directory.glob("*") if p.name.casefold() == f"{name}.csv".casefold()
|
|
23
28
|
]
|
|
24
29
|
if len(paths) != 1:
|
|
25
|
-
raise
|
|
30
|
+
raise DataLoadError(
|
|
26
31
|
f"Expected {name}.csv in Lahman directory '{self.directory}'. "
|
|
27
32
|
"Extract the Lahman CSVs there or select their directory with --data-dir."
|
|
28
33
|
)
|
|
29
34
|
try:
|
|
30
|
-
self.tables[name] = pd.read_csv(
|
|
35
|
+
self.tables[name] = pd.read_csv(
|
|
36
|
+
paths[0],
|
|
37
|
+
encoding="utf-8-sig",
|
|
38
|
+
low_memory=False,
|
|
39
|
+
usecols=lambda column: column in columns,
|
|
40
|
+
)
|
|
41
|
+
self._requested_columns[name] = columns
|
|
31
42
|
except (OSError, ValueError) as exc:
|
|
32
|
-
raise
|
|
43
|
+
raise DataLoadError(f"Unable to read Lahman {name}.csv: {exc}") from exc
|
|
33
44
|
frame = self.tables[name]
|
|
34
45
|
missing = required - set(frame.columns)
|
|
35
46
|
if missing:
|
|
36
|
-
raise
|
|
37
|
-
f"Lahman {name}.csv missing columns: {', '.join(sorted(missing))}."
|
|
38
|
-
)
|
|
47
|
+
raise DataLoadError(f"Lahman {name}.csv missing columns: {', '.join(sorted(missing))}.")
|
|
39
48
|
return frame
|
|
40
49
|
|
|
41
50
|
def season(self, year: int, kind: str, *, qualified: bool = True) -> pd.DataFrame:
|
|
42
51
|
if kind not in {"batting", "pitching"}:
|
|
43
|
-
raise
|
|
52
|
+
raise DataLoadError(f"Invalid kind {kind!r}.")
|
|
44
53
|
name = "Batting" if kind == "batting" else "Pitching"
|
|
45
54
|
components = (
|
|
46
55
|
["AB", "H", "2B", "3B", "HR", "BB", "SO", "R", "RBI", "SB", "CS", "IBB", "GIDP"]
|
|
@@ -66,7 +75,7 @@ class LahmanSource:
|
|
|
66
75
|
stats = self.read(name, {"playerID", "yearID", "stint", "teamID", *components})
|
|
67
76
|
stats = stats[stats["yearID"] == year].copy()
|
|
68
77
|
if stats.empty:
|
|
69
|
-
raise
|
|
78
|
+
raise DataLoadError(
|
|
70
79
|
f"Lahman {name}.csv has no data for {year}; choose a covered year or update the dataset."
|
|
71
80
|
)
|
|
72
81
|
games = self._season_games(year)
|
|
@@ -79,7 +88,7 @@ class LahmanSource:
|
|
|
79
88
|
stats[components] = stats[components].apply(pd.to_numeric, errors="raise").fillna(0)
|
|
80
89
|
stats["stint"] = pd.to_numeric(stats["stint"], errors="raise")
|
|
81
90
|
except (ValueError, TypeError) as exc:
|
|
82
|
-
raise
|
|
91
|
+
raise DataLoadError(
|
|
83
92
|
f"Invalid numeric data in Lahman {name}.csv for {year}: {exc}"
|
|
84
93
|
) from exc
|
|
85
94
|
totals = stats.groupby("playerID", sort=False)[components].sum()
|
|
@@ -100,7 +109,7 @@ class LahmanSource:
|
|
|
100
109
|
if qualified:
|
|
101
110
|
totals = totals[totals["Qualified"]]
|
|
102
111
|
if totals.empty:
|
|
103
|
-
raise
|
|
112
|
+
raise DataLoadError(f"No qualified {kind} players in Lahman for {year}.")
|
|
104
113
|
totals = totals.rename(columns={"GIDP": "GDP"})
|
|
105
114
|
totals = self._attach_names(totals)
|
|
106
115
|
totals["PlayerID"] = totals["playerID"]
|
|
@@ -111,32 +120,39 @@ class LahmanSource:
|
|
|
111
120
|
teams = self.read("Teams", {"yearID", "teamID", "G"})
|
|
112
121
|
games = pd.to_numeric(teams.loc[teams["yearID"] == year, "G"], errors="coerce").max()
|
|
113
122
|
if pd.isna(games) or games <= 0:
|
|
114
|
-
raise
|
|
123
|
+
raise DataLoadError(f"Lahman Teams.csv has no valid game counts for {year}.")
|
|
115
124
|
return float(games)
|
|
116
125
|
|
|
117
126
|
def _attach_names(self, totals: pd.DataFrame) -> pd.DataFrame:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
people["
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
raise DataFetchError("Duplicate player IDs in Lahman People.csv.") from exc
|
|
127
|
+
if self._people is None:
|
|
128
|
+
people = self.read("People", {"playerID", "nameFirst", "nameLast"})
|
|
129
|
+
if people["playerID"].duplicated().any():
|
|
130
|
+
raise DataLoadError("Duplicate player IDs in Lahman People.csv.")
|
|
131
|
+
names = (
|
|
132
|
+
people["nameFirst"].fillna("") + " " + people["nameLast"].fillna("")
|
|
133
|
+
).str.strip()
|
|
134
|
+
self._people = people.assign(Name=names).set_index("playerID")
|
|
135
|
+
self._people = self._people[["Name", *(["birthYear"] if "birthYear" in people else [])]]
|
|
136
|
+
totals = totals.reset_index()
|
|
137
|
+
for column in self._people:
|
|
138
|
+
totals[column] = totals["playerID"].map(self._people[column])
|
|
131
139
|
if totals["Name"].isna().any():
|
|
132
|
-
raise
|
|
140
|
+
raise DataLoadError(
|
|
133
141
|
"Lahman People.csv is missing players present in season statistics."
|
|
134
142
|
)
|
|
135
143
|
return totals
|
|
136
144
|
|
|
137
145
|
def project(
|
|
138
|
-
self,
|
|
146
|
+
self,
|
|
147
|
+
command: str,
|
|
148
|
+
year: int,
|
|
149
|
+
kind: str,
|
|
150
|
+
name: str | None = None,
|
|
151
|
+
team: str | None = None,
|
|
152
|
+
player_id: str | None = None,
|
|
139
153
|
) -> pd.DataFrame:
|
|
154
|
+
if name is not None and player_id is not None:
|
|
155
|
+
raise ValueError("Specify either name or player_id, not both.")
|
|
140
156
|
batting = pd.DataFrame()
|
|
141
157
|
if kind == "batting":
|
|
142
158
|
seasons, batting = self._batting_population(year)
|
|
@@ -148,7 +164,7 @@ class LahmanSource:
|
|
|
148
164
|
)
|
|
149
165
|
eligible = seasons[seasons["Qualified"]]
|
|
150
166
|
if command == "player":
|
|
151
|
-
candidates = self._player_seasons(eligible, name)
|
|
167
|
+
candidates = self._player_seasons(eligible, name, player_id)
|
|
152
168
|
matches = seasons[seasons["PlayerID"] == candidates["PlayerID"].iloc[0]]
|
|
153
169
|
if kind == "batting":
|
|
154
170
|
return batting[batting["PlayerID"] == matches["PlayerID"].iloc[0]].reset_index(
|
|
@@ -182,15 +198,15 @@ class LahmanSource:
|
|
|
182
198
|
pd.to_numeric, errors="raise"
|
|
183
199
|
)
|
|
184
200
|
except (ValueError, TypeError) as exc:
|
|
185
|
-
raise
|
|
201
|
+
raise DataLoadError("Invalid game counts in Lahman Appearances.csv.") from exc
|
|
186
202
|
counts = appearances[["G_all", "G_p"]]
|
|
187
203
|
invalid = counts.replace([float("inf"), -float("inf")], float("nan")).isna() | (counts < 0)
|
|
188
204
|
if invalid.any().any() or (appearances["G_p"] > appearances["G_all"]).any():
|
|
189
|
-
raise
|
|
205
|
+
raise DataLoadError("Invalid game counts in Lahman Appearances.csv.")
|
|
190
206
|
games = appearances.groupby(["playerID", "yearID"])[["G_all", "G_p"]].sum(min_count=1)
|
|
191
207
|
seasons = seasons.merge(games, left_on=["PlayerID", "Season"], right_index=True, how="left")
|
|
192
208
|
if seasons[["G_all", "G_p"]].isna().any().any():
|
|
193
|
-
raise
|
|
209
|
+
raise DataLoadError(
|
|
194
210
|
"Appearances.csv must cover every batting player-season with game counts."
|
|
195
211
|
)
|
|
196
212
|
# A primary pitcher appears as pitcher in more than half of their games.
|
|
@@ -202,7 +218,17 @@ class LahmanSource:
|
|
|
202
218
|
raise ProjectionError("No qualified non-pitcher batters in the prior three seasons.")
|
|
203
219
|
return seasons, projected
|
|
204
220
|
|
|
205
|
-
def _player_seasons(
|
|
221
|
+
def _player_seasons(
|
|
222
|
+
self, seasons: pd.DataFrame, name: str | None, player_id: str | None = None
|
|
223
|
+
) -> pd.DataFrame:
|
|
224
|
+
if player_id is not None:
|
|
225
|
+
matches = seasons[seasons["PlayerID"] == player_id]
|
|
226
|
+
if matches.empty:
|
|
227
|
+
raise PlayerLookupError(
|
|
228
|
+
f"No qualified Lahman player found for player ID '{player_id}' "
|
|
229
|
+
"in the prior three seasons."
|
|
230
|
+
)
|
|
231
|
+
return matches
|
|
206
232
|
normalized = " ".join((name or "").casefold().split())
|
|
207
233
|
matches = seasons[
|
|
208
234
|
seasons["Name"].map(lambda value: " ".join(value.casefold().split())) == normalized
|
|
@@ -214,6 +240,7 @@ class LahmanSource:
|
|
|
214
240
|
)
|
|
215
241
|
if len(ids) > 1:
|
|
216
242
|
raise PlayerLookupError(
|
|
217
|
-
f"Ambiguous Lahman name '{name}'; matching player IDs: {', '.join(ids)}."
|
|
243
|
+
f"Ambiguous Lahman name '{name}'; matching player IDs: {', '.join(ids)}. "
|
|
244
|
+
"Select one with --player-id instead of --name."
|
|
218
245
|
)
|
|
219
246
|
return matches
|