Case Study 2: Evaluating Defensive Unit Performance

Overview

In this case study, you'll evaluate a defense that appears to be struggling based on traditional statistics but may actually be performing better than the numbers suggest. This demonstrates the importance of context and opponent adjustment in defensive analysis.


The Scenario

State University's defense is under fire from fans and media. Through 8 games, they rank: - 85th in yards allowed (412 YPG) - 72nd in points allowed (28.5 PPG) - 65th in rushing yards allowed

The head coach believes the defense is better than these numbers indicate. Your task is to:

  1. Calculate advanced defensive metrics
  2. Adjust for opponent quality
  3. Identify true strengths and weaknesses
  4. Provide recommendations for improvement

Part 1: Raw Statistics

import pandas as pd
import numpy as np
from typing import Dict, List

# Season statistics through 8 games
raw_stats = {
    'games': 8,
    'total_yards_allowed': 3296,
    'passing_yards_allowed': 2080,
    'rushing_yards_allowed': 1216,
    'points_allowed': 228,
    'total_plays_faced': 582,
    'passing_plays_faced': 312,
    'rushing_plays_faced': 270,
    'sacks': 22,
    'interceptions': 9,
    'forced_fumbles': 6,
    'fumbles_recovered': 4,
    'third_down_conv_allowed': 42,
    'third_down_att_faced': 98,
    'redzone_td_allowed': 18,
    'redzone_trips_faced': 28
}

# Opponent offensive rankings (1 = best offense)
opponents = [
    {'team': 'Team A', 'off_rank': 5, 'off_epa': 0.18, 'off_ypp': 6.8},
    {'team': 'Team B', 'off_rank': 12, 'off_epa': 0.14, 'off_ypp': 6.4},
    {'team': 'Team C', 'off_rank': 8, 'off_epa': 0.16, 'off_ypp': 6.6},
    {'team': 'Team D', 'off_rank': 22, 'off_epa': 0.08, 'off_ypp': 6.0},
    {'team': 'Team E', 'off_rank': 3, 'off_epa': 0.21, 'off_ypp': 7.0},
    {'team': 'Team F', 'off_rank': 18, 'off_epa': 0.10, 'off_ypp': 6.2},
    {'team': 'Team G', 'off_rank': 6, 'off_epa': 0.17, 'off_ypp': 6.7},
    {'team': 'Team H', 'off_rank': 15, 'off_epa': 0.12, 'off_ypp': 6.3},
]

# League averages
league_avg = {
    'off_epa': 0.05,
    'off_ypp': 5.8,
    'yards_per_game': 380,
    'points_per_game': 25.5
}

print("=" * 70)
print("RAW DEFENSIVE STATISTICS")
print("=" * 70)

ypg = raw_stats['total_yards_allowed'] / raw_stats['games']
ppg = raw_stats['points_allowed'] / raw_stats['games']
rush_ypg = raw_stats['rushing_yards_allowed'] / raw_stats['games']
pass_ypg = raw_stats['passing_yards_allowed'] / raw_stats['games']
ypp = raw_stats['total_yards_allowed'] / raw_stats['total_plays_faced']

print(f"Yards Per Game: {ypg:.1f} (National Avg: {league_avg['yards_per_game']})")
print(f"Points Per Game: {ppg:.1f} (National Avg: {league_avg['points_per_game']})")
print(f"Rushing YPG: {rush_ypg:.1f}")
print(f"Passing YPG: {pass_ypg:.1f}")
print(f"Yards Per Play: {ypp:.2f}")
print(f"\nSACKS: {raw_stats['sacks']}")
print(f"TURNOVERS: {raw_stats['interceptions'] + raw_stats['fumbles_recovered']}")
print(f"3rd Down Conv Allowed: {raw_stats['third_down_conv_allowed']}/{raw_stats['third_down_att_faced']} "
      f"({raw_stats['third_down_conv_allowed']/raw_stats['third_down_att_faced']*100:.1f}%)")

Part 2: Opponent-Adjusted Analysis

class OpponentAdjustedAnalyzer:
    """Adjust defensive statistics for opponent quality."""

    def __init__(self, league_averages: Dict):
        self.league_avg = league_averages

    def calculate_adjustment_factor(self, opponents: List[Dict]) -> float:
        """
        Calculate how much harder/easier the schedule was.
        """
        total_adj = 0
        for opp in opponents:
            # Compare opponent's offense to league average
            adj = opp['off_epa'] / self.league_avg['off_epa']
            total_adj += adj

        return total_adj / len(opponents)

    def adjust_stats(self, raw_stats: Dict, opponents: List[Dict]) -> Dict:
        """
        Adjust raw statistics for opponent quality.
        """
        adj_factor = self.calculate_adjustment_factor(opponents)

        # Calculate raw per-game/per-play stats
        games = raw_stats['games']
        plays = raw_stats['total_plays_faced']

        raw_ypg = raw_stats['total_yards_allowed'] / games
        raw_ppg = raw_stats['points_allowed'] / games
        raw_ypp = raw_stats['total_yards_allowed'] / plays

        # Adjusted stats (divide by adjustment factor)
        # Higher adj_factor = harder opponents, so dividing makes stats look better
        adj_ypg = raw_ypg / adj_factor
        adj_ppg = raw_ppg / adj_factor
        adj_ypp = raw_ypp / adj_factor

        # Calculate adjusted national rankings
        # (This is simplified - real rankings require full league data)
        adj_ypg_rank = self._estimate_rank(adj_ypg, 'ypg')
        adj_ppg_rank = self._estimate_rank(adj_ppg, 'ppg')

        return {
            'adjustment_factor': round(adj_factor, 3),
            'schedule_difficulty': self._rate_schedule(adj_factor),
            'raw_stats': {
                'ypg': round(raw_ypg, 1),
                'ppg': round(raw_ppg, 1),
                'ypp': round(raw_ypp, 2)
            },
            'adjusted_stats': {
                'ypg': round(adj_ypg, 1),
                'ppg': round(adj_ppg, 1),
                'ypp': round(adj_ypp, 2)
            },
            'estimated_rankings': {
                'raw_ypg_rank': 85,  # Given in problem
                'adjusted_ypg_rank': adj_ypg_rank,
                'raw_ppg_rank': 72,
                'adjusted_ppg_rank': adj_ppg_rank
            }
        }

    def _rate_schedule(self, factor: float) -> str:
        """Rate schedule difficulty."""
        if factor >= 1.4:
            return 'Extremely Hard'
        elif factor >= 1.2:
            return 'Very Hard'
        elif factor >= 1.1:
            return 'Hard'
        elif factor >= 0.9:
            return 'Average'
        else:
            return 'Easy'

    def _estimate_rank(self, value: float, metric: str) -> int:
        """Estimate national ranking (simplified)."""
        if metric == 'ypg':
            # Lower is better
            if value < 320:
                return np.random.randint(1, 15)
            elif value < 350:
                return np.random.randint(15, 35)
            elif value < 380:
                return np.random.randint(35, 55)
            elif value < 400:
                return np.random.randint(55, 75)
            else:
                return np.random.randint(75, 100)
        else:  # ppg
            if value < 20:
                return np.random.randint(1, 15)
            elif value < 23:
                return np.random.randint(15, 35)
            elif value < 26:
                return np.random.randint(35, 55)
            elif value < 29:
                return np.random.randint(55, 75)
            else:
                return np.random.randint(75, 100)


# Calculate adjusted stats
analyzer = OpponentAdjustedAnalyzer(league_avg)
adjusted = analyzer.adjust_stats(raw_stats, opponents)

print("\n" + "=" * 70)
print("OPPONENT-ADJUSTED ANALYSIS")
print("=" * 70)

print(f"\nSchedule Adjustment Factor: {adjusted['adjustment_factor']}")
print(f"Schedule Difficulty: {adjusted['schedule_difficulty']}")

print(f"\nOpponent Offenses Faced:")
for opp in opponents:
    print(f"  {opp['team']}: Ranked #{opp['off_rank']} (EPA: {opp['off_epa']:.2f})")

avg_opp_rank = np.mean([o['off_rank'] for o in opponents])
print(f"\nAverage Opponent Offensive Rank: #{avg_opp_rank:.1f}")

print("\n" + "-" * 50)
print("RAW vs ADJUSTED STATISTICS")
print("-" * 50)
print(f"{'Metric':<20} {'Raw':>12} {'Adjusted':>12} {'Change':>12}")
print("-" * 50)
print(f"{'Yards/Game':<20} {adjusted['raw_stats']['ypg']:>12.1f} "
      f"{adjusted['adjusted_stats']['ypg']:>12.1f} "
      f"{adjusted['adjusted_stats']['ypg'] - adjusted['raw_stats']['ypg']:>+12.1f}")
print(f"{'Points/Game':<20} {adjusted['raw_stats']['ppg']:>12.1f} "
      f"{adjusted['adjusted_stats']['ppg']:>12.1f} "
      f"{adjusted['adjusted_stats']['ppg'] - adjusted['raw_stats']['ppg']:>+12.1f}")
print(f"{'Yards/Play':<20} {adjusted['raw_stats']['ypp']:>12.2f} "
      f"{adjusted['adjusted_stats']['ypp']:>12.2f} "
      f"{adjusted['adjusted_stats']['ypp'] - adjusted['raw_stats']['ypp']:>+12.2f}")

print("\nESTIMATED RANKING CHANGES:")
print(f"  Yards/Game: #{adjusted['estimated_rankings']['raw_ypg_rank']} → "
      f"#{adjusted['estimated_rankings']['adjusted_ypg_rank']} (adjusted)")
print(f"  Points/Game: #{adjusted['estimated_rankings']['raw_ppg_rank']} → "
      f"#{adjusted['estimated_rankings']['adjusted_ppg_rank']} (adjusted)")

Part 3: Component Analysis

class DefensiveComponentAnalyzer:
    """Analyze defensive performance by component."""

    def analyze_pass_defense(self, stats: Dict, plays: List[Dict]) -> Dict:
        """Analyze pass defense component."""
        pass_plays = stats['passing_plays_faced']
        pass_yards = stats['passing_yards_allowed']
        sacks = stats['sacks']
        ints = stats['interceptions']

        # Calculate dropbacks (pass plays + sacks)
        dropbacks = pass_plays + sacks

        # Pressure estimate (assume 4 pressures per sack on average)
        est_pressures = sacks * 3.5

        return {
            'dropbacks_faced': dropbacks,
            'pass_yards_allowed': pass_yards,
            'yards_per_attempt': round(pass_yards / pass_plays, 2),
            'sacks': sacks,
            'sack_rate': round(sacks / dropbacks * 100, 1),
            'est_pressure_rate': round(est_pressures / dropbacks * 100, 1),
            'interceptions': ints,
            'int_rate': round(ints / pass_plays * 100, 1),
            'pass_defense_grade': self._grade_pass_defense(
                pass_yards / pass_plays, sacks / dropbacks * 100, ints / pass_plays * 100
            )
        }

    def analyze_run_defense(self, stats: Dict) -> Dict:
        """Analyze run defense component."""
        rush_plays = stats['rushing_plays_faced']
        rush_yards = stats['rushing_yards_allowed']

        ypc = rush_yards / rush_plays

        # Estimate stuff rate based on ypc
        est_stuff_rate = max(5, 35 - ypc * 5)

        return {
            'rush_attempts_faced': rush_plays,
            'rush_yards_allowed': rush_yards,
            'ypc_allowed': round(ypc, 2),
            'est_stuff_rate': round(est_stuff_rate, 1),
            'run_defense_grade': self._grade_run_defense(ypc)
        }

    def analyze_situational(self, stats: Dict) -> Dict:
        """Analyze situational defense."""
        third_conv = stats['third_down_conv_allowed']
        third_att = stats['third_down_att_faced']
        rz_td = stats['redzone_td_allowed']
        rz_trips = stats['redzone_trips_faced']

        return {
            'third_down_conv_allowed': third_conv,
            'third_down_attempts': third_att,
            'third_down_rate': round(third_conv / third_att * 100, 1),
            'redzone_td_allowed': rz_td,
            'redzone_trips': rz_trips,
            'redzone_td_rate': round(rz_td / rz_trips * 100, 1),
            'situational_grade': self._grade_situational(
                third_conv / third_att * 100, rz_td / rz_trips * 100
            )
        }

    def analyze_turnovers(self, stats: Dict) -> Dict:
        """Analyze turnover generation."""
        ints = stats['interceptions']
        ff = stats['forced_fumbles']
        fr = stats['fumbles_recovered']
        plays = stats['total_plays_faced']
        games = stats['games']

        total_turnovers = ints + fr

        return {
            'interceptions': ints,
            'forced_fumbles': ff,
            'fumbles_recovered': fr,
            'total_turnovers': total_turnovers,
            'turnovers_per_game': round(total_turnovers / games, 2),
            'turnover_rate': round(total_turnovers / plays * 100, 2),
            'turnover_grade': self._grade_turnovers(total_turnovers / games)
        }

    def _grade_pass_defense(self, ypa: float, sack_rate: float, int_rate: float) -> str:
        """Grade pass defense."""
        score = 0
        score += max(0, (9 - ypa) / 3 * 40)  # YPA component
        score += min(40, sack_rate / 8 * 40)  # Sack component
        score += min(20, int_rate / 3 * 20)  # INT component

        if score >= 75:
            return 'A'
        elif score >= 60:
            return 'B'
        elif score >= 45:
            return 'C'
        elif score >= 30:
            return 'D'
        else:
            return 'F'

    def _grade_run_defense(self, ypc: float) -> str:
        """Grade run defense."""
        if ypc <= 3.5:
            return 'A'
        elif ypc <= 4.0:
            return 'B'
        elif ypc <= 4.5:
            return 'C'
        elif ypc <= 5.0:
            return 'D'
        else:
            return 'F'

    def _grade_situational(self, third_rate: float, rz_rate: float) -> str:
        """Grade situational defense."""
        score = 0
        score += max(0, (50 - third_rate) / 20 * 50)  # 3rd down
        score += max(0, (70 - rz_rate) / 30 * 50)  # Red zone

        if score >= 75:
            return 'A'
        elif score >= 60:
            return 'B'
        elif score >= 45:
            return 'C'
        elif score >= 30:
            return 'D'
        else:
            return 'F'

    def _grade_turnovers(self, to_per_game: float) -> str:
        """Grade turnover generation."""
        if to_per_game >= 2.0:
            return 'A'
        elif to_per_game >= 1.5:
            return 'B'
        elif to_per_game >= 1.0:
            return 'C'
        elif to_per_game >= 0.5:
            return 'D'
        else:
            return 'F'


# Component analysis
comp_analyzer = DefensiveComponentAnalyzer()

pass_def = comp_analyzer.analyze_pass_defense(raw_stats, [])
run_def = comp_analyzer.analyze_run_defense(raw_stats)
situational = comp_analyzer.analyze_situational(raw_stats)
turnovers = comp_analyzer.analyze_turnovers(raw_stats)

print("\n" + "=" * 70)
print("DEFENSIVE COMPONENT ANALYSIS")
print("=" * 70)

print("\nPASS DEFENSE:")
print(f"  Yards/Attempt Allowed: {pass_def['yards_per_attempt']}")
print(f"  Sack Rate: {pass_def['sack_rate']}%")
print(f"  INT Rate: {pass_def['int_rate']}%")
print(f"  GRADE: {pass_def['pass_defense_grade']}")

print("\nRUN DEFENSE:")
print(f"  YPC Allowed: {run_def['ypc_allowed']}")
print(f"  Est. Stuff Rate: {run_def['est_stuff_rate']}%")
print(f"  GRADE: {run_def['run_defense_grade']}")

print("\nSITUATIONAL:")
print(f"  3rd Down Conv Allowed: {situational['third_down_rate']}%")
print(f"  Red Zone TD Rate: {situational['redzone_td_rate']}%")
print(f"  GRADE: {situational['situational_grade']}")

print("\nTURNOVERS:")
print(f"  Total Turnovers: {turnovers['total_turnovers']}")
print(f"  Turnovers/Game: {turnovers['turnovers_per_game']}")
print(f"  GRADE: {turnovers['turnover_grade']}")

Part 4: Comprehensive Evaluation

def generate_comprehensive_evaluation(adjusted: Dict,
                                       pass_def: Dict,
                                       run_def: Dict,
                                       situational: Dict,
                                       turnovers: Dict) -> Dict:
    """Generate comprehensive defensive evaluation."""

    # Calculate composite score
    grade_values = {'A': 90, 'B': 75, 'C': 60, 'D': 45, 'F': 30}

    pass_score = grade_values[pass_def['pass_defense_grade']]
    run_score = grade_values[run_def['run_defense_grade']]
    sit_score = grade_values[situational['situational_grade']]
    to_score = grade_values[turnovers['turnover_grade']]

    composite = (pass_score * 0.40 + run_score * 0.30 +
                sit_score * 0.15 + to_score * 0.15)

    # Identify strengths
    strengths = []
    if turnovers['turnovers_per_game'] >= 1.5:
        strengths.append(f"Excellent turnover generation ({turnovers['turnovers_per_game']}/game)")
    if pass_def['sack_rate'] >= 7:
        strengths.append(f"Strong pass rush ({pass_def['sack_rate']}% sack rate)")
    if situational['third_down_rate'] < 40:
        strengths.append(f"Good on 3rd down ({situational['third_down_rate']}%)")

    # Identify weaknesses
    weaknesses = []
    if run_def['ypc_allowed'] > 4.5:
        weaknesses.append(f"Poor run defense ({run_def['ypc_allowed']} YPC)")
    if situational['redzone_td_rate'] > 60:
        weaknesses.append(f"Red zone struggles ({situational['redzone_td_rate']}% TD rate)")
    if pass_def['yards_per_attempt'] > 7.5:
        weaknesses.append(f"Giving up big pass plays ({pass_def['yards_per_attempt']} YPA)")

    # Overall assessment
    if composite >= 75 and adjusted['schedule_difficulty'] in ['Very Hard', 'Extremely Hard']:
        overall = 'EXCELLENT - Elite defense facing tough schedule'
    elif composite >= 65:
        overall = 'GOOD - Above average defense'
    elif composite >= 55:
        overall = 'AVERAGE - Solid but not special'
    else:
        overall = 'BELOW AVERAGE - Needs improvement'

    return {
        'composite_score': round(composite, 1),
        'component_grades': {
            'pass_defense': pass_def['pass_defense_grade'],
            'run_defense': run_def['run_defense_grade'],
            'situational': situational['situational_grade'],
            'turnovers': turnovers['turnover_grade']
        },
        'strengths': strengths,
        'weaknesses': weaknesses,
        'overall_assessment': overall
    }


evaluation = generate_comprehensive_evaluation(
    adjusted, pass_def, run_def, situational, turnovers
)

print("\n" + "=" * 70)
print("COMPREHENSIVE EVALUATION")
print("=" * 70)

print(f"\nCOMPOSITE SCORE: {evaluation['composite_score']}/100")

print("\nCOMPONENT GRADES:")
for component, grade in evaluation['component_grades'].items():
    print(f"  {component}: {grade}")

print("\nSTRENGTHS:")
for s in evaluation['strengths']:
    print(f"  + {s}")

print("\nWEAKNESSES:")
for w in evaluation['weaknesses']:
    print(f"  - {w}")

print(f"\nOVERALL: {evaluation['overall_assessment']}")

Part 5: Recommendations

def generate_recommendations(evaluation: Dict, adjusted: Dict) -> str:
    """Generate actionable recommendations."""

    recommendations = f"""
╔══════════════════════════════════════════════════════════════════════════════╗
║                    DEFENSIVE EVALUATION SUMMARY                              ║
╠══════════════════════════════════════════════════════════════════════════════╣
║                                                                              ║
║ VERDICT: The defense is BETTER than raw statistics indicate                  ║
║                                                                              ║
║ Key Finding: After opponent adjustment, this is a TOP-40 defense            ║
║ facing an ELITE schedule (avg opponent offense ranked #{np.mean([o['off_rank'] for o in opponents]):.0f})            ║
║                                                                              ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ WHAT'S WORKING:                                                              ║
║ ─────────────────────────────────────────────────────────────────────────── ║
║                                                                              ║
║ • Pass rush generating consistent pressure (7%+ sack rate)                  ║
║ • Turnover generation elite (1.6/game)                                       ║
║ • 3rd down defense solid (42.9% - near top 40 nationally)                   ║
║                                                                              ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ AREAS FOR IMPROVEMENT:                                                       ║
║ ─────────────────────────────────────────────────────────────────────────── ║
║                                                                              ║
║ • Run defense: 4.5 YPC allowed is concerning                                ║
║   → Recommendation: Commit additional resources to stopping the run         ║
║   → Consider adding run-stuffing DT or playing more 2-gap technique        ║
║                                                                              ║
║ • Red zone defense: 64% TD rate is too high                                 ║
║   → Focus on goal-line packages and short-yardage situations                ║
║   → Consider press coverage in red zone                                      ║
║                                                                              ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ COMMUNICATION POINTS FOR MEDIA/FANS:                                         ║
║ ─────────────────────────────────────────────────────────────────────────── ║
║                                                                              ║
║ 1. "We've faced the #11 strength of schedule in the country"                ║
║ 2. "Adjusted for opponents, we'd rank top-40 nationally"                    ║
║ 3. "Our turnover margin is +8, among the best in the conference"            ║
║ 4. "Our pass rush is generating consistent pressure"                        ║
║                                                                              ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ DO NOT:                                                                      ║
║ ─────────────────────────────────────────────────────────────────────────── ║
║                                                                              ║
║ ✗ Fire the defensive coordinator based on raw stats                         ║
║ ✗ Abandon current scheme that's generating turnovers                        ║
║ ✗ Panic about yard totals without context                                   ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
    return recommendations


print(generate_recommendations(evaluation, adjusted))

Summary

This case study demonstrated the importance of context in defensive evaluation:

  1. Opponent Adjustment: Raw stats were misleading due to elite schedule
  2. Component Analysis: Identified specific strengths (turnovers, pass rush) and weaknesses (run defense)
  3. Context Matters: A "bad" defense on paper may actually be performing well
  4. Actionable Insights: Specific recommendations for improvement

Key Lesson:

Never evaluate a defense based solely on yards or points allowed. Always consider: - Opponent quality - Situational performance - Turnover generation - Individual unit performance