""" Validation script for Retrosheet-based OF arm ratings This script: 1. Loads 2005 Retrosheet events data 2. Calculates arm ratings using the new Retrosheet method 3. Compares against known strong/weak arms from 2005 4. Generates a detailed report Usage: python test_retrosheet_arms.py """ import pandas as pd from defenders.retrosheet_arm_calculator import ( calculate_of_arms_from_retrosheet, calculate_position_baselines, calculate_player_arm_rating ) def main(): print("="*80) print("RETROSHEET ARM RATING VALIDATION - 2005 SEASON") print("="*80) print() # Load Retrosheet events print("Loading Retrosheet events data...") events_file = 'data-input/retrosheet/retrosheets_events_2005.csv' df_events = pd.read_csv(events_file, low_memory=False) print(f"Loaded {len(df_events):,} events") print() # Calculate arm ratings print("Calculating arm ratings for all outfielders...") arm_ratings = calculate_of_arms_from_retrosheet(df_events, season_pct=1.0) print(f"Calculated ratings for {len(arm_ratings)} outfielders") print() # Distribution summary print("ARM RATING DISTRIBUTION") print("-"*80) ratings_series = pd.Series(list(arm_ratings.values())) for rating in sorted(ratings_series.unique()): count = (ratings_series == rating).sum() pct = count / len(ratings_series) * 100 stars = '*' * int(pct / 2) print(f"Rating {rating:+2d}: {count:3d} players ({pct:5.1f}%) {stars}") print() # Show top and bottom performers print("TOP 20 ARM RATINGS (Elite Arms)") print("-"*80) sorted_players = sorted(arm_ratings.items(), key=lambda x: x[1]) top_20 = sorted_players[:20] for i, (player_id, rating) in enumerate(top_20, 1): # Get their stats player_stats = get_player_arm_stats(df_events, player_id) print(f"{i:2d}. {player_id:12s} Rating: {rating:+2d} " f"Assists: {player_stats['assists']:2d} " f"Home Throws: {player_stats['home_throws']:2d} " f"Assist Rate: {player_stats['assist_rate']:.2f}% " f"Balls: {player_stats['balls_fielded']:3d}") print() print("BOTTOM 15 ARM RATINGS (Weak Arms)") print("-"*80) bottom_15 = sorted_players[-15:] for i, (player_id, rating) in enumerate(bottom_15, 1): player_stats = get_player_arm_stats(df_events, player_id) print(f"{i:2d}. {player_id:12s} Rating: {rating:+2d} " f"Assists: {player_stats['assists']:2d} " f"Assist Rate: {player_stats['assist_rate']:.2f}% " f"Balls: {player_stats['balls_fielded']:3d}") print() # Known strong arms to validate print("VALIDATION: Known Strong Arms (2005)") print("-"*80) known_strong_arms = [ ('suzui001', 'Ichiro Suzuki', 'RF', 'Gold Glove, legendary arm'), ('crawc002', 'Carl Crawford', 'LF', 'Multiple stolen base deterrent'), ('edmoj001', 'Jim Edmonds', 'CF', 'Gold Glove, strong arm'), ('guerv001', 'Vladimir Guerrero', 'RF', 'Feared arm'), ] for player_id, name, pos, notes in known_strong_arms: rating = arm_ratings.get(player_id, 'NOT FOUND') if rating != 'NOT FOUND': stats = get_player_arm_stats(df_events, player_id) print(f"{name:20s} ({pos}): Rating {rating:+2d} " f"Assists: {stats['assists']:2d} " f"Note: {notes}") else: print(f"{name:20s} ({pos}): NOT FOUND IN DATA - Check player_id") print() # Position breakdown print("ARM RATINGS BY POSITION") print("-"*80) position_stats = get_position_breakdown(df_events, arm_ratings) for pos, stats in position_stats.items(): print(f"\n{pos}:") print(f" Total Players: {stats['count']}") print(f" Average Rating: {stats['avg_rating']:.2f}") print(f" Std Dev: {stats['std_rating']:.2f}") print(f" Elite Arms (≤-3): {stats['elite_count']}") print(f" Weak Arms (≥+2): {stats['weak_count']}") print() # Detailed player report print("DETAILED PLAYER REPORT (Elite Arms Only)") print("-"*80) elite_players = [(p, r) for p, r in arm_ratings.items() if r <= -3] elite_players.sort(key=lambda x: x[1]) for player_id, rating in elite_players: print(f"\nPlayer: {player_id} (Rating: {rating:+2d})") detailed_stats = get_detailed_player_stats(df_events, player_id) for key, value in detailed_stats.items(): print(f" {key:20s}: {value}") print("\n" + "="*80) print("VALIDATION COMPLETE") print("="*80) def get_player_arm_stats(df_events, player_id): """Get basic arm stats for a player across all OF positions.""" stats = { 'assists': 0, 'home_throws': 0, 'balls_fielded': 0, 'assist_rate': 0.0 } for a_col, po_col, f_col in [('a7', 'po7', 'f7'), ('a8', 'po8', 'f8'), ('a9', 'po9', 'f9')]: player_plays = df_events[df_events[f_col] == player_id] if len(player_plays) > 0: fielder_num = int(a_col[-1]) assists = player_plays[player_plays[a_col] > 0].shape[0] balls = player_plays[(player_plays[po_col] > 0) | (player_plays[a_col] > 0)].shape[0] home = player_plays[ (player_plays[a_col] > 0) & ((player_plays['brout1'] == fielder_num) | (player_plays['brout2'] == fielder_num) | (player_plays['brout3'] == fielder_num)) ].shape[0] stats['assists'] += assists stats['home_throws'] += home stats['balls_fielded'] += balls if stats['balls_fielded'] > 0: stats['assist_rate'] = stats['assists'] / stats['balls_fielded'] * 100 return stats def get_detailed_player_stats(df_events, player_id): """Get detailed breakdown of arm-related plays.""" detailed = {} for pos_name, a_col, po_col, f_col in [ ('LF', 'a7', 'po7', 'f7'), ('CF', 'a8', 'po8', 'f8'), ('RF', 'a9', 'po9', 'f9') ]: player_plays = df_events[df_events[f_col] == player_id] if len(player_plays) == 0: continue fielder_num = int(a_col[-1]) balls_fielded = player_plays[ (player_plays[po_col] > 0) | (player_plays[a_col] > 0) ].shape[0] if balls_fielded < 50: continue assists = player_plays[player_plays[a_col] > 0].shape[0] throwouts = player_plays[ (player_plays[a_col] > 0) & ((player_plays['brout1'] == fielder_num) | (player_plays['brout2'] == fielder_num) | (player_plays['brout3'] == fielder_num) | (player_plays['brout_b'] == fielder_num)) ].shape[0] home_throws = player_plays[ (player_plays[a_col] > 0) & ((player_plays['brout1'] == fielder_num) | (player_plays['brout2'] == fielder_num) | (player_plays['brout3'] == fielder_num)) ].shape[0] batter_extra = player_plays[ (player_plays[a_col] > 0) & (player_plays['brout_b'] == fielder_num) ].shape[0] detailed[f'{pos_name} Balls Fielded'] = balls_fielded detailed[f'{pos_name} Total Assists'] = assists detailed[f'{pos_name} Throwouts'] = throwouts detailed[f'{pos_name} Home Throws'] = home_throws detailed[f'{pos_name} Batter Extra Outs'] = batter_extra detailed[f'{pos_name} Assist Rate'] = f"{assists/balls_fielded*100:.2f}%" if balls_fielded > 0 else "N/A" return detailed def get_position_breakdown(df_events, arm_ratings): """Get statistical breakdown by position.""" positions = {} for pos_name, f_col in [('LF', 'f7'), ('CF', 'f8'), ('RF', 'f9')]: fielders = df_events[f_col].dropna().unique() pos_ratings = [] for fielder in fielders: if fielder in arm_ratings: # Check if they qualified at this position player_plays = df_events[df_events[f_col] == fielder] a_col = f'a{f_col[-1]}' po_col = f'po{f_col[-1]}' balls = player_plays[ (player_plays[po_col] > 0) | (player_plays[a_col] > 0) ].shape[0] if balls >= 50: pos_ratings.append(arm_ratings[fielder]) if pos_ratings: positions[pos_name] = { 'count': len(pos_ratings), 'avg_rating': pd.Series(pos_ratings).mean(), 'std_rating': pd.Series(pos_ratings).std(), 'elite_count': sum(1 for r in pos_ratings if r <= -3), 'weak_count': sum(1 for r in pos_ratings if r >= 2) } return positions if __name__ == '__main__': main()