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.
Files changed (27) hide show
  1. {backcaster-0.3.4/src/backcaster.egg-info → backcaster-0.3.5}/PKG-INFO +7 -6
  2. {backcaster-0.3.4 → backcaster-0.3.5}/README.md +6 -5
  3. {backcaster-0.3.4 → backcaster-0.3.5}/THIRD_PARTY_NOTICES.md +7 -5
  4. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/__init__.py +1 -1
  5. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/lahman.py +1 -0
  6. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/marcel.py +27 -1
  7. {backcaster-0.3.4 → backcaster-0.3.5/src/backcaster.egg-info}/PKG-INFO +7 -6
  8. {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_lahman.py +21 -0
  9. {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_marcel_math.py +27 -0
  10. {backcaster-0.3.4 → backcaster-0.3.5}/LICENSE +0 -0
  11. {backcaster-0.3.4 → backcaster-0.3.5}/pyproject.toml +0 -0
  12. {backcaster-0.3.4 → backcaster-0.3.5}/setup.cfg +0 -0
  13. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/batters.py +0 -0
  14. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/cli.py +0 -0
  15. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/data.py +0 -0
  16. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/normalize.py +0 -0
  17. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/outputs.py +0 -0
  18. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster/schemas.py +0 -0
  19. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/SOURCES.txt +0 -0
  20. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/dependency_links.txt +0 -0
  21. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/entry_points.txt +0 -0
  22. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/requires.txt +0 -0
  23. {backcaster-0.3.4 → backcaster-0.3.5}/src/backcaster.egg-info/top_level.txt +0 -0
  24. {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_batters.py +0 -0
  25. {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_cli.py +0 -0
  26. {backcaster-0.3.4 → backcaster-0.3.5}/tests/test_normalize.py +0 -0
  27. {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.4
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.4:
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.4
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. Batter projections also
127
- require `Appearances.csv` and a valid `birthYear` in `People.csv`. Filenames are matched
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. See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
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.4:
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.4
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. Batter projections also
96
- require `Appearances.csv` and a valid `birthYear` in `People.csv`. Filenames are matched
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. See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
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
- 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.
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
 
@@ -1,4 +1,4 @@
1
1
  """backcaster package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "0.3.4"
4
+ __version__ = "0.3.5"
@@ -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.4
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.4:
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.4
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. Batter projections also
127
- require `Appearances.csv` and a valid `birthYear` in `People.csv`. Filenames are matched
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. See [the batter model](docs/BATTER_MODEL.md) for the six-stage calculation,
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