Yards per carry (YPC) has long been the standard measure of rushing efficiency, but it fails to capture crucial aspects of modern running game analysis. A back who consistently gains 4 yards on 3rd and 2 provides more value than one who averages 5...
In This Chapter
- Learning Objectives
- 8.1 Introduction: Beyond Yards Per Carry
- 8.2 Rushing Success Rate
- 8.3 Yards After Contact
- 8.4 EPA for Rushing
- 8.5 Rushing Yards Over Expected (RYOE)
- 8.6 Situational Rushing Analysis
- 8.7 Run Blocking Analysis
- 8.8 Comprehensive Running Back Evaluation
- 8.9 Summary
- Exercises
- Further Reading
Chapter 8: Rushing and Running Game Analysis
Learning Objectives
By the end of this chapter, you will be able to:
- Calculate and interpret advanced rushing metrics beyond yards per carry
- Understand rushing success rate and its predictive value
- Analyze rushing efficiency using Expected Points Added (EPA)
- Implement yards after contact and broken tackle metrics
- Evaluate run blocking using available statistics
- Build rushing tendency and play-type analysis
- Create comprehensive running back evaluation systems
8.1 Introduction: Beyond Yards Per Carry
Yards per carry (YPC) has long been the standard measure of rushing efficiency, but it fails to capture crucial aspects of modern running game analysis. A back who consistently gains 4 yards on 3rd and 2 provides more value than one who averages 5 yards but frequently gets stuffed in short-yardage situations.
The Limitations of Traditional Rushing Stats
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple, Optional
# Example: Same YPC, different value
rb_a = {
'carries': 200, 'yards': 900, 'ypc': 4.5,
'explosive_runs': 8, 'stuffed_runs': 40,
'third_short_conv': 12, 'third_short_att': 25
}
rb_b = {
'carries': 200, 'yards': 900, 'ypc': 4.5,
'explosive_runs': 15, 'stuffed_runs': 55,
'third_short_conv': 8, 'third_short_att': 20
}
print("Two Running Backs with 4.5 YPC:")
print("-" * 50)
print(f" RB A: Fewer explosive runs ({rb_a['explosive_runs']}), but more consistent")
print(f" Third-and-short: {rb_a['third_short_conv']}/{rb_a['third_short_att']} ({rb_a['third_short_conv']/rb_a['third_short_att']*100:.0f}%)")
print(f" Stuffed rate: {rb_a['stuffed_runs']/rb_a['carries']*100:.0f}%")
print()
print(f" RB B: More explosive runs ({rb_b['explosive_runs']}), but more stuffed runs")
print(f" Third-and-short: {rb_b['third_short_conv']}/{rb_b['third_short_att']} ({rb_b['third_short_conv']/rb_b['third_short_att']*100:.0f}%)")
print(f" Stuffed rate: {rb_b['stuffed_runs']/rb_b['carries']*100:.0f}%")
What This Chapter Covers
- Rushing Success Rate: Measuring value creation per carry
- Expected Yards and RYOE: Rush Yards Over Expected
- Yards After Contact: Separating back skill from blocking
- Situational Analysis: Short-yardage, goal-line, and late-game
- Efficiency Metrics: EPA for rushing plays
- Run Blocking Analysis: Evaluating the offensive line's contribution
- Comprehensive Evaluation: Multi-factor running back assessment
8.2 Rushing Success Rate
Success rate measures the percentage of carries that "succeed" based on down and distance context. A successful run gains positive expected points, while an unsuccessful run loses value.
Defining Success
class RushingSuccessCalculator:
"""Calculate rushing success rate with customizable criteria."""
def __init__(self, criteria: str = 'standard'):
"""
Initialize with success criteria.
Parameters:
-----------
criteria : str
'standard' - Traditional thresholds
'epa' - Based on expected points added
"""
self.criteria = criteria
def is_successful(self, carry: Dict) -> bool:
"""
Determine if a carry was successful.
Standard criteria:
- 1st down: 40%+ of yards needed (4+ yards on 1st and 10)
- 2nd down: 50%+ of yards needed
- 3rd/4th down: 100% of yards needed (conversion)
Parameters:
-----------
carry : dict
Dictionary with down, distance, yards_gained, (epa optional)
Returns:
--------
bool : Whether the carry was successful
"""
if self.criteria == 'epa':
return carry.get('epa', 0) > 0
down = carry['down']
distance = carry['distance']
yards = carry['yards_gained']
if down == 1:
return yards >= distance * 0.40
elif down == 2:
return yards >= distance * 0.50
else: # 3rd or 4th down
return yards >= distance
def calculate_success_rate(self, carries: List[Dict]) -> Dict:
"""
Calculate success rate for a set of carries.
Returns breakdown by down and overall.
"""
if not carries:
return {}
# Overall
successful = sum(1 for c in carries if self.is_successful(c))
overall_rate = successful / len(carries) * 100
# By down
by_down = {}
for down in [1, 2, 3, 4]:
down_carries = [c for c in carries if c['down'] == down]
if down_carries:
down_success = sum(1 for c in down_carries if self.is_successful(c))
by_down[f'down_{down}'] = {
'carries': len(down_carries),
'successful': down_success,
'rate': round(down_success / len(down_carries) * 100, 1)
}
return {
'total_carries': len(carries),
'successful': successful,
'success_rate': round(overall_rate, 1),
'by_down': by_down
}
# Demonstrate success rate
calc = RushingSuccessCalculator()
sample_carries = [
{'down': 1, 'distance': 10, 'yards_gained': 5}, # Success (50% > 40%)
{'down': 1, 'distance': 10, 'yards_gained': 2}, # Failure (20% < 40%)
{'down': 2, 'distance': 6, 'yards_gained': 4}, # Success (67% > 50%)
{'down': 2, 'distance': 8, 'yards_gained': 3}, # Failure (37.5% < 50%)
{'down': 3, 'distance': 2, 'yards_gained': 3}, # Success (conversion)
{'down': 3, 'distance': 4, 'yards_gained': 3}, # Failure (no conversion)
]
result = calc.calculate_success_rate(sample_carries)
print("Rushing Success Rate Analysis:")
print("-" * 50)
print(f"Overall Success Rate: {result['success_rate']}%")
print(f"Total Carries: {result['total_carries']}")
print(f"Successful: {result['successful']}")
for down, stats in result['by_down'].items():
print(f" {down}: {stats['successful']}/{stats['carries']} ({stats['rate']}%)")
8.3 Yards After Contact
Yards After Contact (YAC) measures how many yards a running back gains after being contacted by a defender. This metric helps separate back skill from offensive line blocking.
class YardsAfterContactAnalyzer:
"""Analyze yards after contact metrics."""
def __init__(self):
pass
def calculate_yac_metrics(self, carries: List[Dict]) -> Dict:
"""
Calculate yards after contact metrics.
Parameters:
-----------
carries : list
List of carries with yards_gained, yards_before_contact
Returns:
--------
dict : YAC metrics
"""
if not carries:
return {}
total_yards = sum(c.get('yards_gained', 0) for c in carries)
total_ybc = sum(c.get('yards_before_contact', 0) for c in carries)
total_yac = sum(
c.get('yards_gained', 0) - c.get('yards_before_contact', 0)
for c in carries
)
# Broken tackles
broken_tackles = sum(c.get('broken_tackles', 0) for c in carries)
return {
'total_carries': len(carries),
'total_yards': total_yards,
'yards_per_carry': round(total_yards / len(carries), 2),
'total_yards_before_contact': total_ybc,
'avg_yards_before_contact': round(total_ybc / len(carries), 2),
'total_yards_after_contact': total_yac,
'avg_yards_after_contact': round(total_yac / len(carries), 2),
'yac_share': round(total_yac / total_yards * 100, 1) if total_yards > 0 else 0,
'broken_tackles': broken_tackles,
'broken_tackles_per_carry': round(broken_tackles / len(carries), 3)
}
def compare_backs(self, rb_data: Dict[str, List[Dict]]) -> pd.DataFrame:
"""Compare multiple running backs by YAC metrics."""
results = []
for rb_name, carries in rb_data.items():
metrics = self.calculate_yac_metrics(carries)
results.append({
'rb': rb_name,
'carries': metrics['total_carries'],
'ypc': metrics['yards_per_carry'],
'avg_ybc': metrics['avg_yards_before_contact'],
'avg_yac': metrics['avg_yards_after_contact'],
'yac_share': metrics['yac_share'],
'bt_per_carry': metrics['broken_tackles_per_carry']
})
df = pd.DataFrame(results)
df['yac_rank'] = df['avg_yac'].rank(ascending=False).astype(int)
return df.sort_values('yac_rank')
# Demonstrate YAC analysis
yac_analyzer = YardsAfterContactAnalyzer()
# Generate sample data for two backs
np.random.seed(42)
def generate_carries(n: int, blocking_quality: float, back_skill: float) -> List[Dict]:
"""Generate carries with given blocking and back skill modifiers."""
carries = []
for _ in range(n):
# Yards before contact (blocking quality)
ybc = max(0, np.random.normal(2.5 + blocking_quality, 1.5))
# Yards after contact (back skill)
yac = max(0, np.random.exponential(1.5 + back_skill))
# Broken tackles
bt = 1 if np.random.random() < (0.15 + back_skill * 0.05) else 0
carries.append({
'yards_before_contact': round(ybc, 1),
'yards_gained': round(ybc + yac, 1),
'broken_tackles': bt
})
return carries
# Good blocking, average back
rb_good_line = generate_carries(80, 1.0, 0.0)
# Average blocking, elite back
rb_elite = generate_carries(75, 0.0, 1.0)
print("\nYards After Contact Comparison:")
print("-" * 70)
comparison = yac_analyzer.compare_backs({
'Good Line RB': rb_good_line,
'Elite Back': rb_elite
})
print(comparison.to_string(index=False))
print("\nInterpretation:")
print(" - Good Line RB: Higher YBC (blocking), lower YAC (back skill)")
print(" - Elite Back: Lower YBC (blocking), higher YAC (creates own yards)")
8.4 EPA for Rushing
Expected Points Added measures the value created by each rushing play, accounting for down, distance, and field position.
class RushingEPACalculator:
"""Calculate EPA for rushing plays."""
def __init__(self):
"""Initialize with simplified EP model."""
self._build_ep_model()
def _build_ep_model(self):
"""Build expected points lookup."""
self.ep_by_yard = {}
for yl in range(1, 100):
if yl <= 50:
self.ep_by_yard[yl] = -0.5 + yl * 0.06
else:
self.ep_by_yard[yl] = 2.5 + (yl - 50) * 0.09
def get_ep(self, yard_line: int, down: int, distance: int) -> float:
"""Get expected points for a situation."""
base = self.ep_by_yard.get(min(max(yard_line, 1), 99), 0)
down_adj = {1: 0.3, 2: 0.1, 3: -0.2, 4: -0.6}
dist_adj = -0.02 * min(distance, 20)
return base + down_adj.get(down, 0) + dist_adj
def calculate_rush_epa(self, carry: Dict) -> float:
"""Calculate EPA for a single carry."""
ep_before = self.get_ep(
carry['yard_line'], carry['down'], carry['distance']
)
yards = carry.get('yards_gained', 0)
new_yl = min(carry['yard_line'] + yards, 99)
# Check for TD
if new_yl >= 100:
return round(7.0 - ep_before, 2)
# Check for first down
if yards >= carry['distance']:
ep_after = self.get_ep(new_yl, 1, 10)
else:
next_down = carry['down'] + 1
new_distance = carry['distance'] - yards
if next_down > 4:
# Turnover on downs
opp_yl = 100 - new_yl
ep_after = -self.get_ep(opp_yl, 1, 10)
else:
ep_after = self.get_ep(new_yl, next_down, new_distance)
return round(ep_after - ep_before, 2)
def analyze_rushing_epa(self, carries: List[Dict]) -> Dict:
"""Calculate aggregate EPA metrics for carries."""
if not carries:
return {}
epas = [self.calculate_rush_epa(c) for c in carries]
return {
'carries': len(carries),
'total_epa': round(sum(epas), 2),
'epa_per_carry': round(sum(epas) / len(carries), 3),
'positive_epa_rate': round(sum(1 for e in epas if e > 0) / len(carries) * 100, 1),
'max_epa': round(max(epas), 2),
'min_epa': round(min(epas), 2)
}
# Demonstrate rushing EPA
epa_calc = RushingEPACalculator()
example_carries = [
{'yard_line': 25, 'down': 1, 'distance': 10, 'yards_gained': 5}, # Modest gain
{'yard_line': 50, 'down': 3, 'distance': 2, 'yards_gained': 4}, # Conversion!
{'yard_line': 35, 'down': 2, 'distance': 8, 'yards_gained': 1}, # Poor gain
{'yard_line': 95, 'down': 1, 'distance': 5, 'yards_gained': 5}, # Touchdown!
{'yard_line': 70, 'down': 1, 'distance': 10, 'yards_gained': -2}, # Loss
]
print("\nRushing EPA Examples:")
print("-" * 70)
for carry in example_carries:
epa = epa_calc.calculate_rush_epa(carry)
desc = f"{carry['down']}&{carry['distance']} at {carry['yard_line']}, {carry['yards_gained']} yards"
print(f" {desc}: EPA = {epa:+.2f}")
# Aggregate analysis
epa_stats = epa_calc.analyze_rushing_epa(example_carries)
print(f"\nAggregate: EPA/carry = {epa_stats['epa_per_carry']:.3f}")
8.5 Rushing Yards Over Expected (RYOE)
RYOE compares a back's actual yards to expected yards based on blocking quality, defensive alignment, and play design.
class ExpectedRushingModel:
"""
Model expected rushing yards based on play context.
In production, this uses tracking data. Here we use simplified factors.
"""
def __init__(self):
"""Initialize model coefficients."""
self.base_yards = 4.2 # League average
# Coefficients (simplified)
self.factors = {
'defenders_in_box': -0.35, # More defenders = fewer yards
'run_gap': {'A': -0.2, 'B': 0.0, 'C': 0.3, 'off_tackle': 0.4},
'shotgun': 0.3, # Shotgun slight boost
'play_action': 0.5, # Fake helps
'stacked_box': -1.2 # 8+ in box
}
def predict_yards(self, play: Dict) -> float:
"""
Predict expected yards for a rushing play.
Parameters:
-----------
play : dict
Play characteristics
Returns:
--------
float : Expected yards
"""
expected = self.base_yards
# Defenders in box adjustment
box = play.get('defenders_in_box', 7)
expected += self.factors['defenders_in_box'] * (box - 7)
# Run gap
gap = play.get('run_gap', 'B')
expected += self.factors['run_gap'].get(gap, 0)
# Formation
if play.get('shotgun', False):
expected += self.factors['shotgun']
# Stacked box (8+)
if play.get('defenders_in_box', 7) >= 8:
expected += self.factors['stacked_box']
return max(0, expected)
def calculate_ryoe(self, play: Dict) -> float:
"""Calculate Rush Yards Over Expected."""
expected = self.predict_yards(play)
actual = play.get('yards_gained', 0)
return round(actual - expected, 2)
def analyze_ryoe(self, carries: List[Dict]) -> Dict:
"""Calculate aggregate RYOE metrics."""
if not carries:
return {}
ryoes = [self.calculate_ryoe(c) for c in carries]
expected_total = sum(self.predict_yards(c) for c in carries)
actual_total = sum(c.get('yards_gained', 0) for c in carries)
return {
'carries': len(carries),
'actual_yards': actual_total,
'expected_yards': round(expected_total, 1),
'total_ryoe': round(actual_total - expected_total, 1),
'ryoe_per_carry': round(sum(ryoes) / len(carries), 2),
'positive_ryoe_rate': round(sum(1 for r in ryoes if r > 0) / len(carries) * 100, 1)
}
# Demonstrate RYOE
ryoe_model = ExpectedRushingModel()
sample_runs = [
{'yards_gained': 6, 'defenders_in_box': 7, 'run_gap': 'B', 'shotgun': False},
{'yards_gained': 2, 'defenders_in_box': 8, 'run_gap': 'A', 'shotgun': False},
{'yards_gained': 8, 'defenders_in_box': 6, 'run_gap': 'off_tackle', 'shotgun': True},
{'yards_gained': 1, 'defenders_in_box': 9, 'run_gap': 'A', 'shotgun': False},
{'yards_gained': 12, 'defenders_in_box': 7, 'run_gap': 'C', 'shotgun': True},
]
print("\nRush Yards Over Expected (RYOE):")
print("-" * 70)
print(f"{'Actual':>8} {'Expected':>10} {'RYOE':>8} {'Context'}")
print("-" * 70)
for run in sample_runs:
expected = ryoe_model.predict_yards(run)
ryoe = ryoe_model.calculate_ryoe(run)
context = f"{run['defenders_in_box']} in box, {run['run_gap']} gap"
print(f"{run['yards_gained']:>8} {expected:>10.1f} {ryoe:>+8.2f} {context}")
# Aggregate
ryoe_stats = ryoe_model.analyze_ryoe(sample_runs)
print(f"\nTotal RYOE: {ryoe_stats['total_ryoe']:+.1f}")
print(f"RYOE/carry: {ryoe_stats['ryoe_per_carry']:+.2f}")
8.6 Situational Rushing Analysis
Different situations call for different evaluation criteria.
Short-Yardage Analysis
class SituationalRushingAnalyzer:
"""Analyze rushing performance by situation."""
def __init__(self):
self.success_calc = RushingSuccessCalculator()
def analyze_short_yardage(self, carries: List[Dict]) -> Dict:
"""
Analyze short-yardage rushing (3rd/4th and 2 or less).
Returns conversion rates and efficiency.
"""
short_yardage = [
c for c in carries
if c['down'] >= 3 and c['distance'] <= 2
]
if not short_yardage:
return {'message': 'No short-yardage carries'}
conversions = sum(1 for c in short_yardage
if c['yards_gained'] >= c['distance'])
stuffs = sum(1 for c in short_yardage if c['yards_gained'] <= 0)
return {
'attempts': len(short_yardage),
'conversions': conversions,
'conversion_rate': round(conversions / len(short_yardage) * 100, 1),
'stuffs': stuffs,
'stuff_rate': round(stuffs / len(short_yardage) * 100, 1),
'avg_yards': round(sum(c['yards_gained'] for c in short_yardage) /
len(short_yardage), 2)
}
def analyze_goal_line(self, carries: List[Dict]) -> Dict:
"""Analyze goal-line rushing (inside the 5)."""
goal_line = [c for c in carries if c.get('yard_line', 0) >= 95]
if not goal_line:
return {'message': 'No goal-line carries'}
touchdowns = sum(1 for c in goal_line
if c['yard_line'] + c['yards_gained'] >= 100)
return {
'attempts': len(goal_line),
'touchdowns': touchdowns,
'td_rate': round(touchdowns / len(goal_line) * 100, 1),
'avg_yards': round(sum(c['yards_gained'] for c in goal_line) /
len(goal_line), 2)
}
def analyze_late_game(self, carries: List[Dict]) -> Dict:
"""
Analyze rushing when protecting a lead in 4th quarter.
Looks at ability to sustain drives and run out clock.
"""
late_game = [
c for c in carries
if c.get('quarter', 0) == 4 and c.get('leading', False)
]
if not late_game:
return {'message': 'No late-game carries'}
first_downs = sum(1 for c in late_game
if c['yards_gained'] >= c['distance'])
return {
'carries': len(late_game),
'first_downs': first_downs,
'first_down_rate': round(first_downs / len(late_game) * 100, 1),
'avg_yards': round(sum(c['yards_gained'] for c in late_game) /
len(late_game), 2),
'success_rate': self.success_calc.calculate_success_rate(late_game)['success_rate']
}
def generate_situational_report(self, carries: List[Dict], rb_name: str) -> str:
"""Generate comprehensive situational report."""
short = self.analyze_short_yardage(carries)
goal = self.analyze_goal_line(carries)
late = self.analyze_late_game(carries)
report = f"""
╔══════════════════════════════════════════════════════════════════════════╗
║ SITUATIONAL RUSHING REPORT: {rb_name:<20} ║
╠══════════════════════════════════════════════════════════════════════════╣
║ SHORT YARDAGE (3rd/4th & 2 or less) ║
║ Attempts: {short.get('attempts', 0):>4} Conversions: {short.get('conversions', 0):>4} Rate: {short.get('conversion_rate', 0):>5.1f}% ║
║ Stuffs: {short.get('stuffs', 0):>4} Stuff Rate: {short.get('stuff_rate', 0):>5.1f}% ║
╠══════════════════════════════════════════════════════════════════════════╣
║ GOAL LINE (Inside 5-yard line) ║
║ Attempts: {goal.get('attempts', 0):>4} TDs: {goal.get('touchdowns', 0):>4} TD Rate: {goal.get('td_rate', 0):>5.1f}% ║
╠══════════════════════════════════════════════════════════════════════════╣
║ LATE GAME (4th quarter with lead) ║
║ Carries: {late.get('carries', 0):>4} First Downs: {late.get('first_downs', 0):>4} FD Rate: {late.get('first_down_rate', 0):>5.1f}% ║
╚══════════════════════════════════════════════════════════════════════════╝
"""
return report
8.7 Run Blocking Analysis
While individual blocking grades require film study, we can infer blocking quality from aggregate statistics.
class RunBlockingAnalyzer:
"""Analyze run blocking effectiveness."""
def __init__(self):
pass
def calculate_blocking_metrics(self, team_carries: List[Dict]) -> Dict:
"""
Calculate run blocking metrics from aggregate data.
Metrics:
- Yards Before Contact (team average)
- Stuff Rate (carries for 0 or less)
- Line Yards (blocked yards before contact)
- Opportunity Rate (runs where blocked well)
"""
if not team_carries:
return {}
# Yards before contact
total_ybc = sum(c.get('yards_before_contact', 2) for c in team_carries)
avg_ybc = total_ybc / len(team_carries)
# Stuffs (0 or negative yards)
stuffs = sum(1 for c in team_carries if c.get('yards_gained', 0) <= 0)
stuff_rate = stuffs / len(team_carries) * 100
# Line yards (credit OL for yards before contact, cap at 4)
line_yards = sum(
min(c.get('yards_before_contact', 2), 4)
for c in team_carries
)
avg_line_yards = line_yards / len(team_carries)
# Opportunity rate (runs with 4+ yards before contact)
opportunities = sum(1 for c in team_carries
if c.get('yards_before_contact', 0) >= 4)
opportunity_rate = opportunities / len(team_carries) * 100
# Second level yards (yards between 5-10, credit partially to OL)
second_level = sum(
max(0, min(c.get('yards_gained', 0), 10) -
min(c.get('yards_gained', 0), 4))
for c in team_carries
)
return {
'carries': len(team_carries),
'avg_yards_before_contact': round(avg_ybc, 2),
'stuff_rate': round(stuff_rate, 1),
'avg_line_yards': round(avg_line_yards, 2),
'opportunity_rate': round(opportunity_rate, 1),
'second_level_yards': round(second_level, 1),
'total_yards': sum(c.get('yards_gained', 0) for c in team_carries),
'ypc': round(sum(c.get('yards_gained', 0) for c in team_carries) /
len(team_carries), 2)
}
def compare_units(self, teams_data: Dict[str, List[Dict]]) -> pd.DataFrame:
"""Compare run blocking across teams."""
results = []
for team, carries in teams_data.items():
metrics = self.calculate_blocking_metrics(carries)
results.append({
'team': team,
'carries': metrics['carries'],
'ypc': metrics['ypc'],
'avg_ybc': metrics['avg_yards_before_contact'],
'line_yards': metrics['avg_line_yards'],
'stuff_rate': metrics['stuff_rate'],
'opportunity_rate': metrics['opportunity_rate']
})
df = pd.DataFrame(results)
df['blocking_rank'] = df['avg_ybc'].rank(ascending=False).astype(int)
return df.sort_values('blocking_rank')
8.8 Comprehensive Running Back Evaluation
Combining all metrics for complete evaluation.
class ComprehensiveRBEvaluator:
"""Complete running back evaluation system."""
def __init__(self):
self.success_calc = RushingSuccessCalculator()
self.yac_analyzer = YardsAfterContactAnalyzer()
self.epa_calc = RushingEPACalculator()
self.situational = SituationalRushingAnalyzer()
def evaluate_running_back(self, rb_name: str, carries: List[Dict],
games: int) -> Dict:
"""
Generate comprehensive RB evaluation.
Parameters:
-----------
rb_name : str
carries : list
games : int
Returns:
--------
dict : Complete evaluation
"""
# Volume stats
total_yards = sum(c.get('yards_gained', 0) for c in carries)
total_tds = sum(1 for c in carries
if c.get('yard_line', 0) + c.get('yards_gained', 0) >= 100)
volume = {
'carries': len(carries),
'yards': total_yards,
'touchdowns': total_tds,
'yards_per_game': round(total_yards / games, 1) if games else 0,
'carries_per_game': round(len(carries) / games, 1) if games else 0
}
# Efficiency
ypc = total_yards / len(carries) if carries else 0
success = self.success_calc.calculate_success_rate(carries)
efficiency = {
'ypc': round(ypc, 2),
'success_rate': success['success_rate']
}
# YAC (if data available)
if any('yards_before_contact' in c for c in carries):
yac_metrics = self.yac_analyzer.calculate_yac_metrics(carries)
efficiency['avg_yac'] = yac_metrics['avg_yards_after_contact']
efficiency['broken_tackles'] = yac_metrics['broken_tackles']
# EPA
epa_metrics = self.epa_calc.analyze_rushing_epa(carries)
value = {
'total_epa': epa_metrics['total_epa'],
'epa_per_carry': epa_metrics['epa_per_carry']
}
# Situational
short = self.situational.analyze_short_yardage(carries)
situational = {
'short_yardage_conv': short.get('conversion_rate', 0)
}
# Big plays
explosive = sum(1 for c in carries if c.get('yards_gained', 0) >= 10)
big_plays = {
'explosive_runs': explosive,
'explosive_rate': round(explosive / len(carries) * 100, 1) if carries else 0
}
# Composite score
composite = self._calculate_composite(efficiency, value, situational, big_plays)
return {
'name': rb_name,
'games': games,
'volume': volume,
'efficiency': efficiency,
'value': value,
'situational': situational,
'big_plays': big_plays,
'composite_score': composite
}
def _calculate_composite(self, efficiency: Dict, value: Dict,
situational: Dict, big_plays: Dict) -> float:
"""Calculate composite ranking score."""
score = 50 # Base
# EPA contribution (most important)
score += value.get('epa_per_carry', 0) * 100
# Success rate contribution
score += (efficiency.get('success_rate', 40) - 40) * 0.5
# Explosive play rate
score += big_plays.get('explosive_rate', 10) * 0.3
# Short yardage (if available)
if situational.get('short_yardage_conv', 0) > 50:
score += 5
return round(max(0, min(100, score)), 1)
def compare_backs(self, rb_data: Dict[str, Tuple[List[Dict], int]]) -> pd.DataFrame:
"""Compare multiple running backs."""
results = []
for rb_name, (carries, games) in rb_data.items():
eval_result = self.evaluate_running_back(rb_name, carries, games)
results.append({
'rb': rb_name,
'carries': eval_result['volume']['carries'],
'yards': eval_result['volume']['yards'],
'ypc': eval_result['efficiency']['ypc'],
'success_rate': eval_result['efficiency']['success_rate'],
'epa_per_carry': eval_result['value']['epa_per_carry'],
'explosive_rate': eval_result['big_plays']['explosive_rate'],
'composite': eval_result['composite_score']
})
df = pd.DataFrame(results)
df['rank'] = df['composite'].rank(ascending=False).astype(int)
return df.sort_values('rank')
8.9 Summary
This chapter covered advanced rushing metrics beyond yards per carry:
- Success Rate: Contextual measure of gaining expected yards
- Yards After Contact: Separates back skill from blocking
- EPA: Measures actual value created per carry
- RYOE: Compares actual to expected yards based on context
- Situational Analysis: Short-yardage, goal-line, late-game
- Blocking Metrics: Evaluating offensive line contribution
- Comprehensive Evaluation: Multi-factor back assessment
Key Takeaways
- YPC is heavily influenced by blocking and situation
- Success rate better predicts future performance than YPC
- YAC reveals back skill independent of blocking
- Situational metrics show reliability in critical moments
- EPA captures value that volume stats miss
Exercises
See the accompanying exercises.md file for practice problems.
Further Reading
See further-reading.md for additional resources on rushing analytics.