Replaces logger.error() with logger.exception() so the full stack trace
is captured when a pitcher card fails to generate, making it possible to
diagnose the root cause rather than just seeing the error message.
Closes#17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#7
The fallback branch of create_pit_position() used `int(df_data["key_bbref"])`
which always raises ValueError for string IDs like 'verlaju01'. The exception
was silently swallowed, causing pitchers without defensive stats to receive no
position record at all.
Fix: use `int(float(df_data["player_id"]))` to match the pattern used in
create_pitching_card() on the same file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#15
`any` (lowercase) refers to the builtin function, not `typing.Any`.
Added `Any` to the `typing` imports in both files and updated the
`cardset` parameter annotation accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The FullCard migration requires offense_col and player_id on each player's
DataFrame row. The retrosheet pipeline calculates ratings before posting,
so both fields were missing — causing silent card layout builder failures.
Adds a three-tier resolution: CSV cache → API bulk fetch → deterministic
hash fallback. Also includes player_id fallback in both calcs modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix duplicate elif branch in HR overflow cascade that prevented
single_one from receiving excess chances, and reorder single_two
secondary dispatch to check flyout full_name before groundout
short_name to prevent false 'B' matches on fly ball results.
Also add missing new_ratings.flyout_cf_b increment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The extracted batting and pitching models used malformed SLG equations that double-counted and omitted outcomes, skewing slash lines. Align formulas with canonical weighting and add regression tests to prevent recurrence.
Co-Authored-By: Claude GPT-5.3-Codex <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move each ratings model class (and, for batters, the helper functions it
depends on) into a dedicated models.py so that calcs_*.py can import from
card_builder.py at module level without circular imports.
- batters/models.py: BattingCardRatingsModel + bp_singles, wh_singles,
one_singles, bp_homeruns, triples, two_doubles, hit_by_pitch, strikeouts,
flyout_a, flyout_bq, flyout_b, groundball_a, groundball_c
- pitchers/models.py: PitchingCardRatingsModel (no helper deps needed)
- batters/calcs_batter.py: imports model + build_batter_full_cards at top
- pitchers/calcs_pitcher.py: imports model + build_pitcher_full_cards at top
- batters/card_builder.py: imports from batters.models
- pitchers/card_builder.py: imports from pitchers.models
- tests/test_batter_calcs.py: import bp_singles, wh_singles from batters.models
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- card_layout.py: Port PlayResult, PLAY_RESULTS, EXACT_CHANCES, get_chances(),
CardResult, CardColumn, FullCard, FullBattingCard, FullPitchingCard from
database/app/card_creation.py. card_output() uses col_* key names.
get_chances() always returns Decimal to avoid float/Decimal type errors.
- batters/card_builder.py: Port get_batter_card_data() algorithm as
build_batter_full_cards(ratings_vl, ratings_vr, offense_col, player_id, hand).
assign_bchances() returns float tuples for compatibility with float-based
BattingCardRatingsModel fields.
- pitchers/card_builder.py: Port get_pitcher_card_data() algorithm as
build_pitcher_full_cards(). assign_pchances() returns float tuples.
Includes card.add_fatigue() at end of each card iteration.
- batters/calcs_batter.py: Integrate card builder in get_batter_ratings().
After computing raw ratings, call build_batter_full_cards() and merge
9 col_* rendered column fields into each ratings dict. Lazy import to
avoid circular dependency.
- pitchers/calcs_pitcher.py: Same integration for get_pitcher_ratings().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit adds support for the new Retrosheet CSV format and resolves
multiple data processing issues in retrosheet_data.py.
New Features:
- Created retrosheet_transformer.py with smart caching system
- Transforms new Retrosheet CSV format to legacy format
- Checks file timestamps to avoid redundant transformations
- Caches normalized data for instant subsequent loads (~5s → <1s)
- Handles column mapping: gid→game_id, bathand→batter_hand, etc.
- Derives event_type from multiple boolean columns
- Converts handedness values R/L → r/l
- Explicitly sets string dtypes for hit_val, hit_location, batted_ball_type
Configuration Updates:
- Updated retrosheet_data.py for 2005 season data
- START_DATE: 19980301 → 20050403 (2005 Opening Day)
- END_DATE: 19980430 → 20051002 (2005 Regular Season End)
- SEASON_PCT: 28/162 → 162/162 (full season)
- MIN_PA_VL/VR: 20/40 → 50/75 (full season minimums)
- CARDSET_ID: Updated for 2005 cardsets
- EVENTS_FILENAME: Updated to use retrosheets_events_2005.csv
Bug Fixes:
1. Multi-team player duplicates
- Players traded during season had duplicate rows (one per team + combined)
- Added filtering to keep only combined totals (2TM, 3TM, etc.)
- Prevents duplicate key_bbref values in ratings dataframes
2. Column name conflicts
- Fixed Tm column conflict when merging periph_stats and defense_p
- Drop duplicate Tm from defense data before merge
3. Pitcher rating calculations (pitchers/calcs_pitcher.py)
- Fixed "truth value is ambiguous" error in min() comparisons
- Explicitly convert pandas values to float before min() operations
4. Dictionary column corruption in ratings
- Fixed ratings_vL and ratings_vR corruption during DataFrame merges
- Only merge specific columns (key_bbref, player_id, card_id) instead of full DataFrame
- Removed unnecessary .set_index() calls from post_batting_cards() and post_pitching_cards()
Documentation:
- Updated CLAUDE.md with comprehensive troubleshooting section
- Added Retrosheet transformation documentation
- Documented defense CSV requirements and column naming
- Added configuration checklist for retrosheet_data.py
- Documented common issues: multi-team players, dictionary corruption, string types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit adds default OPS value constants and type hints to key functions,
improving code documentation and IDE support.
## Changes Made
1. **Add default OPS constants** (creation_helpers.py)
- DEFAULT_BATTER_OPS: Default OPS by rarity (1-5)
- DEFAULT_STARTER_OPS: Default OPS-against for starters (99, 1-5)
- DEFAULT_RELIEVER_OPS: Default OPS-against for relievers (99, 1-5)
- Comprehensive comments explaining usage
- Single source of truth for fallback values
2. **Update batters/creation.py**
- Import DEFAULT_BATTER_OPS
- Replace 6 hardcoded if-checks with clean loop over constants
- Add type hints to post_player_updates function
- Import Dict from typing
3. **Update pitchers/creation.py**
- Import DEFAULT_STARTER_OPS and DEFAULT_RELIEVER_OPS
- Replace 12 hardcoded if-checks with clean loops over constants
- Add type hints to post_player_updates function
- Import Dict from typing
4. **Add typing import** (creation_helpers.py)
- Import Dict, List, Tuple, Optional for type hints
- Enables type hints throughout helper functions
## Impact
### Before
```python
# Scattered hardcoded values (batters)
if 1 not in average_ops:
average_ops[1] = 1.066
if 2 not in average_ops:
average_ops[2] = 0.938
# ... 4 more if-checks
# Scattered hardcoded values (pitchers)
if 99 not in sp_average_ops:
sp_average_ops[99] = 0.388
# ... 5 more if-checks for starters
# ... 6 more if-checks for relievers
```
### After
```python
# Clean, data-driven approach (batters)
for rarity, default_ops in DEFAULT_BATTER_OPS.items():
if rarity not in average_ops:
average_ops[rarity] = default_ops
# Clean, data-driven approach (pitchers)
for rarity, default_ops in DEFAULT_STARTER_OPS.items():
if rarity not in sp_average_ops:
sp_average_ops[rarity] = default_ops
for rarity, default_ops in DEFAULT_RELIEVER_OPS.items():
if rarity not in rp_average_ops:
rp_average_ops[rarity] = default_ops
```
### Benefits
✅ Eliminates 18 if-checks across batters and pitchers
✅ Single source of truth for default OPS values
✅ Easy to modify values (change constant, not scattered code)
✅ Self-documenting with clear constant names and comments
✅ Type hints improve IDE support and catch errors early
✅ Function signatures now document expected types
✅ Consistent with other recent refactorings
## Test Results
✅ 42/42 tests pass
✅ All existing functionality preserved
✅ 100% backward compatible
## Files Modified
- creation_helpers.py: +35 lines (3 constants + typing import)
- batters/creation.py: -4 lines net (cleaner code + type hints)
- pitchers/creation.py: -8 lines net (cleaner code + type hints)
**Net change:** More constants, less scattered magic numbers, better types.
Part of ongoing refactoring to reduce code fragility.