Case Study 2: Creating Visualizations for Multiple Audiences

Overview

Context: Conference championship game analytics package Challenge: Same underlying data needed for four distinct audiences Deliverables: Broadcast graphics, social media content, coaching materials, and executive presentation Learning Focus: Audience-adaptive visualization design


The Scenario

State University is preparing for the conference championship game against Rival University. The analytics team has been asked to create visualizations for four different audiences using the same underlying performance data:

  1. TV Broadcast: Graphics for the game broadcast
  2. Social Media: Fan engagement content
  3. Coaching Staff: Game preparation materials
  4. Athletic Director: Board presentation on program analytics

The core dataset includes season-long performance metrics for both teams:

matchup_data = {
    'State': {
        'record': '11-1',
        'epa_offense': 0.215,
        'epa_defense': 0.142,
        'success_rate_off': 0.472,
        'success_rate_def': 0.388,
        'explosive_rate': 0.134,
        'turnover_margin': +8,
        'red_zone_td_pct': 0.724,
        'third_down_pct': 0.442,
        'points_per_game': 38.2,
        'points_allowed': 18.5,
        'key_player': 'QB Johnson - 3,412 yds, 32 TD'
    },
    'Rival': {
        'record': '10-2',
        'epa_offense': 0.188,
        'epa_defense': 0.165,
        'success_rate_off': 0.445,
        'success_rate_def': 0.402,
        'explosive_rate': 0.098,
        'turnover_margin': +5,
        'red_zone_td_pct': 0.682,
        'third_down_pct': 0.398,
        'points_per_game': 32.8,
        'points_allowed': 16.2,
        'key_player': 'RB Smith - 1,687 yds, 18 TD'
    }
}

Audience 1: TV Broadcast Graphics

Constraints

  • View time: 5-8 seconds per graphic
  • Distance: Viewed on various screen sizes from 10+ feet
  • Context: Viewers may be casually watching, not studying
  • Format: 1920Γ—1080 HD, must work with lower-third banners

Design Approach

Pre-Game Comparison Graphic

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np

def create_broadcast_comparison():
    """Create broadcast-ready team comparison graphic."""

    fig, ax = plt.subplots(figsize=(16, 9))
    fig.patch.set_facecolor('#1a1a2e')
    ax.set_facecolor('#1a1a2e')

    # Team colors
    state_color = '#CC0033'
    rival_color = '#003366'

    # Title area
    ax.text(0.5, 0.92, 'CHAMPIONSHIP MATCHUP',
            ha='center', va='center', fontsize=36,
            color='white', fontweight='bold',
            transform=ax.transAxes)

    # Team names and records
    ax.text(0.25, 0.78, 'STATE', ha='center', fontsize=32,
            color=state_color, fontweight='bold', transform=ax.transAxes)
    ax.text(0.25, 0.70, '11-1', ha='center', fontsize=24,
            color='white', transform=ax.transAxes)

    ax.text(0.75, 0.78, 'RIVAL', ha='center', fontsize=32,
            color=rival_color, fontweight='bold', transform=ax.transAxes)
    ax.text(0.75, 0.70, '10-2', ha='center', fontsize=24,
            color='white', transform=ax.transAxes)

    # VS badge
    circle = plt.Circle((0.5, 0.74), 0.06, color='gold',
                        transform=ax.transAxes, zorder=10)
    ax.add_patch(circle)
    ax.text(0.5, 0.74, 'VS', ha='center', va='center',
            fontsize=20, color='#1a1a2e', fontweight='bold',
            transform=ax.transAxes, zorder=11)

    # Key stats comparison (only 3 stats for broadcast)
    stats = [
        ('POINTS/GAME', 38.2, 32.8),
        ('TURNOVER MARGIN', '+8', '+5'),
        ('RED ZONE TD%', '72%', '68%')
    ]

    y_positions = [0.50, 0.35, 0.20]

    for (label, state_val, rival_val), y in zip(stats, y_positions):
        # Label
        ax.text(0.5, y + 0.05, label, ha='center', fontsize=16,
               color='#888888', transform=ax.transAxes)

        # Values
        ax.text(0.25, y - 0.02, str(state_val), ha='center', fontsize=28,
               color='white', fontweight='bold', transform=ax.transAxes)
        ax.text(0.75, y - 0.02, str(rival_val), ha='center', fontsize=28,
               color='white', fontweight='bold', transform=ax.transAxes)

        # Highlight winner
        if isinstance(state_val, (int, float)) and state_val > rival_val:
            ax.text(0.25, y - 0.02, str(state_val), ha='center', fontsize=28,
                   color=state_color, fontweight='bold', transform=ax.transAxes)
        elif isinstance(rival_val, (int, float)) and rival_val > state_val:
            ax.text(0.75, y - 0.02, str(rival_val), ha='center', fontsize=28,
                   color=rival_color, fontweight='bold', transform=ax.transAxes)

    # Remove axes
    ax.axis('off')

    return fig

Key Design Decisions: 1. Limited metrics: Only 3 stats vs. original 12β€”viewers can't process more in 5 seconds 2. Large text: Minimum 16pt for labels, 28pt+ for key values 3. High contrast: White text on dark background 4. Visual hierarchy: Team names largest, values prominent, labels subdued 5. Winner highlighting: Color indicates which team leads each stat

In-Game Win Probability Graphic

def create_wp_broadcast(current_wp, time_remaining, score):
    """Create in-game win probability display for broadcast."""

    fig, ax = plt.subplots(figsize=(8, 3))
    fig.patch.set_facecolor('#1a1a2e')
    ax.set_facecolor('#1a1a2e')

    # Simple gauge showing current WP
    wp = current_wp  # e.g., 0.73

    # Background bar
    ax.barh(0, 1.0, height=0.4, color='#333333')

    # State's portion
    ax.barh(0, wp, height=0.4, color='#CC0033')

    # Percentage labels
    ax.text(wp/2, 0, f'{int(wp*100)}%', ha='center', va='center',
           fontsize=24, color='white', fontweight='bold')
    ax.text((1+wp)/2, 0, f'{int((1-wp)*100)}%', ha='center', va='center',
           fontsize=24, color='white', fontweight='bold')

    # Team labels
    ax.text(0.02, 0.55, 'STATE', ha='left', fontsize=14,
           color='#CC0033', fontweight='bold', transform=ax.transAxes)
    ax.text(0.98, 0.55, 'RIVAL', ha='right', fontsize=14,
           color='#003366', fontweight='bold', transform=ax.transAxes)

    # Title
    ax.text(0.5, 0.85, 'WIN PROBABILITY', ha='center', fontsize=12,
           color='#888888', transform=ax.transAxes)

    ax.set_xlim(-0.02, 1.02)
    ax.set_ylim(-0.5, 0.8)
    ax.axis('off')

    return fig

Audience 2: Social Media Content

Constraints

  • View time: 2-3 seconds scroll time
  • Format: Square (1080Γ—1080) for Instagram, 16:9 for Twitter
  • Context: Competing with hundreds of other posts
  • Goal: Engagement (likes, shares, comments)

Design Approach

Comparison Infographic (Instagram)

def create_social_comparison():
    """Create social media friendly comparison graphic."""

    fig, ax = plt.subplots(figsize=(10.8, 10.8))  # Square for Instagram
    fig.patch.set_facecolor('#ffffff')
    ax.set_facecolor('#ffffff')

    # Bold header
    ax.text(0.5, 0.95, '🏈 CHAMPIONSHIP', ha='center', fontsize=28,
           fontweight='bold', transform=ax.transAxes)
    ax.text(0.5, 0.89, 'BY THE NUMBERS', ha='center', fontsize=22,
           color='#666666', transform=ax.transAxes)

    # Team headers with emoji flags
    ax.text(0.22, 0.80, 'πŸ”΄ STATE', ha='center', fontsize=20,
           fontweight='bold', color='#CC0033', transform=ax.transAxes)
    ax.text(0.78, 0.80, 'πŸ”΅ RIVAL', ha='center', fontsize=20,
           fontweight='bold', color='#003366', transform=ax.transAxes)

    # Comparison items with visual bars
    comparisons = [
        ('Points/Game', 38.2, 32.8, '🎯'),
        ('Success Rate', 47.2, 44.5, 'βœ…'),
        ('Explosive Plays', 13.4, 9.8, 'πŸ’₯'),
        ('Turnovers', '+8', '+5', 'πŸ”„'),
    ]

    y_start = 0.68
    y_step = 0.15

    for i, (label, state_val, rival_val, emoji) in enumerate(comparisons):
        y = y_start - i * y_step

        # Emoji and label
        ax.text(0.5, y + 0.04, f'{emoji} {label}', ha='center', fontsize=16,
               color='#333333', transform=ax.transAxes)

        # Values with size indicating winner
        state_size = 24 if float(str(state_val).replace('+', '')) >= float(str(rival_val).replace('+', '')) else 18
        rival_size = 24 if float(str(rival_val).replace('+', '')) >= float(str(state_val).replace('+', '')) else 18

        ax.text(0.22, y - 0.03, str(state_val), ha='center',
               fontsize=state_size, fontweight='bold',
               color='#CC0033', transform=ax.transAxes)
        ax.text(0.78, y - 0.03, str(rival_val), ha='center',
               fontsize=rival_size, fontweight='bold',
               color='#003366', transform=ax.transAxes)

    # Call to action
    ax.text(0.5, 0.08, 'Who ya got? πŸ‘‡', ha='center', fontsize=18,
           color='#666666', transform=ax.transAxes)

    # Hashtag
    ax.text(0.5, 0.03, '#ChampionshipGame #CollegeFootball',
           ha='center', fontsize=12, color='#999999', transform=ax.transAxes)

    ax.axis('off')

    return fig

Key Design Decisions: 1. Emoji usage: Adds visual interest and communicates quickly 2. Call to action: "Who ya got?" encourages comments 3. Hashtags: Increases discoverability 4. Bold contrasts: Works even as a small thumbnail 5. Limited text: ~50 words maximum 6. Simple comparison: Visual weight shows winner

"Hot Take" Stat Graphic

def create_hot_take_graphic():
    """Create attention-grabbing single stat graphic."""

    fig, ax = plt.subplots(figsize=(10.8, 10.8))
    fig.patch.set_facecolor('#CC0033')
    ax.set_facecolor('#CC0033')

    # Giant stat
    ax.text(0.5, 0.55, '+8', ha='center', va='center', fontsize=120,
           fontweight='bold', color='white', transform=ax.transAxes)

    # Context
    ax.text(0.5, 0.30, 'TURNOVER MARGIN', ha='center', fontsize=28,
           fontweight='bold', color='white', transform=ax.transAxes)

    ax.text(0.5, 0.22, 'Best in the Conference', ha='center', fontsize=18,
           color='#ffcccc', transform=ax.transAxes)

    # Team branding
    ax.text(0.5, 0.85, 'STATE FOOTBALL', ha='center', fontsize=24,
           fontweight='bold', color='white', transform=ax.transAxes)

    ax.axis('off')

    return fig

Audience 3: Coaching Staff

Constraints

  • View time: Extended study (minutes to hours)
  • Format: Print-ready and tablet-optimized
  • Context: Film room, practice planning, game prep
  • Goal: Actionable insights for game planning

Design Approach

Situational Tendency Report

def create_coaching_report():
    """Create detailed coaching analysis visualization."""

    fig = plt.figure(figsize=(11, 8.5))  # Letter size for printing

    # Create grid layout
    gs = fig.add_gridspec(3, 3, hspace=0.3, wspace=0.3,
                         left=0.08, right=0.95, top=0.90, bottom=0.08)

    # Title
    fig.suptitle('RIVAL UNIVERSITY - OFFENSIVE TENDENCIES',
                fontsize=16, fontweight='bold', y=0.96)
    fig.text(0.5, 0.92, 'Based on last 5 games | Prepared for Championship Week',
            ha='center', fontsize=10, color='gray')

    # 1. Down & Distance Heatmap (spans 2 columns)
    ax1 = fig.add_subplot(gs[0, :2])
    create_tendency_heatmap(ax1)

    # 2. Formation Distribution
    ax2 = fig.add_subplot(gs[0, 2])
    create_formation_pie(ax2)

    # 3. Red Zone Tendencies
    ax3 = fig.add_subplot(gs[1, 0])
    create_redzone_breakdown(ax3)

    # 4. Third Down Analysis
    ax4 = fig.add_subplot(gs[1, 1])
    create_third_down_chart(ax4)

    # 5. Key Player Focus
    ax5 = fig.add_subplot(gs[1, 2])
    create_player_focus(ax5)

    # 6. Drive Chart (spans full width)
    ax6 = fig.add_subplot(gs[2, :])
    create_drive_summary(ax6)

    return fig

def create_tendency_heatmap(ax):
    """Down Γ— Distance play-calling tendencies."""

    data = np.array([
        [0.45, 0.38, 0.35, 0.30],  # 1st down: run %
        [0.42, 0.35, 0.28, 0.22],  # 2nd down
        [0.25, 0.18, 0.12, 0.08]   # 3rd down
    ])

    im = ax.imshow(data, cmap='RdYlGn_r', aspect='auto', vmin=0, vmax=0.5)

    # Annotations
    for i in range(3):
        for j in range(4):
            text = f'{data[i,j]:.0%}'
            ax.text(j, i, text, ha='center', va='center', fontsize=10)

    ax.set_xticks(range(4))
    ax.set_xticklabels(['1-3', '4-6', '7-10', '11+'], fontsize=9)
    ax.set_yticks(range(3))
    ax.set_yticklabels(['1st', '2nd', '3rd'], fontsize=9)
    ax.set_xlabel('Distance', fontsize=10)
    ax.set_ylabel('Down', fontsize=10)
    ax.set_title('Run % by Situation', fontsize=11, fontweight='bold')

    # Colorbar
    cbar = plt.colorbar(im, ax=ax, shrink=0.8)
    cbar.ax.tick_params(labelsize=8)

def create_formation_pie(ax):
    """Formation usage breakdown."""
    formations = ['11 Personnel', '12 Personnel', '21 Personnel', 'Other']
    sizes = [45, 28, 18, 9]
    colors = ['#2d4a6f', '#4a7ca8', '#7eb5d6', '#b8d4e8']

    ax.pie(sizes, labels=formations, autopct='%1.0f%%', colors=colors,
          textprops={'fontsize': 8})
    ax.set_title('Personnel Groupings', fontsize=11, fontweight='bold')

def create_redzone_breakdown(ax):
    """Red zone play calling."""
    plays = ['Run Inside', 'Run Outside', 'Pass Short', 'Pass Deep']
    values = [35, 20, 30, 15]

    bars = ax.barh(plays, values, color='#CC0033')
    ax.set_xlim(0, 50)
    ax.set_xlabel('% of Plays', fontsize=9)
    ax.set_title('Red Zone Tendencies', fontsize=11, fontweight='bold')

    for bar, val in zip(bars, values):
        ax.text(val + 1, bar.get_y() + bar.get_height()/2,
               f'{val}%', va='center', fontsize=9)

def create_third_down_chart(ax):
    """Third down conversion analysis."""
    distances = ['Short (1-3)', 'Medium (4-6)', 'Long (7+)']
    conv_rate = [72, 48, 28]

    colors = ['green', 'orange', 'red']
    bars = ax.bar(distances, conv_rate, color=colors, alpha=0.7)
    ax.set_ylabel('Conv. Rate %', fontsize=9)
    ax.set_title('3rd Down Conversion', fontsize=11, fontweight='bold')
    ax.set_ylim(0, 100)

    for bar, val in zip(bars, conv_rate):
        ax.text(bar.get_x() + bar.get_width()/2, val + 2,
               f'{val}%', ha='center', fontsize=9)

def create_player_focus(ax):
    """Key player analysis."""
    ax.text(0.5, 0.85, 'RB SMITH', ha='center', fontsize=14,
           fontweight='bold', transform=ax.transAxes)
    ax.text(0.5, 0.70, '#22', ha='center', fontsize=12,
           color='gray', transform=ax.transAxes)

    stats = [
        'Carries: 245 (20.4/game)',
        'Yards: 1,687 (6.9 YPC)',
        'TDs: 18',
        'Broken Tackles: 47',
        'Yards After Contact: 3.8'
    ]

    for i, stat in enumerate(stats):
        ax.text(0.5, 0.55 - i*0.12, stat, ha='center', fontsize=9,
               transform=ax.transAxes)

    ax.text(0.5, 0.08, '⚠️ Key threat on outside zone',
           ha='center', fontsize=9, color='red', transform=ax.transAxes)
    ax.axis('off')
    ax.set_title('Key Player', fontsize=11, fontweight='bold')

def create_drive_summary(ax):
    """Last 5 games drive outcomes."""
    games = ['Game 8', 'Game 9', 'Game 10', 'Game 11', 'Game 12']
    tds = [4, 5, 3, 6, 4]
    fgs = [2, 1, 2, 1, 2]
    punts = [3, 2, 4, 2, 3]
    turnovers = [1, 1, 2, 0, 1]

    x = np.arange(len(games))
    width = 0.2

    ax.bar(x - 1.5*width, tds, width, label='TD', color='green')
    ax.bar(x - 0.5*width, fgs, width, label='FG', color='blue')
    ax.bar(x + 0.5*width, punts, width, label='Punt', color='gray')
    ax.bar(x + 1.5*width, turnovers, width, label='TO', color='red')

    ax.set_xticks(x)
    ax.set_xticklabels(games, fontsize=9)
    ax.set_ylabel('Drives', fontsize=9)
    ax.set_title('Drive Outcomes - Last 5 Games', fontsize=11, fontweight='bold')
    ax.legend(loc='upper right', fontsize=8)

Key Design Decisions: 1. Dense information: Coaches need comprehensive data 2. Print-optimized: Clear at 300 DPI on letter paper 3. Structured layout: Information hierarchy through grid 4. Actionable callouts: Key warnings highlighted in red 5. Multiple chart types: Different insights need different views


Audience 4: Athletic Director Presentation

Constraints

  • View time: 2-3 minutes per slide
  • Format: PowerPoint-ready, projected in boardroom
  • Context: Budget discussions, program evaluation
  • Goal: Demonstrate analytics ROI and program success

Design Approach

Program Success Overview

def create_executive_slide():
    """Create executive presentation slide."""

    fig, axes = plt.subplots(1, 3, figsize=(14, 5))
    fig.patch.set_facecolor('white')

    # Slide title (would be in PowerPoint, shown here for context)
    fig.suptitle('Analytics-Driven Performance Improvement',
                fontsize=18, fontweight='bold', y=1.02)

    # Chart 1: Year-over-year EPA improvement
    ax1 = axes[0]
    years = ['2021', '2022', '2023', '2024']
    epa = [0.08, 0.12, 0.18, 0.22]

    bars = ax1.bar(years, epa, color=['#cccccc', '#999999', '#666666', '#CC0033'])
    ax1.set_ylabel('Offensive EPA/Play', fontsize=11)
    ax1.set_title('4-Year EPA Trajectory', fontsize=12, fontweight='bold')
    ax1.axhline(y=0.15, color='green', linestyle='--', label='Top 25 Threshold')
    ax1.legend(fontsize=9)

    for bar, val in zip(bars, epa):
        ax1.text(bar.get_x() + bar.get_width()/2, val + 0.005,
                f'{val:.2f}', ha='center', fontsize=10, fontweight='bold')

    # Chart 2: Win correlation
    ax2 = axes[1]
    teams_epa = np.random.uniform(0, 0.25, 130)
    teams_wins = 3 + teams_epa * 35 + np.random.normal(0, 1.5, 130)
    teams_wins = np.clip(teams_wins, 0, 12)

    ax2.scatter(teams_epa, teams_wins, alpha=0.3, color='gray', s=30)
    ax2.scatter([0.22], [11], color='#CC0033', s=150, zorder=5,
               label='State University')

    # Trend line
    z = np.polyfit(teams_epa, teams_wins, 1)
    p = np.poly1d(z)
    x_line = np.linspace(0, 0.25, 100)
    ax2.plot(x_line, p(x_line), 'b--', alpha=0.5)

    ax2.set_xlabel('Offensive EPA/Play', fontsize=11)
    ax2.set_ylabel('Wins', fontsize=11)
    ax2.set_title('EPA vs. Wins (FBS)', fontsize=12, fontweight='bold')
    ax2.legend(fontsize=9)

    # Chart 3: Conference ranking
    ax3 = axes[2]
    categories = ['Offense\nEPA', 'Defense\nEPA', 'Special\nTeams', 'Overall']
    ranks = [2, 3, 4, 2]

    bars = ax3.bar(categories, ranks, color='#CC0033')
    ax3.set_ylabel('Conference Rank', fontsize=11)
    ax3.set_title('Conference Standings', fontsize=12, fontweight='bold')
    ax3.set_ylim(0, 15)
    ax3.invert_yaxis()  # Lower rank is better

    for bar, rank in zip(bars, ranks):
        ax3.text(bar.get_x() + bar.get_width()/2, rank + 0.3,
                f'#{rank}', ha='center', fontsize=12, fontweight='bold')

    plt.tight_layout()
    return fig

Key Design Decisions: 1. Business metrics focus: EPA linked to wins (ROI demonstration) 2. Simple comparisons: 3 clear charts, not 15 complex ones 3. Success highlighted: School position clearly marked 4. Trend emphasis: Multi-year improvement shown 5. Minimal technical jargon: "EPA" explained in title


Comparative Analysis

Summary Table

Aspect Broadcast Social Media Coaching Executive
View Time 5-8 sec 2-3 sec Minutes-Hours 2-3 min
Metrics Shown 3 4 20+ 6-8
Font Size (min) 16pt 14pt 8pt 10pt
Color Complexity Low Medium High Medium
Interactivity None None Links to film Click-through
Emotional Appeal Medium High Low Medium
Technical Depth Surface Surface Deep Summary
Call to Action Watch game Engage Prepare Approve budget

Color Palette Adaptations

# Same semantic meaning, different execution
color_schemes = {
    'broadcast': {
        'background': '#1a1a2e',  # Dark for TV contrast
        'primary': '#ffffff',
        'accent': 'team_colors'
    },
    'social': {
        'background': '#ffffff',  # Clean for small screens
        'primary': '#333333',
        'accent': 'bright_saturated'
    },
    'coaching': {
        'background': '#ffffff',  # Print-friendly
        'primary': '#000000',
        'accent': 'muted_functional'
    },
    'executive': {
        'background': '#ffffff',  # Professional
        'primary': '#333333',
        'accent': 'brand_colors'
    }
}

Key Lessons

1. Same Data, Different Stories

The underlying data was identical, but each visualization told a different story: - Broadcast: "This is an exciting matchup" - Social: "Who will win?" - Coaching: "Here's how to beat them" - Executive: "Analytics drives wins"

2. Audience Constraints Drive Design

Every design decision flowed from audience constraints: - Time available β†’ Information density - Viewing context β†’ Typography choices - User goals β†’ Which metrics to highlight - Technical literacy β†’ Terminology level

3. Simplification Requires Expertise

Knowing what to leave out is harder than including everything. The broadcast graphic showing only 3 stats required understanding which 3 were most meaningful.

4. Brand Consistency Across Contexts

Despite different designs, brand elements remained consistent: - School colors appeared in all versions - Logo placement followed guidelines - Typography family remained consistent


Discussion Questions

  1. How would you adapt these visualizations for a radio broadcast (audio only)?

  2. What additional audience segment might benefit from customized visualizations?

  3. How do you balance data accuracy with emotional appeal in fan-facing content?

  4. What ethical considerations arise when simplifying data for different audiences?

  5. How would you measure the effectiveness of each visualization for its intended purpose?


Practice Exercise

Using the matchup data provided at the beginning of this case study, create your own visualization for a fifth audience: High School Recruits and Their Families.

Consider: - What do recruits care about most? - How sophisticated are they with football analytics? - What emotional elements should be included? - What's the appropriate balance of program success vs. player development metrics?

Document your design decisions and justify each choice based on audience analysis.