Skip to content

Instantly share code, notes, and snippets.

@earlonrails
Last active September 29, 2025 18:43
Show Gist options
  • Select an option

  • Save earlonrails/3e39e26d5ab240fff8aa40b28d41d512 to your computer and use it in GitHub Desktop.

Select an option

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.
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}%")
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