backcaster 0.3.4__tar.gz → 0.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {backcaster-0.3.4/src/backcaster.egg-info → backcaster-0.3.5}/PKG-INFO +7 -6
- {backcaster-0.3.4 → backcaster-0.3.5}/README.md +6 -5
- {backcaster-0.3.4 → backcaster-0.3.5}/THIRD_PARTY_NOTICES.md +7 -5
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/__init__.py +1 -1
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/lahman.py +1 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/marcel.py +27 -1
- {backcaster-0.3.4 → backcaster-0.3.5/src/backcaster.egg-info}/PKG-INFO +7 -6
- {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_lahman.py +21 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_marcel_math.py +27 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/LICENSE +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/pyproject.toml +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/setup.cfg +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/batters.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/cli.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/data.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/normalize.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/outputs.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/schemas.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/SOURCES.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/dependency_links.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/entry_points.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/requires.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/top_level.txt +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_batters.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_cli.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_normalize.py +0 -0
- {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_outputs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backcaster
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
@@ -56,10 +56,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
56
56
|
python -m pip install --upgrade backcaster
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
To install a specific release, such as 0.3.
|
|
59
|
+
To install a specific release, such as 0.3.5:
|
|
60
60
|
|
|
61
61
|
```powershell
|
|
62
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
62
|
+
python -m pip install --upgrade backcaster==0.3.5
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -123,8 +123,8 @@ All projections use local Lahman CSVs. On Windows, the default directory is
|
|
|
123
123
|
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
124
124
|
`--data-dir` to override it for one command. No network requests or online
|
|
125
125
|
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
126
|
-
`Batting.csv` or `Pitching.csv` for the selected kind.
|
|
127
|
-
|
|
126
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Both kinds require valid
|
|
127
|
+
`birthYear` values in `People.csv`; batters additionally require `Appearances.csv`. Filenames are matched
|
|
128
128
|
without regard to case. The database is not bundled with Backcaster.
|
|
129
129
|
|
|
130
130
|
For the default Windows directory, no additional flag is needed:
|
|
@@ -166,7 +166,8 @@ ambiguous name lookups report the matching IDs.
|
|
|
166
166
|
|
|
167
167
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
168
168
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
169
|
-
baseline
|
|
169
|
+
baseline, with real-age performance adjustments documented in
|
|
170
|
+
[the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
170
171
|
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
171
172
|
Single-player projections require qualification in all three preceding seasons.
|
|
172
173
|
Team and batch projections use each player's available qualified seasons within
|
|
@@ -25,10 +25,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
25
25
|
python -m pip install --upgrade backcaster
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
To install a specific release, such as 0.3.
|
|
28
|
+
To install a specific release, such as 0.3.5:
|
|
29
29
|
|
|
30
30
|
```powershell
|
|
31
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
31
|
+
python -m pip install --upgrade backcaster==0.3.5
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -92,8 +92,8 @@ All projections use local Lahman CSVs. On Windows, the default directory is
|
|
|
92
92
|
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
93
93
|
`--data-dir` to override it for one command. No network requests or online
|
|
94
94
|
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
95
|
-
`Batting.csv` or `Pitching.csv` for the selected kind.
|
|
96
|
-
|
|
95
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Both kinds require valid
|
|
96
|
+
`birthYear` values in `People.csv`; batters additionally require `Appearances.csv`. Filenames are matched
|
|
97
97
|
without regard to case. The database is not bundled with Backcaster.
|
|
98
98
|
|
|
99
99
|
For the default Windows directory, no additional flag is needed:
|
|
@@ -135,7 +135,8 @@ ambiguous name lookups report the matching IDs.
|
|
|
135
135
|
|
|
136
136
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
137
137
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
138
|
-
baseline
|
|
138
|
+
baseline, with real-age performance adjustments documented in
|
|
139
|
+
[the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
139
140
|
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
140
141
|
Single-player projections require qualification in all three preceding seasons.
|
|
141
142
|
Team and batch projections use each player's available qualified seasons within
|
|
@@ -12,11 +12,13 @@ Reference revision: `13a1c7943f5a7317c9c812a28e299c2184060046` (March 1, 2020).
|
|
|
12
12
|
See [reference and adaptation notes](docs/MARCELR_REFERENCE.md) for the files
|
|
13
13
|
reviewed, formulas examined, current differences, and attribution practices.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
Backcaster's pitcher-aging equations in `src/backcaster/marcel.py` are adapted
|
|
16
|
+
from the age-adjustment formulas in marcelR's `R/utilities.R` and their stat-specific
|
|
17
|
+
application in `inst/scripts/marcel_data_exporter.R` at the revision above.
|
|
18
|
+
The implementation is Python, with local birth-year validation and unchanged
|
|
19
|
+
pitching opportunities; it is not a complete port of marcelR. No marcelR datasets
|
|
20
|
+
are bundled. Backcaster does not depend on R or marcelR. Attribution does not
|
|
21
|
+
imply endorsement or verified numerical parity.
|
|
20
22
|
|
|
21
23
|
The following is marcelR's MIT license notice, reproduced without changes:
|
|
22
24
|
|
|
@@ -124,6 +124,7 @@ class LahmanSource:
|
|
|
124
124
|
if kind == "batting":
|
|
125
125
|
seasons, batting = self._batting_population(year)
|
|
126
126
|
else:
|
|
127
|
+
self.read("People", {"playerID", "nameFirst", "nameLast", "birthYear"})
|
|
127
128
|
seasons = pd.concat(
|
|
128
129
|
[self.season(y, kind) for y in (year - 1, year - 2, year - 3)], ignore_index=True
|
|
129
130
|
)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import math
|
|
4
|
+
|
|
3
5
|
import pandas as pd
|
|
4
6
|
|
|
5
7
|
from backcaster.normalize import safe_divide
|
|
@@ -14,6 +16,26 @@ BATTING_COMPONENTS = ["PA", "AB", "H", "2B", "3B", "HR", "BB", "SO", "HBP", "SF"
|
|
|
14
16
|
PITCHING_COMPONENTS = ["IP", "ER", "H", "HR", "BB", "SO", "BF"]
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def _pitcher_age(prior: pd.DataFrame, year: int, fallback: float) -> float:
|
|
20
|
+
age = fallback
|
|
21
|
+
if "birthYear" in prior:
|
|
22
|
+
age = year - float(pd.to_numeric(prior["birthYear"].iloc[0], errors="coerce"))
|
|
23
|
+
if not math.isfinite(age) or not 15 <= age <= 70:
|
|
24
|
+
raise ProjectionError("A valid birthYear or explicit age is required for pitcher aging.")
|
|
25
|
+
return age
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _age_pitching(counts: pd.Series, age: float) -> pd.Series:
|
|
29
|
+
# Equations adapted from Ben Dilday's marcelR, utilities.R and
|
|
30
|
+
# inst/scripts/marcel_data_exporter.R, revision 13a1c7943f5a7317c9c812a28e299c2184060046.
|
|
31
|
+
# Copyright (c) 2017 Ben Dilday, MIT; see THIRD_PARTY_NOTICES.md.
|
|
32
|
+
benefit = 1 / (1 + 0.003 * (age - 29)) if age > 29 else 1 + 0.006 * (29 - age)
|
|
33
|
+
result = counts.copy()
|
|
34
|
+
result[["ER", "H", "HR", "BB"]] /= benefit
|
|
35
|
+
result["SO"] *= benefit
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
17
39
|
def _weighted_row(
|
|
18
40
|
rows: list[pd.Series], weights: tuple[float, float, float], cols: list[str]
|
|
19
41
|
) -> pd.Series:
|
|
@@ -148,7 +170,7 @@ def project_player(
|
|
|
148
170
|
regressed[c] = regressed_rate * weighted_pt
|
|
149
171
|
|
|
150
172
|
growth = config.default_pa_growth if kind == "batting" else config.default_ip_growth
|
|
151
|
-
age_adj = config.age_adjustment_fn(age)
|
|
173
|
+
age_adj = config.age_adjustment_fn(age) if kind == "batting" else 1.0
|
|
152
174
|
projected_pt = weighted_pt * growth * age_adj
|
|
153
175
|
pt_scale = safe_divide(projected_pt, weighted_pt)
|
|
154
176
|
regressed *= pt_scale
|
|
@@ -156,12 +178,16 @@ def project_player(
|
|
|
156
178
|
if kind == "batting":
|
|
157
179
|
rates = _derive_batting_rates(regressed)
|
|
158
180
|
else:
|
|
181
|
+
age = _pitcher_age(prior_df, year, age)
|
|
182
|
+
regressed = _age_pitching(regressed, age)
|
|
159
183
|
rates = _derive_pitching_rates(regressed)
|
|
160
184
|
|
|
161
185
|
output = pd.concat([regressed, rates])
|
|
162
186
|
output["Reliability"] = reliability
|
|
163
187
|
output["Name"] = player_name
|
|
164
188
|
output["Year"] = year
|
|
189
|
+
if kind == "pitching":
|
|
190
|
+
output["Age"] = age
|
|
165
191
|
return output.to_frame().T
|
|
166
192
|
|
|
167
193
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: backcaster
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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
|
|
@@ -56,10 +56,10 @@ Upgrade to the latest release available on PyPI:
|
|
|
56
56
|
python -m pip install --upgrade backcaster
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
To install a specific release, such as 0.3.
|
|
59
|
+
To install a specific release, such as 0.3.5:
|
|
60
60
|
|
|
61
61
|
```powershell
|
|
62
|
-
python -m pip install --upgrade backcaster==0.3.
|
|
62
|
+
python -m pip install --upgrade backcaster==0.3.5
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Run the command using the same Python environment where you run Backcaster.
|
|
@@ -123,8 +123,8 @@ All projections use local Lahman CSVs. On Windows, the default directory is
|
|
|
123
123
|
Set the `LAHMAN_DATA_DIR` environment variable to change the default, or pass
|
|
124
124
|
`--data-dir` to override it for one command. No network requests or online
|
|
125
125
|
fallbacks are made. The directory must contain `People.csv`, `Teams.csv`, and
|
|
126
|
-
`Batting.csv` or `Pitching.csv` for the selected kind.
|
|
127
|
-
|
|
126
|
+
`Batting.csv` or `Pitching.csv` for the selected kind. Both kinds require valid
|
|
127
|
+
`birthYear` values in `People.csv`; batters additionally require `Appearances.csv`. Filenames are matched
|
|
128
128
|
without regard to case. The database is not bundled with Backcaster.
|
|
129
129
|
|
|
130
130
|
For the default Windows directory, no additional flag is needed:
|
|
@@ -166,7 +166,8 @@ ambiguous name lookups report the matching IDs.
|
|
|
166
166
|
|
|
167
167
|
Batter league regression and rebaselining use all non-pitcher player-seasons;
|
|
168
168
|
qualification selects the displayed players. Pitching retains its qualified-only
|
|
169
|
-
baseline
|
|
169
|
+
baseline, with real-age performance adjustments documented in
|
|
170
|
+
[the pitcher model](docs/PITCHER_MODEL.md). See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
|
|
170
171
|
real-age adjustment, PA formula, and explicit rebaselining choices.
|
|
171
172
|
Single-player projections require qualification in all three preceding seasons.
|
|
172
173
|
Team and batch projections use each player's available qualified seasons within
|
|
@@ -337,3 +337,24 @@ def test_missing_birth_year_column(lahman: Path) -> None:
|
|
|
337
337
|
source = LahmanSource(str(lahman))
|
|
338
338
|
with pytest.raises(DataFetchError, match="birthYear"):
|
|
339
339
|
source.project("batch", 2022, "batting")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_pitcher_aging_consistent_across_commands(lahman: Path) -> None:
|
|
343
|
+
source = LahmanSource(str(lahman))
|
|
344
|
+
player = source.project("player", 2022, "pitching", name="Test Star").iloc[0]
|
|
345
|
+
team = source.project("team", 2022, "pitching", team="BOS")
|
|
346
|
+
batch = source.project("batch", 2022, "pitching")
|
|
347
|
+
assert player["Age"] == 32
|
|
348
|
+
assert player["IP"] == pytest.approx(487 / 3)
|
|
349
|
+
for result in [team, batch]:
|
|
350
|
+
row = result[result.Name == "Test Star"].iloc[0]
|
|
351
|
+
for stat in ["IP", "SO", "ERA", "WHIP", "Age"]:
|
|
352
|
+
assert row[stat] == pytest.approx(player[stat])
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_pitcher_requires_birth_year(lahman: Path) -> None:
|
|
356
|
+
path = lahman / "People.csv"
|
|
357
|
+
pd.read_csv(path).drop(columns="birthYear").to_csv(path, index=False)
|
|
358
|
+
source = LahmanSource(str(lahman))
|
|
359
|
+
with pytest.raises(DataFetchError, match="birthYear"):
|
|
360
|
+
source.project("batch", 2022, "pitching")
|
|
@@ -408,3 +408,30 @@ def test_one_season_regression_uses_only_available_weights() -> None:
|
|
|
408
408
|
result = project_player("One Season", prior, "batting", 2025, league_rates=rates)
|
|
409
409
|
assert float(result.loc[0, "HR"]) / float(result.loc[0, "PA"]) == pytest.approx(324 / 4200)
|
|
410
410
|
assert float(result.loc[0, "Reliability"]) == pytest.approx(3000 / 4200)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@pytest.mark.parametrize("age, factor", [(25, 1.024), (29, 1.0), (34, 1 / 1.015)])
|
|
414
|
+
def test_pitcher_age_curve_changes_rates_not_opportunities(age: int, factor: float) -> None:
|
|
415
|
+
neutral = project_player("Pitcher", _pitching_prior(), "pitching", 2026, age=29).iloc[0]
|
|
416
|
+
result = project_player("Pitcher", _pitching_prior(), "pitching", 2026, age=age).iloc[0]
|
|
417
|
+
assert result["IP"] == pytest.approx(neutral["IP"])
|
|
418
|
+
assert result["BF"] == pytest.approx(neutral["BF"])
|
|
419
|
+
assert result["Reliability"] == pytest.approx(neutral["Reliability"])
|
|
420
|
+
for stat in ["ER", "H", "HR", "BB", "ERA", "WHIP"]:
|
|
421
|
+
assert result[stat] == pytest.approx(neutral[stat] / factor)
|
|
422
|
+
assert result["SO"] == pytest.approx(neutral["SO"] * factor)
|
|
423
|
+
assert result["Age"] == age
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_pitcher_birth_year_overrides_default_age() -> None:
|
|
427
|
+
prior = _pitching_prior().assign(birthYear=1992)
|
|
428
|
+
actual = project_player("Pitcher", prior, "pitching", 2026)
|
|
429
|
+
expected = project_player("Pitcher", _pitching_prior(), "pitching", 2026, age=34)
|
|
430
|
+
pd.testing.assert_frame_equal(actual, expected)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@pytest.mark.parametrize("birth", [float("nan"), float("inf"), "unknown", 1800, 2025])
|
|
434
|
+
def test_pitcher_invalid_birth_year(birth: object) -> None:
|
|
435
|
+
prior = _pitching_prior().assign(birthYear=birth)
|
|
436
|
+
with pytest.raises(ProjectionError, match="valid birthYear"):
|
|
437
|
+
project_player("Pitcher", prior, "pitching", 2026)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|