Skip to content

PR #564: Wire arm angle + swing path + chase discipline into prop enrichment#433

Merged
jaayslaughter-cpu merged 1 commit into
mainfrom
pr-564-new-statcast-signals
May 15, 2026
Merged

PR #564: Wire arm angle + swing path + chase discipline into prop enrichment#433
jaayslaughter-cpu merged 1 commit into
mainfrom
pr-564-new-statcast-signals

Conversation

@jaayslaughter-cpu
Copy link
Copy Markdown
Owner

@jaayslaughter-cpu jaayslaughter-cpu commented May 15, 2026

PR #564: Wire 3 new Statcast signals into prop enrichment

What's added

statcast_static_layer.py (+38 lines)

  • Loads data/statcast/swing-take.csv into _swing_take dict (per-PA runs gained/lost by zone)
  • New public getter: get_batter_chase_discipline(player_id){runs_chase_pa, runs_shadow_pa, pa}

prop_enrichment_layer.py (+90 lines, 3 signal blocks)

Signal Source Prop types Effect
Arm angle deception pitcher_arm_angles.csv strikeouts Deviation from 42° avg → max +3pp K-over
Swing path K-susceptibility bat-tracking-swing-path.csv hitter_strikeouts High tilt (>22°) + low ideal attack rate → +2.5pp max
Chase discipline swing-take.csv hitter_strikeouts Negative runs_chase/PA (undisciplined chaser) → +1.5pp

All 3 signals flow through the adjustment dampener (logit-space decay at 0.70/signal) — no stacking blowup.
All 3 signals added to _layer_audit JSONB and _all_adjs dampener list.

Signal ranges (calibrated conservatively)

  • Arm angle: max +3pp (requires >8° deviation from 42° MLB avg)
  • Swing path: max +4pp combined (tilt + ideal rate)
  • Chase discipline: max ±2.5pp

No regressions

  • All fallback-safe: try/except blocks, no hard failures
  • hitter_strikeouts props already flowed through BPV layer — signals appended after
  • statcast_static_layer.py: 911 → 1011 lines; prop_enrichment_layer.py: 1966 → 2055 lines

Summary by cubic

Adds three new Statcast signals to enrich K props. Improves pitcher and hitter strikeout accuracy with dampened adjustments and new audit fields.

  • New Features
    • Arm angle deception (pitcher strikeouts): uses pitcher_arm_angles.csv to measure deviation from the 42° MLB average; adds _arm_angle_adj (up to +3pp), recorded in _all_adjs and audit (arm_angle).
    • Swing path K-susceptibility (hitter_strikeouts): uses bat-tracking-swing-path.csv (swing_tilt, ideal_attack_angle_rate); adds _swing_path_k_adj (up to +4pp), plus _swing_tilt/_ideal_attack_rate; logged in _all_adjs and audit (swing_path_k).
    • Chase discipline (hitter_strikeouts): loads swing-take.csv into a new _swing_take store and exposes get_batter_chase_discipline; computes _chase_discipline_k_adj from runs_chase_pa (±2.5pp), logged in _all_adjs and audit (chase_disc_k).
    • All three signals run through the logit-space dampener (0.70 per signal) and use safe try/except fallbacks to avoid hard failures.

Written for commit fe11e06. Summary will update on new commits.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@jaayslaughter-cpu has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 53 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 94be4dcd-03c4-4cb9-84fc-aa131480be5a

📥 Commits

Reviewing files that changed from the base of the PR and between 58c74d8 and fe11e06.

📒 Files selected for processing (2)
  • prop_enrichment_layer.py
  • statcast_static_layer.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-564-new-statcast-signals

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented May 15, 2026

DeepSource Code Review

We reviewed changes in 58c74d8...fe11e06 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Docker May 15, 2026 2:29a.m. Review ↗
JavaScript May 15, 2026 2:29a.m. Review ↗
Python May 15, 2026 2:29a.m. Review ↗
SQL May 15, 2026 2:29a.m. Review ↗
Secrets May 15, 2026 2:29a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@jaayslaughter-cpu jaayslaughter-cpu merged commit c1b567d into main May 15, 2026
6 of 9 checks passed
@codacy-production
Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 high

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
ErrorProne 1 high

View in Codacy

🟢 Metrics 37 complexity

Metric Results
Complexity 37

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread statcast_static_layer.py

Falls back to empty dict if player not in dataset.
"""
if not _LOADED:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undefined variable '_LOADED'


The variable name is not defined where it is used.
This will lead to an error during the runtime.
Make sure there is no typo. If the name was supposed to be imported, verify that you've actually imported the name.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces new enrichment signals for pitcher strikeouts and batter strikeout susceptibility by incorporating Statcast data on arm angle deception, swing path mechanics, and chase zone discipline. The feedback highlights a critical error where an undefined variable _LOADED is referenced instead of _loaded, and a high-severity bug in the data loading logic where 0.0 values are incorrectly handled by _safe_float, causing valid player records to be skipped.

Comment thread statcast_static_layer.py
Comment on lines +973 to +974
if not _LOADED:
_load()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The variable _LOADED is not defined in this module; the correct variable name is _loaded (lowercase), as seen on line 68. Furthermore, the _load() function already contains an internal check for the _loaded state (line 137), making this manual check redundant. Calling _load() directly is the standard pattern used throughout this module.

    _load()

Comment thread statcast_static_layer.py
Comment on lines +551 to +554
"runs_chase_pa": round(_safe_float(r.get("runs_chase")) / pa, 5) if r.get("runs_chase") else None,
"runs_shadow_pa": round(_safe_float(r.get("runs_shadow")) / pa, 5) if r.get("runs_shadow") else None,
"runs_heart_pa": round(_safe_float(r.get("runs_heart")) / pa, 5) if r.get("runs_heart") else None,
"runs_waste_pa": round(_safe_float(r.get("runs_waste")) / pa, 5) if r.get("runs_waste") else None,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of _safe_float (line 111) returns the default value (which is None here) when the input is 0.0. This causes a TypeError during the division by pa on lines 551-554, which is then caught by the except block on line 557, resulting in the entire player record being skipped. Since 0.0 is a valid and common run value in the Statcast dataset, it should be handled correctly. Using float() directly is safe here because the if condition already ensures the value is a non-empty string.

Suggested change
"runs_chase_pa": round(_safe_float(r.get("runs_chase")) / pa, 5) if r.get("runs_chase") else None,
"runs_shadow_pa": round(_safe_float(r.get("runs_shadow")) / pa, 5) if r.get("runs_shadow") else None,
"runs_heart_pa": round(_safe_float(r.get("runs_heart")) / pa, 5) if r.get("runs_heart") else None,
"runs_waste_pa": round(_safe_float(r.get("runs_waste")) / pa, 5) if r.get("runs_waste") else None,
"runs_chase_pa": round(float(r.get("runs_chase")) / pa, 5) if r.get("runs_chase") else None,
"runs_shadow_pa": round(float(r.get("runs_shadow")) / pa, 5) if r.get("runs_shadow") else None,
"runs_heart_pa": round(float(r.get("runs_heart")) / pa, 5) if r.get("runs_heart") else None,
"runs_waste_pa": round(float(r.get("runs_waste")) / pa, 5) if r.get("runs_waste") else None,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant