> "Statistics are like a bikini. What they reveal is suggestive, but what they conceal is vital." — Aaron Levenstein
In This Chapter
- Learning Objectives
- 6.1 The Foundation: Counting Statistics
- 6.2 Offensive Statistics
- 6.3 Defensive Statistics
- 6.4 Special Teams Statistics
- 6.5 Per-Game and Per-Attempt Metrics
- 6.6 Reading a Box Score
- 6.7 Historical Context and Comparisons
- 6.8 Limitations of Traditional Statistics
- 6.9 Building a Complete Statistics Calculator
- Chapter Summary
- Key Terms
- References
Chapter 6: Traditional Football Statistics
"Statistics are like a bikini. What they reveal is suggestive, but what they conceal is vital." — Aaron Levenstein
Before diving into advanced metrics, every football analyst must master the traditional statistics that form the foundation of the sport's quantitative analysis. These metrics—developed over decades of football history—remain essential for understanding performance, communicating with stakeholders, and building more sophisticated measurements.
Learning Objectives
By the end of this chapter, you will be able to:
- Calculate and interpret fundamental offensive, defensive, and special teams statistics
- Understand the strengths and limitations of traditional metrics
- Aggregate statistics at the play, game, and season levels
- Read and analyze a box score comprehensively
- Compare players and teams using appropriate per-unit metrics
- Build functions for calculating standard football statistics in Python
6.1 The Foundation: Counting Statistics
Why Traditional Stats Still Matter
Despite the analytics revolution, traditional statistics remain crucial because:
- Universal Language: Everyone from coaches to fans understands them
- Data Availability: Available for historical comparisons back decades
- Building Blocks: Advanced metrics are built upon these foundations
- Regulatory Importance: Used in official records, awards, and contracts
The Core Counting Statistics
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple
# Sample play-by-play data structure
play_columns = [
'game_id', 'play_id', 'offense', 'defense',
'play_type', 'yards_gained', 'is_touchdown',
'is_first_down', 'down', 'distance', 'quarter'
]
class CountingStats:
"""Calculate basic counting statistics from play-by-play data."""
@staticmethod
def total_yards(plays: pd.DataFrame, team: str) -> int:
"""Total yards gained by a team."""
return plays[plays['offense'] == team]['yards_gained'].sum()
@staticmethod
def rushing_yards(plays: pd.DataFrame, team: str) -> int:
"""Total rushing yards."""
mask = (plays['offense'] == team) & (plays['play_type'] == 'run')
return plays.loc[mask, 'yards_gained'].sum()
@staticmethod
def passing_yards(plays: pd.DataFrame, team: str) -> int:
"""Total passing yards."""
mask = (plays['offense'] == team) & (plays['play_type'] == 'pass')
return plays.loc[mask, 'yards_gained'].sum()
@staticmethod
def touchdowns(plays: pd.DataFrame, team: str) -> int:
"""Total offensive touchdowns."""
mask = (plays['offense'] == team) & (plays['is_touchdown'] == 1)
return len(plays[mask])
@staticmethod
def first_downs(plays: pd.DataFrame, team: str) -> int:
"""Total first downs gained."""
mask = (plays['offense'] == team) & (plays['is_first_down'] == 1)
return len(plays[mask])
@staticmethod
def turnovers(plays: pd.DataFrame, team: str) -> int:
"""Total turnovers committed."""
mask = (plays['offense'] == team) & (plays['is_turnover'] == 1)
return len(plays[mask])
Understanding Each Core Statistic
| Statistic | What It Measures | Limitations |
|---|---|---|
| Total Yards | Overall offensive output | Doesn't account for context or efficiency |
| Rushing Yards | Ground game production | Skewed by scheme and game situation |
| Passing Yards | Aerial attack production | Inflated in trailing situations |
| Touchdowns | Scoring production | Ignores field goals, red zone efficiency |
| First Downs | Sustained offensive drives | Doesn't measure how efficiently obtained |
| Turnovers | Ball security/creation | Luck factor; not fully in team's control |
6.2 Offensive Statistics
Passing Statistics
The passing game is measured through multiple interconnected metrics:
class PassingStats:
"""Calculate passing statistics."""
def __init__(self, plays: pd.DataFrame):
self.plays = plays
def calculate_for_team(self, team: str, player: str = None) -> Dict:
"""
Calculate comprehensive passing statistics.
Parameters
----------
team : str
Team name
player : str, optional
Specific player to filter
Returns
-------
dict : Passing statistics dictionary
"""
mask = (self.plays['offense'] == team) & \
(self.plays['play_type'] == 'pass')
if player:
mask = mask & (self.plays['passer'] == player)
pass_plays = self.plays[mask]
# Attempts and completions
attempts = len(pass_plays)
completions = pass_plays['is_complete'].sum()
yards = pass_plays['yards_gained'].sum()
# Touchdowns and interceptions
touchdowns = pass_plays['is_touchdown'].sum()
interceptions = pass_plays['is_interception'].sum()
# Sacks (if available)
sacks = len(pass_plays[pass_plays['is_sack'] == 1]) if 'is_sack' in pass_plays.columns else 0
# Calculate rates
comp_pct = (completions / attempts * 100) if attempts > 0 else 0
yards_per_attempt = yards / attempts if attempts > 0 else 0
td_pct = (touchdowns / attempts * 100) if attempts > 0 else 0
int_pct = (interceptions / attempts * 100) if attempts > 0 else 0
# Passer rating (NFL formula)
passer_rating = self._calculate_passer_rating(
comp_pct, yards_per_attempt * 100, td_pct, int_pct
)
return {
'attempts': attempts,
'completions': completions,
'yards': yards,
'touchdowns': touchdowns,
'interceptions': interceptions,
'sacks': sacks,
'completion_pct': round(comp_pct, 1),
'yards_per_attempt': round(yards_per_attempt, 2),
'td_pct': round(td_pct, 1),
'int_pct': round(int_pct, 1),
'passer_rating': round(passer_rating, 1)
}
def _calculate_passer_rating(self, comp_pct: float, ypa: float,
td_pct: float, int_pct: float) -> float:
"""
Calculate NFL passer rating.
The formula caps each component between 0 and 2.375.
"""
# Component a: Completion percentage
a = ((comp_pct - 30) / 20)
a = max(0, min(a, 2.375))
# Component b: Yards per attempt
b = ((ypa - 3) / 4)
b = max(0, min(b, 2.375))
# Component c: Touchdown percentage
c = (td_pct / 5)
c = max(0, min(c, 2.375))
# Component d: Interception percentage (inverted)
d = 2.375 - (int_pct / 4)
d = max(0, min(d, 2.375))
rating = ((a + b + c + d) / 6) * 100
return rating
# Example usage
"""
passer_stats = PassingStats(plays_df)
qb_stats = passer_stats.calculate_for_team('Alabama', 'Bryce Young')
print(f"Completion %: {qb_stats['completion_pct']}%")
print(f"Passer Rating: {qb_stats['passer_rating']}")
"""
Key Passing Metrics Explained
| Metric | Formula | Elite Level (FBS) | Average (FBS) |
|---|---|---|---|
| Completion % | Completions / Attempts × 100 | >68% | ~62% |
| Yards/Attempt | Passing Yards / Attempts | >9.0 | ~7.5 |
| TD% | Pass TDs / Attempts × 100 | >7% | ~4.5% |
| INT% | Interceptions / Attempts × 100 | <1.5% | ~2.5% |
| Passer Rating | NFL composite formula | >150 | ~125 |
Rushing Statistics
class RushingStats:
"""Calculate rushing statistics."""
def __init__(self, plays: pd.DataFrame):
self.plays = plays
def calculate_for_team(self, team: str, player: str = None) -> Dict:
"""Calculate comprehensive rushing statistics."""
mask = (self.plays['offense'] == team) & \
(self.plays['play_type'] == 'run')
if player:
mask = mask & (self.plays['rusher'] == player)
rush_plays = self.plays[mask]
carries = len(rush_plays)
yards = rush_plays['yards_gained'].sum()
touchdowns = rush_plays['is_touchdown'].sum()
# Derived stats
ypc = yards / carries if carries > 0 else 0
long_run = rush_plays['yards_gained'].max() if carries > 0 else 0
# Runs of 10+ yards
explosive_runs = (rush_plays['yards_gained'] >= 10).sum()
negative_runs = (rush_plays['yards_gained'] < 0).sum()
return {
'carries': carries,
'yards': yards,
'touchdowns': touchdowns,
'yards_per_carry': round(ypc, 2),
'long': long_run,
'explosive_runs': explosive_runs,
'negative_runs': negative_runs,
'explosive_pct': round(explosive_runs / carries * 100, 1) if carries > 0 else 0
}
Receiving Statistics
class ReceivingStats:
"""Calculate receiving statistics."""
def __init__(self, plays: pd.DataFrame):
self.plays = plays
def calculate_for_player(self, team: str, player: str) -> Dict:
"""Calculate receiving statistics for a player."""
mask = (self.plays['offense'] == team) & \
(self.plays['receiver'] == player)
receives = self.plays[mask]
targets = len(receives)
receptions = receives['is_complete'].sum()
yards = receives.loc[receives['is_complete'] == 1, 'yards_gained'].sum()
touchdowns = receives['is_touchdown'].sum()
drops = receives['is_drop'].sum() if 'is_drop' in receives.columns else 0
# Derived stats
catch_rate = (receptions / targets * 100) if targets > 0 else 0
ypr = yards / receptions if receptions > 0 else 0
ypt = yards / targets if targets > 0 else 0
return {
'targets': targets,
'receptions': receptions,
'yards': yards,
'touchdowns': touchdowns,
'drops': drops,
'catch_rate': round(catch_rate, 1),
'yards_per_reception': round(ypr, 2),
'yards_per_target': round(ypt, 2)
}
6.3 Defensive Statistics
Defensive statistics are traditionally more difficult to capture but equally important.
Team Defense Metrics
class DefensiveStats:
"""Calculate defensive statistics."""
def __init__(self, plays: pd.DataFrame):
self.plays = plays
def calculate_for_team(self, team: str) -> Dict:
"""
Calculate team defensive statistics.
Note: Team appears as 'defense' when measuring defensive performance.
"""
mask = self.plays['defense'] == team
def_plays = self.plays[mask]
# Points and yards allowed
total_plays = len(def_plays)
yards_allowed = def_plays['yards_gained'].sum()
tds_allowed = def_plays['is_touchdown'].sum()
first_downs_allowed = def_plays['is_first_down'].sum()
# Turnovers forced
turnovers = def_plays['is_turnover'].sum() if 'is_turnover' in def_plays.columns else 0
interceptions = def_plays['is_interception'].sum() if 'is_interception' in def_plays.columns else 0
fumbles_recovered = turnovers - interceptions
# Sacks
sacks = def_plays['is_sack'].sum() if 'is_sack' in def_plays.columns else 0
# Third down defense
third_downs = def_plays[def_plays['down'] == 3]
third_down_stops = len(third_downs) - third_downs['is_first_down'].sum()
third_down_pct = (third_down_stops / len(third_downs) * 100) if len(third_downs) > 0 else 0
return {
'total_plays_faced': total_plays,
'yards_allowed': yards_allowed,
'yards_per_play_allowed': round(yards_allowed / total_plays, 2) if total_plays > 0 else 0,
'touchdowns_allowed': tds_allowed,
'first_downs_allowed': first_downs_allowed,
'turnovers_forced': turnovers,
'interceptions': interceptions,
'fumbles_recovered': fumbles_recovered,
'sacks': sacks,
'third_down_stop_pct': round(third_down_pct, 1)
}
def calculate_individual(self, team: str, player: str) -> Dict:
"""Calculate individual defensive statistics."""
# Note: Individual defensive stats require charting data
# This is a simplified version
mask = (self.plays['defense'] == team)
def_plays = self.plays[mask]
# These would come from charting data
tackles = 0
tackles_for_loss = 0
sacks = 0
interceptions = 0
pass_breakups = 0
return {
'tackles': tackles,
'tackles_for_loss': tackles_for_loss,
'sacks': sacks,
'interceptions': interceptions,
'pass_breakups': pass_breakups,
'forced_fumbles': 0
}
Key Defensive Metrics
| Metric | What It Measures | Context |
|---|---|---|
| Points Allowed | Overall defensive effectiveness | Game outcome indicator |
| Yards Allowed | Total defensive yardage given up | Volume metric |
| Yards/Play | Efficiency of opposing offense | Better than total yards |
| Third Down % | Ability to get off field | Critical situational metric |
| Turnovers Forced | Ball-hawking ability | High variance, partly luck |
| Sacks | Pass rush effectiveness | Doesn't capture all pressure |
6.4 Special Teams Statistics
Special teams often determine close games but are frequently overlooked.
class SpecialTeamsStats:
"""Calculate special teams statistics."""
def __init__(self, plays: pd.DataFrame):
self.plays = plays
def kicking_stats(self, team: str) -> Dict:
"""Calculate kicking statistics."""
# Field goals
fg_plays = self.plays[
(self.plays['offense'] == team) &
(self.plays['play_type'] == 'field_goal')
]
fg_attempts = len(fg_plays)
fg_made = fg_plays['is_made'].sum() if 'is_made' in fg_plays.columns else 0
fg_pct = (fg_made / fg_attempts * 100) if fg_attempts > 0 else 0
# Extra points
xp_plays = self.plays[
(self.plays['offense'] == team) &
(self.plays['play_type'] == 'extra_point')
]
xp_attempts = len(xp_plays)
xp_made = xp_plays['is_made'].sum() if 'is_made' in xp_plays.columns else 0
return {
'fg_attempts': fg_attempts,
'fg_made': fg_made,
'fg_pct': round(fg_pct, 1),
'xp_attempts': xp_attempts,
'xp_made': xp_made
}
def punting_stats(self, team: str) -> Dict:
"""Calculate punting statistics."""
punt_plays = self.plays[
(self.plays['offense'] == team) &
(self.plays['play_type'] == 'punt')
]
punts = len(punt_plays)
gross_yards = punt_plays['kick_distance'].sum() if 'kick_distance' in punt_plays.columns else 0
avg_distance = gross_yards / punts if punts > 0 else 0
inside_20 = punt_plays['is_inside_20'].sum() if 'is_inside_20' in punt_plays.columns else 0
touchbacks = punt_plays['is_touchback'].sum() if 'is_touchback' in punt_plays.columns else 0
return {
'punts': punts,
'gross_yards': gross_yards,
'avg_distance': round(avg_distance, 1),
'inside_20': inside_20,
'touchbacks': touchbacks
}
def return_stats(self, team: str, return_type: str = 'kickoff') -> Dict:
"""Calculate return statistics."""
mask = (self.plays['return_team'] == team) if 'return_team' in self.plays.columns else False
if return_type == 'kickoff':
mask = mask & (self.plays['play_type'] == 'kickoff_return')
else:
mask = mask & (self.plays['play_type'] == 'punt_return')
returns = self.plays[mask]
num_returns = len(returns)
return_yards = returns['return_yards'].sum() if 'return_yards' in returns.columns else 0
avg_return = return_yards / num_returns if num_returns > 0 else 0
return_tds = returns['is_touchdown'].sum() if 'is_touchdown' in returns.columns else 0
return {
'returns': num_returns,
'yards': return_yards,
'avg_return': round(avg_return, 1),
'touchdowns': return_tds
}
6.5 Per-Game and Per-Attempt Metrics
Raw totals can be misleading. Per-unit metrics provide better comparisons.
Why Per-Game Matters
def calculate_per_game_stats(season_totals: Dict, games_played: int) -> Dict:
"""
Convert season totals to per-game averages.
Parameters
----------
season_totals : dict
Dictionary of season statistics
games_played : int
Number of games played
Returns
-------
dict : Per-game statistics
"""
if games_played == 0:
return {k: 0 for k in season_totals}
return {
f"{key}_per_game": round(value / games_played, 2)
for key, value in season_totals.items()
if isinstance(value, (int, float))
}
# Example
season = {
'rushing_yards': 1856,
'rushing_tds': 18,
'carries': 245
}
games = 13
per_game = calculate_per_game_stats(season, games)
# {'rushing_yards_per_game': 142.77, 'rushing_tds_per_game': 1.38, 'carries_per_game': 18.85}
Per-Attempt Efficiency
def calculate_efficiency_metrics(stats: Dict) -> Dict:
"""
Calculate per-attempt efficiency metrics.
Converts counting stats to rate stats.
"""
efficiency = {}
# Passing efficiency
if 'pass_attempts' in stats and stats['pass_attempts'] > 0:
efficiency['yards_per_attempt'] = stats['passing_yards'] / stats['pass_attempts']
efficiency['td_per_attempt'] = stats['passing_tds'] / stats['pass_attempts']
efficiency['completion_pct'] = stats['completions'] / stats['pass_attempts'] * 100
# Rushing efficiency
if 'carries' in stats and stats['carries'] > 0:
efficiency['yards_per_carry'] = stats['rushing_yards'] / stats['carries']
efficiency['td_per_carry'] = stats['rushing_tds'] / stats['carries']
# Receiving efficiency
if 'targets' in stats and stats['targets'] > 0:
efficiency['catch_rate'] = stats['receptions'] / stats['targets'] * 100
efficiency['yards_per_target'] = stats['receiving_yards'] / stats['targets']
return {k: round(v, 2) for k, v in efficiency.items()}
6.6 Reading a Box Score
The box score is the standard format for game statistics. Understanding how to read and analyze one is essential.
Anatomy of a Box Score
class BoxScore:
"""Parse and analyze a football box score."""
def __init__(self, game_data: Dict):
self.game_data = game_data
self.home_team = game_data['home_team']
self.away_team = game_data['away_team']
def display_summary(self) -> str:
"""Generate box score summary."""
home = self.game_data['home_stats']
away = self.game_data['away_stats']
summary = f"""
{'='*60}
{self.away_team} at {self.home_team}
{'='*60}
SCORE BY QUARTER
{'-'*40}
{self.away_team:15s} {away.get('q1',0):3d} {away.get('q2',0):3d} {away.get('q3',0):3d} {away.get('q4',0):3d} - {away.get('total',0):3d}
{self.home_team:15s} {home.get('q1',0):3d} {home.get('q2',0):3d} {home.get('q3',0):3d} {home.get('q4',0):3d} - {home.get('total',0):3d}
TEAM STATISTICS
{'-'*40}
{'Statistic':25s} {'Away':>8s} {'Home':>8s}
{'-'*40}
{'First Downs':25s} {away.get('first_downs',0):8d} {home.get('first_downs',0):8d}
{'Total Yards':25s} {away.get('total_yards',0):8d} {home.get('total_yards',0):8d}
{'Rushing Yards':25s} {away.get('rushing_yards',0):8d} {home.get('rushing_yards',0):8d}
{'Passing Yards':25s} {away.get('passing_yards',0):8d} {home.get('passing_yards',0):8d}
{'Turnovers':25s} {away.get('turnovers',0):8d} {home.get('turnovers',0):8d}
{'Penalties':25s} {away.get('penalties',0):8d} {home.get('penalties',0):8d}
{'Time of Possession':25s} {away.get('top','0:00'):>8s} {home.get('top','0:00'):>8s}
"""
return summary
def calculate_efficiency_comparison(self) -> pd.DataFrame:
"""Compare team efficiencies."""
home = self.game_data['home_stats']
away = self.game_data['away_stats']
comparison = {
'Metric': [
'Yards per Play',
'Yards per Rush',
'Yards per Pass Attempt',
'Third Down %',
'Red Zone %'
],
self.away_team: [
away.get('total_yards', 0) / max(away.get('total_plays', 1), 1),
away.get('rushing_yards', 0) / max(away.get('rush_attempts', 1), 1),
away.get('passing_yards', 0) / max(away.get('pass_attempts', 1), 1),
away.get('third_down_conv', 0) / max(away.get('third_down_att', 1), 1) * 100,
away.get('red_zone_td', 0) / max(away.get('red_zone_att', 1), 1) * 100
],
self.home_team: [
home.get('total_yards', 0) / max(home.get('total_plays', 1), 1),
home.get('rushing_yards', 0) / max(home.get('rush_attempts', 1), 1),
home.get('passing_yards', 0) / max(home.get('pass_attempts', 1), 1),
home.get('third_down_conv', 0) / max(home.get('third_down_att', 1), 1) * 100,
home.get('red_zone_td', 0) / max(home.get('red_zone_att', 1), 1) * 100
]
}
return pd.DataFrame(comparison)
6.7 Historical Context and Comparisons
Era Adjustment
Statistics must be contextualized within their era:
def era_adjust_statistic(value: float,
stat_type: str,
year: int,
league_averages: Dict[int, Dict]) -> float:
"""
Adjust a statistic relative to league average for that era.
Parameters
----------
value : float
Raw statistic value
stat_type : str
Type of statistic (e.g., 'passing_yards', 'completion_pct')
year : int
Season year
league_averages : dict
{year: {stat_type: average}}
Returns
-------
float : Era-adjusted value (1.0 = league average)
"""
if year not in league_averages:
return value
league_avg = league_averages[year].get(stat_type, 1)
if league_avg == 0:
return value
return value / league_avg
# Example: FBS passing trends
fbs_passing_averages = {
2010: {'passing_yards_per_game': 230, 'completion_pct': 60.2},
2015: {'passing_yards_per_game': 248, 'completion_pct': 61.5},
2020: {'passing_yards_per_game': 265, 'completion_pct': 62.8},
2023: {'passing_yards_per_game': 272, 'completion_pct': 63.4}
}
# A QB with 280 yards/game in 2023 is less impressive than 280 in 2010
# 2023: 280 / 272 = 1.03 (3% above average)
# 2010: 280 / 230 = 1.22 (22% above average)
Positional Comparisons
def calculate_percentile_rank(value: float,
distribution: pd.Series,
higher_is_better: bool = True) -> float:
"""
Calculate percentile rank within a distribution.
Parameters
----------
value : float
Value to rank
distribution : pd.Series
Reference distribution
higher_is_better : bool
Whether higher values are better
Returns
-------
float : Percentile (0-100)
"""
if higher_is_better:
percentile = (distribution < value).mean() * 100
else:
percentile = (distribution > value).mean() * 100
return round(percentile, 1)
# Example: Ranking a RB's yards per carry
"""
all_rb_ypc = pd.Series([4.2, 4.8, 5.1, 4.5, 5.6, 4.9, 5.3, 4.1, 5.0, 4.7])
player_ypc = 5.4
percentile = calculate_percentile_rank(player_ypc, all_rb_ypc)
print(f"This RB is in the {percentile}th percentile for YPC")
"""
6.8 Limitations of Traditional Statistics
Understanding what traditional stats miss is as important as knowing what they measure.
Common Pitfalls
| Limitation | Example | Better Alternative |
|---|---|---|
| Context-blind | 150 yards against #1 defense vs. #130 defense | Opponent-adjusted metrics |
| Situation-ignored | Running up score vs. crucial drive | Leverage-weighted stats |
| Incomplete credit | RB yards don't credit O-line | EPA, yards after contact |
| Outcome bias | Dropped INT not counted | Target-based metrics |
| Volume over efficiency | Yards leaders vs. YPC leaders | Per-attempt rates |
What Traditional Stats Miss
def demonstrate_stat_limitations():
"""Show examples of misleading traditional statistics."""
examples = {
'garbage_time_yards': {
'scenario': 'QB throws for 150 yards when down 35-7 in 4th quarter',
'traditional': 'Adds to season total like any other yards',
'limitation': 'These yards had no impact on game outcome',
'better_metric': 'EPA (Expected Points Added) accounts for game state'
},
'yard_to_go_context': {
'scenario': '3-yard run on 3rd and 2 vs. 3-yard run on 3rd and 10',
'traditional': 'Both count as 3 rushing yards',
'limitation': 'First converts first down, second fails',
'better_metric': 'Success rate measures meeting situation needs'
},
'opponent_quality': {
'scenario': '400 yards against worst defense in FBS',
'traditional': 'Counts same as 400 vs. top defense',
'limitation': 'Ignores difficulty of achievement',
'better_metric': 'Opponent-adjusted metrics'
}
}
return examples
6.9 Building a Complete Statistics Calculator
class FootballStatsCalculator:
"""
Comprehensive football statistics calculator.
Handles play-by-play data to produce team and player statistics.
"""
def __init__(self, plays: pd.DataFrame):
self.plays = plays
self.passing = PassingStats(plays)
self.rushing = RushingStats(plays)
self.receiving = ReceivingStats(plays)
self.defense = DefensiveStats(plays)
self.special_teams = SpecialTeamsStats(plays)
def team_summary(self, team: str) -> Dict:
"""Generate comprehensive team statistics."""
passing = self.passing.calculate_for_team(team)
rushing = self.rushing.calculate_for_team(team)
defense = self.defense.calculate_for_team(team)
return {
'team': team,
'offense': {
'passing': passing,
'rushing': rushing,
'total_yards': passing['yards'] + rushing['yards'],
'touchdowns': passing['touchdowns'] + rushing['touchdowns']
},
'defense': defense
}
def player_summary(self, team: str, player: str, position: str) -> Dict:
"""Generate player statistics based on position."""
if position == 'QB':
return self.passing.calculate_for_team(team, player)
elif position == 'RB':
return self.rushing.calculate_for_team(team, player)
elif position in ['WR', 'TE']:
return self.receiving.calculate_for_player(team, player)
else:
return {}
def leaderboard(self, stat: str, top_n: int = 10) -> pd.DataFrame:
"""
Generate leaderboard for a specific statistic.
Parameters
----------
stat : str
Statistic to rank by (e.g., 'passing_yards', 'rushing_tds')
top_n : int
Number of leaders to return
Returns
-------
pd.DataFrame : Leaderboard
"""
# This would aggregate by player and sort
# Implementation depends on data structure
pass
Chapter Summary
Traditional football statistics form the essential foundation for all football analysis. In this chapter, you learned:
- Counting statistics capture raw production but lack context
- Rate statistics provide better efficiency comparisons
- Per-game metrics normalize for opportunity differences
- Box scores present game information in standardized format
- Era adjustments enable fair historical comparisons
- Limitations of traditional stats point toward advanced metrics
Traditional statistics remain vital for communication and historical comparison, even as advanced metrics provide deeper insights. Master these fundamentals before building upon them.
Key Terms
- Counting Statistic: Raw total (yards, touchdowns, first downs)
- Rate Statistic: Per-attempt or per-game measure (YPC, completion %)
- Passer Rating: Composite measure of passing efficiency
- Box Score: Standard game summary format
- Era Adjustment: Contextualizing statistics within historical norms
References
- Carroll, B., Palmer, P., & Thorn, J. (1988). The Hidden Game of Football. Warner Books.
- ESPN Research. (2023). "FBS Statistical Glossary."
- Pro Football Reference. "Glossary of Terms." https://www.pro-football-reference.com/about/glossary.htm
- CollegeFootballData.com API Documentation.