Last active
September 29, 2025 18:43
-
-
Save earlonrails/3e39e26d5ab240fff8aa40b28d41d512 to your computer and use it in GitHub Desktop.
Analyze the first drive of games when Matt LaFleur is coaching the Greenbay Packers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import nflreadpy as nfl | |
| import polars as pl | |
| def analyze_lafleur_opening_drives(): | |
| """ | |
| Analyze Matt LaFleur's opening drive play-calling for Green Bay Packers | |
| Matt LaFleur became head coach in 2019 | |
| """ | |
| # Load play-by-play data for LaFleur's tenure (2019-2024) | |
| print("Loading play-by-play data...") | |
| pbp = nfl.load_pbp(seasons=range(2019, 2025)) | |
| # Filter for Green Bay Packers games | |
| gb_games = pbp.filter( | |
| (pl.col('posteam') == 'GB') | (pl.col('defteam') == 'GB') | |
| ) | |
| # Get unique game IDs where GB was involved | |
| game_ids = gb_games.select('game_id').unique().sort('game_id') | |
| print(f"\nTotal games found: {len(game_ids)}") | |
| print("=" * 80) | |
| results = [] | |
| for game_row in game_ids.iter_rows(named=True): | |
| game_id = game_row['game_id'] | |
| # Get all plays for this game | |
| game_plays = pbp.filter(pl.col('game_id') == game_id) | |
| # Get the first drive for Green Bay (where they have possession) | |
| gb_first_drive = game_plays.filter( | |
| (pl.col('posteam') == 'GB') & | |
| (pl.col('drive').is_not_null()) | |
| ).sort(['qtr', 'game_seconds_remaining'], descending=[False, True]) | |
| if len(gb_first_drive) == 0: | |
| continue | |
| # Get the first drive number | |
| first_drive_num = gb_first_drive.select('drive').head(1).item() | |
| # Get all plays from the first drive | |
| first_drive_plays = gb_first_drive.filter(pl.col('drive') == first_drive_num) | |
| # Filter out non-plays (penalties with no play, timeouts, etc.) | |
| actual_plays = first_drive_plays.filter( | |
| (pl.col('play_type').is_in(['pass', 'run'])) & | |
| (pl.col('desc').is_not_null()) | |
| ) | |
| if len(actual_plays) < 3: | |
| continue | |
| # Get first 3 plays | |
| first_3_plays = actual_plays.head(3) | |
| # Extract play types | |
| play_types = first_3_plays.select('play_type').to_series().to_list() | |
| play_descriptions = first_3_plays.select('desc').to_series().to_list() | |
| # Check if all 3 are passes | |
| all_passes = all(pt == 'pass' for pt in play_types) | |
| # Get drive result | |
| drive_result = first_drive_plays.select('fixed_drive_result').head(1).item() | |
| # Get game result | |
| game_info = game_plays.head(1) | |
| home_team = game_info.select('home_team').item() | |
| away_team = game_info.select('away_team').item() | |
| home_score = game_info.select('home_score').item() | |
| away_score = game_info.select('away_score').item() | |
| if home_team == 'GB': | |
| gb_score = home_score | |
| opp_score = away_score | |
| opponent = away_team | |
| else: | |
| gb_score = away_score | |
| opp_score = home_score | |
| opponent = home_team | |
| if gb_score > opp_score: | |
| game_result = 'W' | |
| elif gb_score < opp_score: | |
| game_result = 'L' | |
| else: | |
| game_result = 'T' | |
| # Get season and week | |
| season = game_info.select('season').item() | |
| week = game_info.select('week').item() | |
| results.append({ | |
| 'season': season, | |
| 'week': week, | |
| 'game_id': game_id, | |
| 'opponent': opponent, | |
| 'started_3_passes': all_passes, | |
| 'play_1': play_types[0] if len(play_types) > 0 else None, | |
| 'play_2': play_types[1] if len(play_types) > 1 else None, | |
| 'play_3': play_types[2] if len(play_types) > 2 else None, | |
| 'drive_result': drive_result, | |
| 'game_result': game_result, | |
| 'gb_score': gb_score, | |
| 'opp_score': opp_score, | |
| 'desc_1': play_descriptions[0][:100] if len(play_descriptions) > 0 else None, | |
| 'desc_2': play_descriptions[1][:100] if len(play_descriptions) > 1 else None, | |
| 'desc_3': play_descriptions[2][:100] if len(play_descriptions) > 2 else None, | |
| }) | |
| # Convert to DataFrame | |
| results_df = pl.DataFrame(results) | |
| print(f"\nGames analyzed: {len(results_df)}") | |
| print("=" * 80) | |
| # Filter for games starting with 3 passes | |
| three_pass_games = results_df.filter(pl.col('started_3_passes') == True) | |
| not_three_pass_games = results_df.filter(pl.col('started_3_passes') == False) | |
| # Filter for games starting with a run | |
| run_first_games = results_df.filter(pl.col('play_1') == 'run') | |
| print(f"\n{'='*80}") | |
| print(f"GAMES STARTING WITH 3 PASSES: {len(three_pass_games)}") | |
| print(f"{'='*80}\n") | |
| if len(three_pass_games) > 0: | |
| # Overall record | |
| wins = three_pass_games.filter(pl.col('game_result') == 'W').height | |
| losses = three_pass_games.filter(pl.col('game_result') == 'L').height | |
| ties = three_pass_games.filter(pl.col('game_result') == 'T').height | |
| print(f"Overall Record: {wins}-{losses}" + (f"-{ties}" if ties > 0 else "")) | |
| if wins + losses > 0: | |
| win_pct = wins / (wins + losses) * 100 | |
| print(f"Win Percentage: {win_pct:.1f}%\n") | |
| # Drive outcomes breakdown | |
| print("Drive Outcomes:") | |
| drive_outcomes = three_pass_games.group_by('drive_result').agg( | |
| pl.len().alias('count') | |
| ).sort('count', descending=True) | |
| for row in drive_outcomes.iter_rows(named=True): | |
| print(f" {row['drive_result']}: {row['count']}") | |
| print("\nDetailed Game Results:") | |
| print("-" * 80) | |
| for row in three_pass_games.sort(['season', 'week']).iter_rows(named=True): | |
| print(f"\n{row['season']} Week {row['week']} vs {row['opponent']}") | |
| print(f" Result: {row['game_result']} ({row['gb_score']}-{row['opp_score']})") | |
| print(f" Drive Outcome: {row['drive_result']}") | |
| print(f" Play 1 (pass): {row['desc_1']}") | |
| print(f" Play 2 (pass): {row['desc_2']}") | |
| print(f" Play 3 (pass): {row['desc_3']}") | |
| else: | |
| print("No games found starting with 3 passes") | |
| print(f"\n{'='*80}") | |
| print(f"GAMES NOT STARTING WITH 3 PASSES: {len(not_three_pass_games)}") | |
| print(f"{'='*80}\n") | |
| if len(not_three_pass_games) > 0: | |
| wins = not_three_pass_games.filter(pl.col('game_result') == 'W').height | |
| losses = not_three_pass_games.filter(pl.col('game_result') == 'L').height | |
| ties = not_three_pass_games.filter(pl.col('game_result') == 'T').height | |
| print(f"Overall Record: {wins}-{losses}" + (f"-{ties}" if ties > 0 else "")) | |
| if wins + losses > 0: | |
| win_pct = wins / (wins + losses) * 100 | |
| print(f"Win Percentage: {win_pct:.1f}%\n") | |
| # Drive outcomes breakdown | |
| print("Drive Outcomes:") | |
| drive_outcomes = not_three_pass_games.group_by('drive_result').agg( | |
| pl.len().alias('count') | |
| ).sort('count', descending=True) | |
| for row in drive_outcomes.iter_rows(named=True): | |
| print(f" {row['drive_result']}: {row['count']}") | |
| print("\n" + "=" * 80) | |
| print("COMPARISON") | |
| print("=" * 80) | |
| # Calculate TD rates | |
| td_rate_3pass = 0 | |
| td_rate_other = 0 | |
| td_rate_run_first = 0 | |
| if len(three_pass_games) > 0: | |
| td_count_3pass = three_pass_games.filter(pl.col('drive_result') == 'Touchdown').height | |
| td_rate_3pass = td_count_3pass / len(three_pass_games) * 100 | |
| if len(not_three_pass_games) > 0: | |
| td_count_other = not_three_pass_games.filter(pl.col('drive_result') == 'Touchdown').height | |
| td_rate_other = td_count_other / len(not_three_pass_games) * 100 | |
| if len(run_first_games) > 0: | |
| td_count_run = run_first_games.filter(pl.col('drive_result') == 'Touchdown').height | |
| td_rate_run_first = td_count_run / len(run_first_games) * 100 | |
| if len(three_pass_games) > 0 and len(not_three_pass_games) > 0: | |
| wp_3pass = three_pass_games.filter(pl.col('game_result') == 'W').height / len(three_pass_games) * 100 | |
| wp_other = not_three_pass_games.filter(pl.col('game_result') == 'W').height / len(not_three_pass_games) * 100 | |
| print(f"\nWin % when starting with 3 passes: {wp_3pass:.1f}%") | |
| print(f"Win % when NOT starting with 3 passes: {wp_other:.1f}%") | |
| print(f"Difference: {wp_3pass - wp_other:+.1f} percentage points") | |
| print(f"\nTD % when starting with 3 passes: {td_rate_3pass:.1f}% ({td_count_3pass}/{len(three_pass_games)})") | |
| print(f"TD % when NOT starting with 3 passes: {td_rate_other:.1f}% ({td_count_other}/{len(not_three_pass_games)})") | |
| if len(run_first_games) > 0: | |
| wp_run_first = run_first_games.filter(pl.col('game_result') == 'W').height / len(run_first_games) * 100 | |
| print(f"\n{'='*80}") | |
| print(f"GAMES STARTING WITH A RUN: {len(run_first_games)}") | |
| print(f"{'='*80}") | |
| print(f"Win %: {wp_run_first:.1f}%") | |
| print(f"TD %: {td_rate_run_first:.1f}% ({td_count_run}/{len(run_first_games)})") | |
| # Show drive outcomes for run-first | |
| print("\nDrive Outcomes:") | |
| drive_outcomes = run_first_games.group_by('drive_result').agg( | |
| pl.len().alias('count') | |
| ).sort('count', descending=True) | |
| for row in drive_outcomes.iter_rows(named=True): | |
| print(f" {row['drive_result']}: {row['count']}") | |
| return results_df | |
| def calculate_league_opening_drive_stats(seasons=range(2019, 2025)): | |
| """Calculate league-wide opening drive statistics for comparison""" | |
| print("\n" + "=" * 80) | |
| print("CALCULATING LEAGUE-WIDE OPENING DRIVE STATISTICS") | |
| print("=" * 80) | |
| pbp = nfl.load_pbp(seasons=seasons) | |
| # Get all games | |
| all_games = pbp.select('game_id').unique() | |
| opening_drives = [] | |
| for game_row in all_games.iter_rows(named=True): | |
| game_id = game_row['game_id'] | |
| game_plays = pbp.filter(pl.col('game_id') == game_id) | |
| # Get both teams | |
| teams = game_plays.select('posteam').unique().drop_nulls() | |
| for team_row in teams.iter_rows(named=True): | |
| team = team_row['posteam'] | |
| if team is None: | |
| continue | |
| # Get first drive for this team | |
| team_drives = game_plays.filter( | |
| (pl.col('posteam') == team) & | |
| (pl.col('drive').is_not_null()) | |
| ).sort(['qtr', 'game_seconds_remaining'], descending=[False, True]) | |
| if len(team_drives) == 0: | |
| continue | |
| first_drive_num = team_drives.select('drive').head(1).item() | |
| first_drive = team_drives.filter(pl.col('drive') == first_drive_num) | |
| drive_result = first_drive.select('fixed_drive_result').head(1).item() | |
| opening_drives.append({ | |
| 'team': team, | |
| 'game_id': game_id, | |
| 'drive_result': drive_result | |
| }) | |
| drives_df = pl.DataFrame(opening_drives) | |
| print(f"\nTotal opening drives analyzed: {len(drives_df)}") | |
| # Calculate touchdown rate | |
| td_drives = drives_df.filter(pl.col('drive_result') == 'Touchdown').height | |
| td_rate = td_drives / len(drives_df) * 100 | |
| print(f"\nOpening drives ending in Touchdown: {td_drives} ({td_rate:.1f}%)") | |
| # Show all outcomes | |
| print("\nAll opening drive outcomes:") | |
| outcomes = drives_df.group_by('drive_result').agg( | |
| pl.len().alias('count') | |
| ).sort('count', descending=True) | |
| for row in outcomes.iter_rows(named=True): | |
| pct = row['count'] / len(drives_df) * 100 | |
| print(f" {row['drive_result']}: {row['count']} ({pct:.1f}%)") | |
| return td_rate | |
| if __name__ == "__main__": | |
| results = analyze_lafleur_opening_drives() | |
| league_td_rate = calculate_league_opening_drive_stats() | |
| print("\n\nAnalysis complete!") | |
| print("\n" + "=" * 80) | |
| print("CONTEXT") | |
| print("=" * 80) | |
| # Calculate LaFleur's TD rates | |
| three_pass = results.filter(pl.col('started_3_passes') == True) | |
| run_first = results.filter(pl.col('play_1') == 'run') | |
| td_3pass = three_pass.filter(pl.col('drive_result') == 'Touchdown').height if len(three_pass) > 0 else 0 | |
| td_run = run_first.filter(pl.col('drive_result') == 'Touchdown').height if len(run_first) > 0 else 0 | |
| rate_3pass = (td_3pass / len(three_pass) * 100) if len(three_pass) > 0 else 0 | |
| rate_run = (td_run / len(run_first) * 100) if len(run_first) > 0 else 0 | |
| print(f"LaFleur's TD rate when starting with 3 passes: {rate_3pass:.1f}% ({td_3pass} of {len(three_pass)})") | |
| print(f"LaFleur's TD rate when starting with a run: {rate_run:.1f}% ({td_run} of {len(run_first)})") | |
| print(f"League average opening drive TD rate (2019-2024): {league_td_rate:.1f}%") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Loading play-by-play data... | |
| Total games found: 108 | |
| ================================================================================ | |
| Games analyzed: 106 | |
| ================================================================================ | |
| ================================================================================ | |
| GAMES STARTING WITH 3 PASSES: 12 | |
| ================================================================================ | |
| Overall Record: 7-5 | |
| Win Percentage: 58.3% | |
| Drive Outcomes: | |
| Punt: 3 | |
| Touchdown: 3 | |
| Field goal: 2 | |
| Missed field goal: 2 | |
| Turnover: 1 | |
| Turnover on downs: 1 | |
| Detailed Game Results: | |
| -------------------------------------------------------------------------------- | |
| 2019 Week 4 vs PHI | |
| Result: L (27-34) | |
| Drive Outcome: Touchdown | |
| Play 1 (pass): (12:49) (Shotgun) 12-A.Rodgers pass short left to 30-J.Williams to GB 11 for no gain (53-N.Bradham). | |
| Play 2 (pass): (12:25) (Shotgun) 12-A.Rodgers pass short middle to 17-D.Adams to GB 35 for 9 yards (27-M.Jenkins). | |
| Play 3 (pass): (11:52) (Shotgun) 12-A.Rodgers pass deep right to 17-D.Adams to PHI 7 for 58 yards (22-S.Jones). | |
| 2019 Week 9 vs LAC | |
| Result: L (11-26) | |
| Drive Outcome: Punt | |
| Play 1 (pass): (6:50) (Shotgun) 12-A.Rodgers pass short right to 33-A.Jones to GB 24 for -1 yards (52-D.Perryman). | |
| Play 2 (pass): (5:50) (Shotgun) 12-A.Rodgers pass short middle to 17-D.Adams to GB 27 for 8 yards (52-D.Perryman). | |
| Play 3 (pass): (5:09) (Shotgun) 12-A.Rodgers sacked at GB 18 for -9 yards (97-J.Bosa). | |
| 2020 Week 4 vs ATL | |
| Result: W (30-16) | |
| Drive Outcome: Touchdown | |
| Play 1 (pass): (13:33) (Shotgun) 12-A.Rodgers pass incomplete deep left to 83-M.Valdes-Scantling. | |
| Play 2 (pass): (13:28) (Shotgun) 12-A.Rodgers pass short middle to 85-R.Tonyan to ATL 46 for 27 yards (32-J.Hawkins | |
| Play 3 (pass): (12:50) (Shotgun) 12-A.Rodgers pass short right to 33-A.Jones to ATL 38 for 8 yards (54-F.Oluokun). | |
| 2020 Week 14 vs DET | |
| Result: W (31-24) | |
| Drive Outcome: Touchdown | |
| Play 1 (pass): (9:24) 12-A.Rodgers pass short right to 17-D.Adams pushed ob at GB 29 for 4 yards (24-A.Oruwariye). | |
| Play 2 (pass): (8:51) (Shotgun) 12-A.Rodgers pass short right to 19-E.St. Brown to GB 44 for 15 yards (51-J.Tavai). | |
| Play 3 (pass): (8:10) (Shotgun) 12-A.Rodgers pass short right to 17-D.Adams for 56 yards, TOUCHDOWN. | |
| 2021 Week 10 vs SEA | |
| Result: W (17-0) | |
| Drive Outcome: Missed field goal | |
| Play 1 (pass): (14:54) (Shotgun) 12-A.Rodgers pass short left to 17-D.Adams pushed ob at GB 39 for 3 yards (2-D.Ree | |
| Play 2 (pass): (14:15) (Shotgun) 12-A.Rodgers pass incomplete short left to 17-D.Adams (2-D.Reed). | |
| Play 3 (pass): (14:12) (Shotgun) 12-A.Rodgers pass deep left to 83-M.Valdes-Scantling to SEA 20 for 41 yards (2-D.R | |
| 2021 Week 11 vs MIN | |
| Result: L (31-34) | |
| Drive Outcome: Field goal | |
| Play 1 (pass): (15:00) (Shotgun) 12-A.Rodgers pass short right to 17-D.Adams to MIN 38 for 37 yards (54-E.Kendricks | |
| Play 2 (pass): (14:17) (Shotgun) 12-A.Rodgers pass incomplete short left to 83-M.Valdes-Scantling (23-X.Woods). | |
| Play 3 (pass): (14:13) (Shotgun) 12-A.Rodgers pass short right to 28-A.Dillon to MIN 36 for 2 yards (96-A.Watts; 54 | |
| 2021 Week 14 vs CHI | |
| Result: W (45-30) | |
| Drive Outcome: Punt | |
| Play 1 (pass): (11:28) 12-A.Rodgers pass short left to 13-A.Lazard to GB 26 for 6 yards (25-A.Burns, 58-R.Smith). | |
| Play 2 (pass): (10:52) (Shotgun) 12-A.Rodgers sacked at GB 15 for -11 yards (94-R.Quinn). | |
| Play 3 (pass): (10:09) (Shotgun) 12-A.Rodgers pass short right to 81-J.Deguara to GB 21 for 6 yards (21-X.Crawford) | |
| 2021 Week 16 vs CLE | |
| Result: W (24-22) | |
| Drive Outcome: Punt | |
| Play 1 (pass): (12:21) (Shotgun) 12-A.Rodgers pass short right to 89-M.Lewis to GB 26 for 1 yard (21-D.Ward). | |
| Play 2 (pass): (11:50) 12-A.Rodgers pass incomplete short middle to 33-A.Jones. | |
| Play 3 (pass): (11:46) (Shotgun) 12-A.Rodgers pass short left to 33-A.Jones to GB 34 for 8 yards (36-M.Stewart, 22- | |
| 2022 Week 9 vs DET | |
| Result: L (9-15) | |
| Drive Outcome: Turnover | |
| Play 1 (pass): (10:51) 12-A.Rodgers pass deep middle to 87-R.Doubs to GB 25 for 18 yards (31-K.Joseph; 23-M.Hughes) | |
| Play 2 (pass): (10:06) 12-A.Rodgers pass incomplete short right. Ball thrown away. | |
| Play 3 (pass): (10:00) (Shotgun) 12-A.Rodgers pass incomplete short middle to 13-A.Lazard (5-D.Elliott). | |
| 2023 Week 4 vs DET | |
| Result: L (20-34) | |
| Drive Outcome: Field goal | |
| Play 1 (pass): (13:34) (Shotgun) 10-J.Love pass incomplete short right to 33-A.Jones. | |
| Play 2 (pass): (13:25) (Shotgun) 10-J.Love sacked at DET 25 for -9 yards (97-A.Hutchinson). | |
| Play 3 (pass): (12:41) (Shotgun) 10-J.Love pass short left to 87-R.Doubs to DET 16 for 9 yards (6-I.Melifonwu). | |
| 2023 Week 11 vs LAC | |
| Result: W (23-20) | |
| Drive Outcome: Turnover on downs | |
| Play 1 (pass): (14:56) (Shotgun) 10-J.Love pass short left to 87-R.Doubs to GB 23 for 15 yards (3-D.James). | |
| Play 2 (pass): (14:19) (Shotgun) 10-J.Love pass short left to 87-R.Doubs to GB 31 for 8 yards (43-M.Davis). | |
| Play 3 (pass): (13:34) (Shotgun) 10-J.Love pass short right to 88-L.Musgrave to GB 42 for 11 yards (20-D.Marlowe). | |
| 2023 Week 18 vs CHI | |
| Result: W (17-9) | |
| Drive Outcome: Missed field goal | |
| Play 1 (pass): (8:54) 10-J.Love pass short left to 33-A.Jones to GB 29 for 4 yards (49-T.Edmunds). | |
| Play 2 (pass): (8:12) (Shotgun) 10-J.Love pass incomplete short left to 13-D.Wicks. | |
| Play 3 (pass): (8:09) (Shotgun) 10-J.Love pass short right to 33-A.Jones to GB 36 for 7 yards (53-T.Edwards). | |
| ================================================================================ | |
| GAMES NOT STARTING WITH 3 PASSES: 94 | |
| ================================================================================ | |
| Overall Record: 62-32 | |
| Win Percentage: 66.0% | |
| Drive Outcomes: | |
| Punt: 38 | |
| Touchdown: 30 | |
| Field goal: 15 | |
| Turnover: 6 | |
| Turnover on downs: 3 | |
| Missed field goal: 2 | |
| ================================================================================ | |
| COMPARISON | |
| ================================================================================ | |
| Win % when starting with 3 passes: 58.3% | |
| Win % when NOT starting with 3 passes: 66.0% | |
| Difference: -7.6 percentage points | |
| TD % when starting with 3 passes: 25.0% (3/12) | |
| TD % when NOT starting with 3 passes: 31.9% (30/94) | |
| ================================================================================ | |
| GAMES STARTING WITH A RUN: 56 | |
| ================================================================================ | |
| Win %: 64.3% | |
| TD %: 26.8% (15/56) | |
| Drive Outcomes: | |
| Punt: 23 | |
| Touchdown: 15 | |
| Field goal: 13 | |
| Turnover: 2 | |
| Missed field goal: 2 | |
| Turnover on downs: 1 | |
| ================================================================================ | |
| CALCULATING LEAGUE-WIDE OPENING DRIVE STATISTICS | |
| ================================================================================ | |
| Total opening drives analyzed: 3350 | |
| Opening drives ending in Touchdown: 799 (23.9%) | |
| All opening drive outcomes: | |
| Punt: 1498 (44.7%) | |
| Touchdown: 799 (23.9%) | |
| Field goal: 533 (15.9%) | |
| Turnover: 282 (8.4%) | |
| Turnover on downs: 115 (3.4%) | |
| Missed field goal: 76 (2.3%) | |
| Opp touchdown: 41 (1.2%) | |
| Safety: 6 (0.2%) | |
| Analysis complete! | |
| ================================================================================ | |
| CONTEXT | |
| ================================================================================ | |
| LaFleur's TD rate when starting with 3 passes: 25.0% (3 of 12) | |
| LaFleur's TD rate when starting with a run: 26.8% (15 of 56) | |
| League average opening drive TD rate (2019-2024): 23.9% |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment