4 min read

> "Statistics are like a bikini. What they reveal is suggestive, but what they conceal is vital." — Aaron Levenstein

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:

  1. Calculate and interpret fundamental offensive, defensive, and special teams statistics
  2. Understand the strengths and limitations of traditional metrics
  3. Aggregate statistics at the play, game, and season levels
  4. Read and analyze a box score comprehensively
  5. Compare players and teams using appropriate per-unit metrics
  6. 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:

  1. Universal Language: Everyone from coaches to fans understands them
  2. Data Availability: Available for historical comparisons back decades
  3. Building Blocks: Advanced metrics are built upon these foundations
  4. 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:

  1. Counting statistics capture raw production but lack context
  2. Rate statistics provide better efficiency comparisons
  3. Per-game metrics normalize for opportunity differences
  4. Box scores present game information in standardized format
  5. Era adjustments enable fair historical comparisons
  6. 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

  1. Carroll, B., Palmer, P., & Thorn, J. (1988). The Hidden Game of Football. Warner Books.
  2. ESPN Research. (2023). "FBS Statistical Glossary."
  3. Pro Football Reference. "Glossary of Terms." https://www.pro-football-reference.com/about/glossary.htm
  4. CollegeFootballData.com API Documentation.