Case Study 2: Diagnosing a Struggling Run Game
Overview
In this case study, you'll analyze a team's struggling run game to determine whether the problem lies with the running backs, offensive line, play-calling, or some combination. This reflects real in-season analysis that teams perform.
The Scenario
State University's run game has underperformed expectations. After averaging 4.8 YPC last season, they're only managing 3.6 YPC through 6 games. The coaching staff wants answers before making personnel changes.
Part 1: The Problem
import pandas as pd
import numpy as np
from typing import Dict, List
# Season comparison
last_season = {
'games': 13,
'carries': 456,
'yards': 2189,
'touchdowns': 22,
'success_rate': 48.2,
'avg_ybc': 2.8,
'avg_yac': 2.0,
'stuff_rate': 16.5,
'explosive_rate': 14.2
}
this_season = {
'games': 6,
'carries': 198,
'yards': 713,
'touchdowns': 5,
'success_rate': 38.4,
'avg_ybc': 1.9,
'avg_yac': 1.7,
'stuff_rate': 24.7,
'explosive_rate': 9.1
}
print("=" * 70)
print("SEASON COMPARISON: RUN GAME")
print("=" * 70)
print(f"{'Metric':<25} {'Last Season':>15} {'This Season':>15} {'Change':>12}")
print("-" * 70)
metrics = ['ypc', 'success_rate', 'avg_ybc', 'avg_yac', 'stuff_rate', 'explosive_rate']
# Calculate YPC
last_season['ypc'] = last_season['yards'] / last_season['carries']
this_season['ypc'] = this_season['yards'] / this_season['carries']
for metric in metrics:
last = last_season.get(metric, 0)
this = this_season.get(metric, 0)
change = this - last
indicator = "⚠️" if (metric in ['stuff_rate'] and change > 0) or \
(metric not in ['stuff_rate'] and change < 0) else ""
print(f"{metric:<25} {last:>15.1f} {this:>15.1f} {change:>+11.1f} {indicator}")
Part 2: Component Analysis
class RunGameDiagnostic:
"""Diagnose run game issues."""
def __init__(self, carries: List[Dict]):
self.carries = carries
def analyze_blocking(self) -> Dict:
"""Analyze offensive line contribution."""
# Yards before contact
total_ybc = sum(c.get('yards_before_contact', 0) for c in self.carries)
avg_ybc = total_ybc / len(self.carries)
# Line yards (capped at 4)
line_yards = sum(min(c.get('yards_before_contact', 0), 4) for c in self.carries)
avg_line_yards = line_yards / len(self.carries)
# Opportunity rate (4+ yards before contact)
opportunities = sum(1 for c in self.carries
if c.get('yards_before_contact', 0) >= 4)
opportunity_rate = opportunities / len(self.carries) * 100
# Stuffs at line
stuffs_at_line = sum(1 for c in self.carries
if c.get('yards_before_contact', 0) <= 0)
stuff_rate = stuffs_at_line / len(self.carries) * 100
# Grade (A-F)
if avg_ybc >= 3.0:
grade = 'A'
elif avg_ybc >= 2.5:
grade = 'B'
elif avg_ybc >= 2.0:
grade = 'C'
elif avg_ybc >= 1.5:
grade = 'D'
else:
grade = 'F'
return {
'avg_ybc': round(avg_ybc, 2),
'avg_line_yards': round(avg_line_yards, 2),
'opportunity_rate': round(opportunity_rate, 1),
'stuff_rate_at_line': round(stuff_rate, 1),
'grade': grade
}
def analyze_backs(self) -> Dict:
"""Analyze running back contribution."""
# Yards after contact
total_yac = sum(
c.get('yards_gained', 0) - c.get('yards_before_contact', 0)
for c in self.carries
)
avg_yac = total_yac / len(self.carries)
# Broken tackles
broken_tackles = sum(c.get('broken_tackles', 0) for c in self.carries)
bt_per_carry = broken_tackles / len(self.carries)
# Missed opportunities (good blocking but short gain)
missed = sum(1 for c in self.carries
if c.get('yards_before_contact', 0) >= 4 and
c.get('yards_gained', 0) < c.get('yards_before_contact', 0) + 2)
missed_rate = missed / max(sum(1 for c in self.carries
if c.get('yards_before_contact', 0) >= 4), 1) * 100
# Grade
if avg_yac >= 2.5:
grade = 'A'
elif avg_yac >= 2.0:
grade = 'B'
elif avg_yac >= 1.5:
grade = 'C'
elif avg_yac >= 1.0:
grade = 'D'
else:
grade = 'F'
return {
'avg_yac': round(avg_yac, 2),
'bt_per_carry': round(bt_per_carry, 3),
'missed_opportunity_rate': round(missed_rate, 1),
'grade': grade
}
def analyze_scheme(self) -> Dict:
"""Analyze play-calling and scheme effectiveness."""
# Run gap distribution
gaps = {'A': 0, 'B': 0, 'C': 0, 'outside': 0}
for c in self.carries:
gap = c.get('run_gap', 'B')
if gap in gaps:
gaps[gap] += 1
# Success by gap
gap_success = {}
for gap in ['A', 'B', 'C', 'outside']:
gap_carries = [c for c in self.carries if c.get('run_gap') == gap]
if gap_carries:
success = sum(1 for c in gap_carries
if self._is_successful(c)) / len(gap_carries) * 100
gap_success[gap] = round(success, 1)
# Predictability (run rate by down)
first_down = [c for c in self.carries if c.get('down') == 1]
second_down = [c for c in self.carries if c.get('down') == 2]
# Box count faced
avg_box = np.mean([c.get('defenders_in_box', 7) for c in self.carries])
return {
'gap_distribution': gaps,
'gap_success': gap_success,
'avg_defenders_in_box': round(avg_box, 1),
'first_down_runs': len(first_down),
'run_heavy_first': len(first_down) > len(self.carries) * 0.4
}
def _is_successful(self, carry: Dict) -> bool:
"""Determine if carry was successful."""
down = carry.get('down', 1)
distance = carry.get('distance', 10)
yards = carry.get('yards_gained', 0)
if down == 1:
return yards >= distance * 0.4
elif down == 2:
return yards >= distance * 0.5
else:
return yards >= distance
def generate_diagnosis(self) -> Dict:
"""Generate full diagnosis with attribution."""
blocking = self.analyze_blocking()
backs = self.analyze_backs()
scheme = self.analyze_scheme()
# Attribution percentages
if blocking['grade'] in ['D', 'F'] and backs['grade'] in ['B', 'A']:
attribution = {'blocking': 60, 'backs': 15, 'scheme': 25}
primary_issue = 'OFFENSIVE LINE'
elif backs['grade'] in ['D', 'F'] and blocking['grade'] in ['B', 'A']:
attribution = {'blocking': 15, 'backs': 60, 'scheme': 25}
primary_issue = 'RUNNING BACKS'
elif scheme['avg_defenders_in_box'] > 7.5:
attribution = {'blocking': 30, 'backs': 20, 'scheme': 50}
primary_issue = 'SCHEME/PREDICTABILITY'
else:
attribution = {'blocking': 35, 'backs': 35, 'scheme': 30}
primary_issue = 'MULTIPLE FACTORS'
return {
'blocking_analysis': blocking,
'back_analysis': backs,
'scheme_analysis': scheme,
'attribution': attribution,
'primary_issue': primary_issue
}
# Generate sample carry data for this season
np.random.seed(42)
this_season_carries = []
for _ in range(198):
# Simulate poor blocking (avg YBC of 1.9)
ybc = max(0, np.random.normal(1.9, 1.5))
# Simulate slightly below average YAC
yac = max(0, np.random.exponential(1.4))
this_season_carries.append({
'yards_before_contact': round(ybc, 1),
'yards_gained': round(ybc + yac, 1),
'broken_tackles': 1 if np.random.random() < 0.12 else 0,
'down': np.random.choice([1, 2, 3], p=[0.45, 0.35, 0.20]),
'distance': np.random.choice([10, 8, 5, 3, 2], p=[0.4, 0.2, 0.2, 0.1, 0.1]),
'run_gap': np.random.choice(['A', 'B', 'C', 'outside'], p=[0.25, 0.35, 0.25, 0.15]),
'defenders_in_box': np.random.choice([6, 7, 8, 9], p=[0.15, 0.35, 0.35, 0.15])
})
# Run diagnosis
diagnostic = RunGameDiagnostic(this_season_carries)
diagnosis = diagnostic.generate_diagnosis()
print("\n" + "=" * 70)
print("RUN GAME DIAGNOSIS")
print("=" * 70)
print("\nOFFENSIVE LINE ANALYSIS:")
print("-" * 40)
for k, v in diagnosis['blocking_analysis'].items():
print(f" {k}: {v}")
print("\nRUNNING BACK ANALYSIS:")
print("-" * 40)
for k, v in diagnosis['back_analysis'].items():
print(f" {k}: {v}")
print("\nSCHEME ANALYSIS:")
print("-" * 40)
for k, v in diagnosis['scheme_analysis'].items():
print(f" {k}: {v}")
print(f"\n{'='*70}")
print(f"PRIMARY ISSUE: {diagnosis['primary_issue']}")
print(f"{'='*70}")
print(f"Attribution: O-Line {diagnosis['attribution']['blocking']}%, "
f"Backs {diagnosis['attribution']['backs']}%, "
f"Scheme {diagnosis['attribution']['scheme']}%")
Part 3: Position-by-Position Analysis
# Individual linemen analysis (simulated grades)
linemen_grades = {
'LT Johnson': {'run_block': 72.5, 'pass_block': 78.2, 'stuffs_allowed': 8},
'LG Smith': {'run_block': 58.4, 'pass_block': 65.1, 'stuffs_allowed': 15},
'C Williams': {'run_block': 68.2, 'pass_block': 71.5, 'stuffs_allowed': 9},
'RG Davis': {'run_block': 52.1, 'pass_block': 60.8, 'stuffs_allowed': 18},
'RT Brown': {'run_block': 65.5, 'pass_block': 68.9, 'stuffs_allowed': 12}
}
# Running back analysis
rb_analysis = {
'RB1 (Starter)': {
'carries': 125, 'yards': 438, 'ypc': 3.50,
'avg_yac': 1.5, 'bt_per_carry': 0.11, 'success_rate': 36.0
},
'RB2 (Backup)': {
'carries': 48, 'yards': 185, 'ypc': 3.85,
'avg_yac': 2.1, 'bt_per_carry': 0.15, 'success_rate': 44.0
},
'RB3 (Third)': {
'carries': 25, 'yards': 90, 'ypc': 3.60,
'avg_yac': 1.8, 'bt_per_carry': 0.08, 'success_rate': 40.0
}
}
print("\n" + "=" * 70)
print("OFFENSIVE LINE GRADES")
print("=" * 70)
print(f"{'Player':<15} {'Run Block':>12} {'Stuffs Allowed':>15}")
print("-" * 45)
for player, grades in linemen_grades.items():
print(f"{player:<15} {grades['run_block']:>12.1f} {grades['stuffs_allowed']:>15}")
# Identify weak links
weak_spots = [p for p, g in linemen_grades.items() if g['run_block'] < 60]
print(f"\nWeak Spots: {', '.join(weak_spots)}")
print("\n" + "=" * 70)
print("RUNNING BACK BREAKDOWN")
print("=" * 70)
print(f"{'RB':<15} {'Carries':>8} {'YPC':>6} {'YAC':>6} {'BT/C':>8} {'Succ%':>8}")
print("-" * 55)
for rb, stats in rb_analysis.items():
print(f"{rb:<15} {stats['carries']:>8} {stats['ypc']:>6.2f} "
f"{stats['avg_yac']:>6.1f} {stats['bt_per_carry']:>8.2f} {stats['success_rate']:>7.1f}%")
Part 4: Recommendations
def generate_recommendations(diagnosis: Dict, linemen: Dict, rbs: Dict) -> str:
"""Generate actionable recommendations."""
recommendations = """
╔══════════════════════════════════════════════════════════════════════════╗
║ RUN GAME IMPROVEMENT PLAN ║
╠══════════════════════════════════════════════════════════════════════════╣
║ ║
║ IMMEDIATE ACTIONS: ║
║ ─────────────────────────────────────────────────────────────────────── ║
"""
if diagnosis['primary_issue'] == 'OFFENSIVE LINE':
recommendations += """
║ 1. PERSONNEL CHANGE: Bench RG Davis (52.1 run block grade) ║
║ - Move G Backup to starting RG position ║
║ - RG Davis has 18 stuffs allowed (team high) ║
║ ║
║ 2. SCHEME ADJUSTMENT: Run away from weak side ║
║ - Increase runs to left side (stronger blockers) ║
║ - Reduce inside zone, add more gap/power concepts ║
║ ║
║ 3. GAME PLAN: Include more pulling guards ║
║ - Get Smith (LG) moving to create angles ║
"""
else:
recommendations += """
║ 1. INCREASE RB2's CARRIES ║
║ - RB2 showing better YAC and success rate ║
║ - Consider 60/40 split instead of current 75/25 ║
"""
recommendations += """
║ ║
║ SCHEME ADJUSTMENTS: ║
║ ─────────────────────────────────────────────────────────────────────── ║
║ ║
║ • Reduce predictable 1st down runs (45% of runs on 1st down) ║
║ • Mix in more play-action to lighten boxes (avg 7.5 in box) ║
║ • Add RPO concepts to give RBs better angles ║
║ • Increase outside runs (only 15% currently) ║
║ ║
║ DO NOT RECOMMEND: ║
║ ─────────────────────────────────────────────────────────────────────── ║
║ ║
║ ✗ Abandoning the run game entirely ║
║ Analysis shows issues are correctable ║
║ ║
║ ✗ Benching RB1 completely ║
║ Production issues primarily blocking-related ║
║ ║
╚══════════════════════════════════════════════════════════════════════════╝
"""
return recommendations
print(generate_recommendations(diagnosis, linemen_grades, rb_analysis))
Summary
This case study demonstrated how to diagnose run game issues:
- Compare to baseline: Identify what changed from successful periods
- Component analysis: Separate blocking, back skill, and scheme
- Attribution: Determine primary cause of struggles
- Position-level analysis: Find specific weak links
- Actionable recommendations: Personnel and scheme adjustments
Key Findings:
- Offensive line (specifically RG) is primary issue
- RB2 outperforming starter in limited carries
- Scheme too predictable (heavy 1st down runs)
- Defenses loading box due to lack of play-action