Learning ObjectivesBy the end of this chapter, you will be able to:
- Communicate technical concepts to non-technical audiences effectively
- Build effective visualizations and dashboards for football decision-makers
- Present analytics recommendations persuasively to coaches and executives
- Tailor communication strategies to different stakeholder groups
- Build credibility and trust with coaches, executives, and scouts
Introduction
The most sophisticated analytics model in the world is worthless if it cannot influence decisions. In football, where coaches and executives must make rapid, high-stakes choices under pressure, the ability to communicate analytics insights clearly and persuasively is as important as the analytical skills themselves.
This chapter addresses what many analytics professionals consider the hardest part of the job: translating complex statistical findings into actionable insights that non-technical decision-makers can understand, trust, and implement. While technical skills can get you hired, communication skills determine whether your work actually influences decisions and improves team performance.
Consider the real-world challenge facing analytics professionals: You spend weeks developing a sophisticated Expected Points Added (EPA) model that suggests the team should pass 15% more on early downs. Your analysis is rigorous—you've accounted for down-and-distance, field position, personnel groupings, and opponent quality. Your cross-validation shows strong predictive power. You've run thousands of simulations confirming the recommendation. But when you present this to the offensive coordinator—a former quarterback with 25 years of coaching experience—you're met with skepticism, pushback, or worse, polite indifference.
The problem isn't your analysis. The problem is the communication gap between data science and football operations. Coaches think in terms of formations, matchups, and game situations. They trust their eyes and their experience. They make decisions based on feel, instinct, and pattern recognition developed over decades. Analytics professionals, meanwhile, think in terms of statistical significance, confidence intervals, and expected values. These are two different languages, two different cultures, two different ways of understanding the game.
The Communication Challenge
A study of NFL analytics departments found that technical skills accounted for only 40% of an analyst's effectiveness. The remaining 60% depended on communication skills, relationship-building, and understanding football operations. The best analysts are those who can bridge both worlds—speaking fluent "football" while thinking in data. This isn't unique to football. In every industry where analytics meets domain expertise, communication determines impact. Medical researchers must communicate with physicians. Data scientists must communicate with business executives. The technical work is necessary but not sufficient. What separates good analysts from great ones is the ability to make complex insights accessible, actionable, and persuasive.The answer to our communication challenge involves understanding your audience deeply, presenting data visually rather than numerically, telling compelling stories with data, addressing objections preemptively, and building trust incrementally over time. It requires humility about what analytics can and cannot do, respect for football expertise, and patience as organizational culture evolves. This chapter will teach you how to master these essential communication skills.
Throughout this chapter, we'll explore the full spectrum of analytics communication: from knowing your audience and tailoring your message, to building interactive dashboards and automated reports, to presenting recommendations persuasively and handling resistance. We'll cover visualization best practices specifically for football decision-makers, storytelling techniques that make data memorable, and credibility-building strategies that transform skeptics into advocates. By the end, you'll have a complete toolkit for communicating analytics insights effectively in football organizations.
Know Your Audience
Different stakeholders in a football organization have vastly different backgrounds, priorities, and decision-making styles. Effective communication requires tailoring your message to each audience. A presentation that works for a general manager with an MBA background will fail completely with a position coach who played in the NFL for ten years. Understanding these differences is the foundation of effective communication.
The key to audience analysis is recognizing that different roles in football organizations have different decision contexts, time constraints, technical literacy, and sources of influence. A head coach makes dozens of critical decisions during a game with seconds to think. A general manager makes a few high-stakes decisions per year with months to deliberate. A scout evaluates hundreds of players using criteria developed over decades of observation. Each audience requires a different communication approach.
Audience Types and Characteristics
Head Coaches
- Background: Typically former players or position coaches who worked their way up through the ranks
- Time constraints: Extremely limited—often just minutes to review information, not hours
- Decision focus: Game planning, in-game adjustments, player deployment, practice structure
- Analytics appetite: Varies widely; some are enthusiastic adopters, others are deeply skeptical
- Communication preference: Executive summaries, visual dashboards, brief verbal updates
- What they value: Actionable insights that can be implemented immediately, information that provides competitive advantage
- What frustrates them: Academic analysis without clear recommendations, jargon-heavy presentations, information overload
Coordinators (Offensive/Defensive/Special Teams)
- Background: Deep tactical knowledge in their specific domain, often former players or specialists
- Time constraints: Moderate during the week for planning, severe on game days
- Decision focus: Play design, game scripts, matchup exploitation, personnel packages
- Analytics appetite: Generally higher than head coaches; more willing to engage with detailed analysis
- Communication preference: Detailed reports with specific recommendations, interactive sessions to discuss findings
- What they value: Tactical insights about opponents, validation of strategic hunches, optimization of existing approaches
- What frustrates them: Generic league-wide trends that don't apply to their specific scheme or personnel
General Managers and Front Office
- Background: Mix of former players, scouts, and business professionals; increasingly MBA-educated
- Time constraints: More available during certain seasons (draft, free agency, training camp)
- Decision focus: Player acquisition, salary cap management, draft strategy, long-term roster construction
- Analytics appetite: Usually high; many have formal business training and understand statistical concepts
- Communication preference: Comprehensive reports, comparative analyses, financial models, ROI calculations
- What they value: Predictive models for player performance, valuation frameworks, market inefficiencies
- What frustrates them: Analysis that ignores salary cap implications or roster constraints
Owners
- Background: Business executives, often from non-football industries (technology, finance, entertainment)
- Time constraints: Episodic engagement rather than daily involvement
- Decision focus: Strategic direction, major investments, coaching and front office hires
- Analytics appetite: High for business metrics, variable for football-specific analytics
- Communication preference: Executive summaries, ROI analyses, benchmarking against other teams
- What they value: Competitive advantage, measurable return on investment, modern approaches that signal innovation
- What frustrates them: Football jargon without business context, inability to quantify impact
Scouts
- Background: Extensive player evaluation experience, often former players or coaches
- Time constraints: Travel-heavy schedules make them hard to reach consistently
- Decision focus: Player identification, trait evaluation, projection of college to pro performance
- Analytics appetite: Historically low, but increasing as younger scouts enter the profession
- Communication preference: Player-specific reports, trait correlations, validation tools that complement film study
- What they value: Analytics that confirms or challenges their evaluations, measurable indicators of traits they observe on film
- What frustrates them: Models that seem to contradict obvious film evidence, over-reliance on statistics without context
Position Coaches
- Background: Deep expertise in specific positions, often former players at that position
- Time constraints: Very limited; focused on daily practice and player development
- Decision focus: Individual player development, technique coaching, weekly game plan implementation
- Analytics appetite: Moderate; interested in performance measurement for their position group
- Communication preference: Individual player metrics, development tracking over time, peer comparisons
- What they value: Objective performance measures that track player improvement, insights about opponents' players at their position
- What frustrates them: Team-level analysis that doesn't help with their specific coaching responsibilities
Best Practice: Conduct Audience Research
Before presenting analytics insights, invest time in understanding your audience: 1. **Understand their background**: Did they play football? At what level? What position? Do they have formal business or analytics education? Their background shapes how they process information. 2. **Learn their language**: What terminology do they use in meetings? How do they describe plays and concepts? Adopt their vocabulary rather than forcing them to learn yours. 3. **Identify their pain points**: What decisions keep them up at night? What problems are they trying to solve? Frame your insights as solutions to their specific challenges. 4. **Discover their preferred format**: Do they prefer visual information or written reports? Do they want to interact with dashboards or receive static PDFs? Do they like face-to-face presentations or asynchronous communication? Adapt to their preferences. 5. **Gauge their analytics literacy**: Can they interpret a confidence interval? Do they understand regression? Or do you need to explain everything in plain English? Calibrate your technical level accordingly. 6. **Observe their decision-making style**: Do they make quick intuitive decisions or deliberate carefully? Do they seek consensus or decide independently? Do they value data or trust their gut? Understanding their style helps you position analytics appropriately.Example: Tailoring the Same Insight
Let's see how you might present the same finding—that the team should pass more on early downs—to different audiences. Notice how the core insight remains the same, but the framing, language, level of detail, and call-to-action change dramatically:
To the Head Coach (30-second elevator pitch):
"Coach, our early-down data shows we're running into stacked boxes 60% of the time, averaging just 3.2 yards per carry. When we pass on early downs against these same defensive looks, we average 7.8 yards and sustain drives. We've identified 8-10 formations where early-down passes have worked particularly well against the fronts we're seeing. Can I show you a two-minute video compilation of these plays? I think we can exploit this pattern against our next opponent."
Why this works for head coaches: It's brief and actionable. It connects to observable football concepts (stacked boxes, yards per play) rather than abstract statistics. It offers a concrete next step (watch a short video) rather than demanding immediate commitment. It frames the insight as an opportunity to exploit, not a criticism of current approach.
To the Offensive Coordinator (detailed session):
"Looking at our first and second down efficiency over the past five games, I've identified a 0.12 EPA advantage when we pass against 7+ defenders in the box—that's statistically significant at p<0.01 with a large effect size. Here's the breakdown by formation, personnel grouping, and down-and-distance. The advantage is strongest from 11 personnel in shotgun on 1st-and-10. I've also pulled examples from Ravens and 49ers film showing similar concepts that worked effectively against opponents we face in the next three weeks. I can build you a tendency report showing when opponents are most likely to stack the box, which would help you script early-down passes for maximum advantage."
Why this works for coordinators: It provides tactical detail and statistical rigor they can trust. It breaks down the finding by the categories coordinators think in (formations, personnel, situations). It connects to film study they're already doing. It offers a specific tool (tendency report) that fits their workflow. It respects their expertise by presenting analytics as complementary to their film study.
To the General Manager (strategic memo):
"Our offensive efficiency metrics suggest we're not optimizing our passing game investment. We rank 3rd in the NFL in pass-play EPA (0.15) but 22nd in pass rate on early downs (48% vs. league average 58%). This represents a strategic misalignment between our personnel investments and our play-calling strategy. Given our significant investment in [QB name]'s contract ($40M/year) and our draft capital allocated to receivers (2nd round pick in 2022, 3rd round in 2023), increasing early-down passing could improve our scoring efficiency by an estimated 0.8 points per game. Over a 17-game season, this translates to approximately 1-2 additional wins, which based on historical data would increase our playoff probability by 15-20 percentage points. The implementation cost is minimal—this is a play-calling adjustment, not a personnel change—making it one of our highest ROI opportunities."
Why this works for general managers: It connects tactical analysis to strategic decisions (roster construction, salary allocation). It quantifies impact in business terms (ROI, playoff probability). It frames the recommendation within organizational resource constraints. It provides the "so what" in terms they care about (wins, playoffs) rather than football minutiae.
To a Scout (player evaluation context):
"When evaluating college receivers for the upcoming draft, I wanted to share some context about our offensive usage. Our analytics show we're passing on early downs at below-average rates (48% vs. NFL average of 58%), which means we're potentially underutilizing our receivers' pass-catching abilities. This suggests we should prioritize route-running ability and hands over blocking skills in our receiver targets, since increasing our early-down pass rate would give receivers more opportunities to contribute. Specifically, receivers who excel at short and intermediate route concepts would provide immediate value if we adjust our offensive approach. I've flagged three prospects in the draft model who fit this profile and are projected to be available in rounds 3-5."
Why this works for scouts: It connects analytics to their specific responsibility (player evaluation and draft preparation). It provides actionable criteria for scouting (prioritize route-running over blocking). It respects their domain expertise while offering data-driven context. It concludes with specific prospects to evaluate, making the insight immediately applicable to their work.
Key Principle: One Finding, Multiple Framings
The same analytical finding can and should be communicated differently to different audiences. The underlying truth doesn't change, but the framing, emphasis, level of detail, and call-to-action must adapt to each stakeholder's role, priorities, and decision context. Master communicators develop the skill of "code-switching"—moving fluidly between different communication modes depending on their audience. This isn't about being manipulative or inconsistent. It's about respecting that different roles require different types of information to make decisions. A head coach needs quick, actionable insights for immediate implementation. A coordinator needs tactical details to design plays. A GM needs strategic context for resource allocation. A scout needs player evaluation criteria. The analytics insight serves all these needs simultaneously—you just need to present it in the language and format each audience requires.Principles of Effective Communication
Regardless of your audience, certain principles apply universally to analytics communication in football. These foundational principles transcend specific situations and create a framework for effective communication across all stakeholder groups.
1. Start with the "So What"
Decision-makers don't care about your methodology until they understand why it matters. Lead with the actionable insight, not the analytical process. This principle recognizes that coaches and executives are overwhelmed with information and have limited attention spans. You have approximately 30 seconds to convince them that your analysis is worth their time.
Poor approach (methodology-first):
"We built a gradient boosting model with 47 features including down, distance, field position, personnel groupings, and defensive fronts. We used 5-fold cross-validation and achieved 0.83 AUC on the validation set with RMSE of 6.2 yards. The most important features by SHAP values were defensive front alignment, receiver spacing, and play-action tendency..."
Why this fails: By the second sentence, you've lost your audience. They don't know what gradient boosting is, don't care about AUC scores, and have no idea why this matters for winning football games. You're showcasing technical sophistication but failing to communicate value.
Better approach (insight-first):
"Our quarterback performs significantly better against press coverage than off coverage—he averages 8.2 yards per attempt against press compared to 6.1 yards against off coverage, a difference worth about 0.18 EPA per play. This means we should design more plays specifically to attack press coverage. Against teams that play primarily press coverage, we have a substantial advantage we're not fully exploiting. I can show you the film examples and the specific route concepts that work best if you'd like details."
Why this works: You immediately answer "so what"—we have an exploitable advantage. You provide football-relevant context (specific coverage types) and quantify the opportunity (8.2 vs. 6.1 yards). You offer details on demand rather than forcing them upfront. The decision-maker can immediately understand the value and choose whether to engage further.
2. Use Football Language, Not Statistics Language
Translate statistical concepts into football terms whenever possible. Football professionals have a rich, precise vocabulary for describing game situations. When you use their language, they immediately understand and trust you more. When you use statistical jargon, you create barriers.
| Statistics Term | Football Translation | Why the Translation Works |
|---|---|---|
| "95% confidence interval from 4.8 to 6.2 yards" | "We're very confident the true value is around 5-6 yards" | Eliminates technical jargon while preserving the key insight: high certainty within a range |
| "Statistically significant at p<0.05" | "This pattern is real, not just random luck" | Conveys the core meaning (signal vs. noise) without requiring statistical training |
| "Regression to the mean" | "Hot streaks and cold streaks tend to even out over time" | Uses intuitive concept everyone understands from experience |
| "Multicollinearity between features" | "These factors overlap, so we can't separate their individual effects" | Explains the statistical issue in plain English |
| "P-value of 0.03" | "This happens by chance less than 1 in 30 times" | Translates probability into frequency, which is more intuitive |
| "Heteroscedasticity in residuals" | "Our uncertainty is higher in some situations than others" | Describes the practical implication rather than the technical problem |
| "Standard deviation of 1.2 yards" | "Most plays fall within about 1-2 yards of the average" | Connects abstract measure to concrete yardage |
| "Correlation coefficient of 0.67" | "These two things tend to happen together, but not always" | Describes relationship without technical terminology |
Common Pitfall: The Jargon Trap
Many analysts fall into the "jargon trap"—using technical language to sound smart or credible. This backfires. When you use terms like "heteroscedasticity," "multicollinearity," or "autoregressive" without definition, you're signaling that this analysis is not for them—it's for other analysts. You're creating distance rather than building bridges. Some analytics professionals defend jargon as "precise" or "necessary." But precision is worthless if your audience doesn't understand. The goal isn't to impress people with your vocabulary—it's to influence decisions. Simple, clear language influences decisions. Jargon does not. **Exception**: When presenting to analytically sophisticated audiences (some GMs, analytics-forward front offices), you can use more technical language. But even then, start simple and only increase complexity if your audience signals they want more detail.3. Show, Don't Just Tell
Visualizations are almost always more effective than tables or text for communicating patterns and insights. The human brain processes visual information faster and more effectively than text or numbers. A well-designed chart can communicate in seconds what would take paragraphs to explain in words.
Why visualization works in football:
- Pattern recognition: Coaches are expert pattern recognizers from watching thousands of hours of film. Visualizations leverage this strength.
- Quick comprehension: During busy weeks, coaches have minutes, not hours. A dashboard can convey complex information instantly.
- Memory: People remember images better than numbers. A striking visualization stays with decision-makers.
- Comparison: Visual encodings (position, color, size) make comparisons easy and obvious.
Visualization hierarchy for football audiences:
1. Best: Interactive dashboards with team logos, clean design, annotated insights
2. Good: Static charts with clear labels, team colors, contextual information
3. Acceptable: Simple tables with conditional formatting and highlights
4. Poor: Large tables of raw numbers without formatting
5. Worst: Dense text descriptions of numerical patterns
We'll explore specific visualization techniques and examples in the next section, but the principle is clear: when you have a choice between showing and telling, always choose showing.
4. Provide Context and Comparisons
Numbers mean nothing in isolation. Always provide context through comparisons, benchmarks, or historical trends. A coach hearing "0.15 EPA per play" has no frame of reference. Is that good? Bad? Average? Exceptional? Without context, the number is meaningless.
Types of context that work:
- League averages: "Our 0.15 EPA/play ranks 3rd in the NFL, well above the league average of 0.06"
- Historical trends: "This is our highest defensive success rate in 10 years"
- Peer comparisons: "Among teams that made the playoffs last year, we rank 4th in this metric"
- Expected values: "We expected about 9 sacks based on our pressure rate; we generated only 6, suggesting we need to improve conversion"
- Year-over-year changes: "Our third-down conversion rate improved from 38% last year to 44% this year"
- Positional benchmarks: "For a quarterback in his second year, this production is in the 85th percentile historically"
- Situational norms: "Teams typically convert 60% of fourth-and-1 attempts; we're converting 72%"
The comparison you choose depends on your audience and purpose. When presenting to competitive coaches, peer comparisons work well ("We rank 3rd"). When discussing player development, historical trends matter ("Best in 10 years"). When evaluating decisions, expected values provide context ("Expected 9, got 6"). Choose comparisons that are meaningful for your specific audience and message.
5. Acknowledge Uncertainty
Being honest about limitations builds trust. Analytics professionals sometimes oversell their findings to seem more credible, but this backfires. Experienced decision-makers know that football has randomness, that models have limitations, and that recommendations don't work 100% of the time. When you acknowledge these realities upfront, you demonstrate intellectual honesty and build credibility.
How to acknowledge uncertainty effectively:
"Our model suggests this approach would improve our expected points by 0.15 per drive, but this estimate is based on data from 2020-2023. Defensive strategies may have evolved since then, and our specific opponent might have prepared for this approach. We recommend testing it in practice before full implementation. If it doesn't work in practice, we should reconsider."
What this accomplishes:
- Shows you understand football context (opponents prepare, strategies evolve)
- Demonstrates scientific thinking (test before implementing)
- Protects your credibility (you warned them if it doesn't work)
- Invites collaboration (asking for their input via practice testing)
Types of uncertainty to acknowledge:
- Data limitations: "We only have 5 games of data on this opponent, so we're less confident in these tendencies"
- Model assumptions: "This assumes our offensive line performs similarly to how they've played this season"
- External validity: "This worked for other teams, but may not work with our specific personnel"
- Randomness: "Even if this is the right decision, it won't work every time due to execution and luck"
- Incomplete information: "We don't have data on [X factor], which could influence outcomes"
6. Be Action-Oriented
Every analysis should lead to clear next steps. Descriptive analysis is interesting, but prescriptive recommendations drive decisions. Your audience is busy; they need to know not just what you found, but what they should do about it.
Weak conclusion (descriptive only):
"Running backs drafted in the first round tend to have longer careers than those drafted later. First-round backs average 6.2 seasons while Day 2-3 backs average 4.1 seasons. Career length correlates with draft position at r=0.34, which is statistically significant."
Why this fails: It's academically interesting but provides no decision guidance. Should we draft a running back in the first round? Should we avoid late-round backs? What's the actionable takeaway?
Strong conclusion (prescriptive and actionable):
"Because early-round running backs have longer careers (6.2 vs. 4.1 seasons) but similar peak performance to later-round backs, we should target Day 2-3 running backs who fit our zone-blocking scheme rather than using a first-round pick on the position. This allows us to get 70% of the production at 20% of the draft capital cost, which we can reallocate to positions with higher value-above-replacement like offensive line or pass rusher. Specifically, I recommend we target running backs in rounds 3-5 who excel in the following traits: [specific list]."
Why this works: It translates the finding into a clear strategic recommendation. It explains the reasoning (similar peak performance, lower cost). It provides specific guidance (rounds 3-5, specific traits). It connects to broader roster strategy (capital reallocation). The decision-maker knows exactly what to do.
Framework for action-oriented conclusions:
1. What: State the specific recommendation clearly
2. Why: Explain the evidence supporting it
3. When: Specify timing or triggers for implementation
4. Who: Identify responsible parties
5. How: Outline implementation steps if appropriate
6. What if: Address potential objections or failure modes
7. Respect Football Expertise
Analytics complements football knowledge; it doesn't replace it. The best analysts recognize that coaches and scouts have irreplaceable expertise developed over decades of observation, practice, and competition. Frame insights as tools to enhance existing expertise, not as revelations from on high.
Arrogant framing (antagonistic):
"The data proves you're wrong about this player. Your evaluation says he's good, but the numbers clearly show he's below average. You need to trust the model, not your eyes."
Why this fails catastrophically: You've insulted their expertise, positioned analytics as adversarial to scouting, and demanded they trust your black box over their judgment. Even if you're right, you've poisoned the relationship.
Collaborative framing (respectful):
"Your instinct about this player's coverage ability was right. The data confirms he struggles specifically in zone coverage—he allows 8.2 yards per target in zone compared to 5.4 in man. This suggests we should primarily use him in man-coverage situations, which matches what you're seeing on film. Can you help me understand what specifically makes him better in man? That might help us identify other prospects with similar traits."
Why this works: You've validated their expertise, provided data that confirms and extends their observation, suggested a specific tactical application, and asked for their input. You've positioned analytics as a partner, not a replacement. You've created a collaborative dynamic.
Key phrases that show respect:
- "Your instinct was right—the data confirms..."
- "Can you help me understand what you're seeing on film that might explain this pattern?"
- "The numbers show [X], but you know the players better—does this match your observation?"
- "This is what the data suggests, but what am I missing from a coaching perspective?"
- "How would you use this information in your game planning?"
The Humility Principle
Humility doesn't weaken your message—it strengthens it. When you acknowledge the limits of analytics and defer to football expertise in appropriate domains, you build trust. When you arrogantly claim analytics has all the answers, you create resistance. Think of yourself as a tool provider, not a decision-maker. You're giving coaches and executives another tool for their toolkit—one that complements film study, scouting reports, practice observations, and accumulated experience. The best organizations integrate all these tools. The worst organizations pit them against each other.Data Visualization Best Practices for Decision-Makers
Effective visualization is crucial in football analytics communication. Decision-makers often scan reports quickly or view dashboards on mobile devices during travel. They need to extract insights in seconds, not minutes. This section explores visualization principles specifically tailored for football decision-makers, with practical code examples you can adapt.
Principles for Football Visualizations
1. Clarity Over Complexity
Simplify ruthlessly. Every element should serve a purpose. Remove chart junk, eliminate unnecessary grid lines, and minimize the data-ink ratio. Ask yourself: "Can I remove this element without losing information?" If yes, remove it.
Bad visualization elements to avoid:
- 3D effects that distort data perception
- Multiple y-axes with different scales
- Excessive decimal places (0.1534 instead of 0.15)
- Busy backgrounds or textures
- Too many colors without purpose
- Tiny fonts that are unreadable on mobile devices
Good visualization elements to include:
- Clear, descriptive titles that state the insight
- Axis labels with units clearly indicated
- Team logos for immediate recognition
- Annotations highlighting key findings
- Sufficient white space for readability
- Consistent color schemes throughout reports
2. Focus on Actionable Differences
Highlight what matters for decisions, not just statistical significance. A 0.02 EPA difference might be statistically significant with enough data, but is it large enough to change play-calling? Focus your visualization on differences that are both statistically significant AND practically meaningful.
Example: If you're comparing two formations, don't just show that one is better (statistically). Show that it's better by enough to matter (0.15 EPA vs. 0.02 EPA difference), and show the sample sizes so coaches can assess reliability.
3. Use Team Colors and Logos
Visual identity helps stakeholders quickly locate their team in comparisons. When showing 32 teams on a scatter plot, your team's logo should be immediately recognizable—perhaps larger, brighter, or highlighted. Team colors create instant recognition and connection.
Technical note: The nflplotR package in R makes this easy with functions like geom_nfl_logos(). In Python, you can load team logos from URLs and overlay them on matplotlib plots.
4. Annotate Key Insights
Don't make viewers figure out what the chart means. Add text annotations that point out the key findings. A good annotation says "Elite teams are in this quadrant" or "Our team improved by 15% here." This reduces cognitive load and ensures everyone interprets the visualization correctly.
5. Design for Mobile and Printing
Coaches often review materials on tablets during flights or printed handouts in meeting rooms. Test your visualizations on small screens and in black-and-white printing. Can you still read the text? Do the colors still differentiate when printed in grayscale? Use patterns or shapes in addition to colors for better accessibility.
6. Maintain Consistent Scales
When comparing across weeks or situations, use consistent axis scales so readers can visually compare. If Week 1 shows EPA from -0.1 to 0.3 and Week 2 shows -0.5 to 0.5, the visual comparison is misleading. Fix the scales unless you have a specific reason to let them vary.
Common Visualization Types
Let's explore effective visualization approaches with detailed code examples. We'll build a team performance comparison chart that demonstrates key principles.
Before creating the visualization, we need to understand what we're trying to communicate. Team performance comparisons typically aim to show how a team ranks across two dimensions simultaneously—often offensive and defensive efficiency. The scatter plot format works perfectly for this because position on both axes conveys information, and we can use team logos as the plotting symbols for instant recognition.
The challenge with team comparison visualizations is information density. With 32 NFL teams, the plot can become cluttered. We solve this by using logos instead of points, adding quadrant lines to show average performance, and annotating specific regions (like "Elite" for top-right quadrant). This allows decision-makers to assess their team's position and identify relevant peer groups instantly.
#| label: team-comparison-viz
#| message: false
#| warning: false
#| fig-width: 12
#| fig-height: 8
#| fig-cap: "NFL team offensive and defensive efficiency for 2023 season, with quadrant lines showing league average. Teams in the top-right quadrant excel on both offense and defense."
# Load required libraries for data manipulation and visualization
library(tidyverse) # Data manipulation and ggplot2
library(nflfastR) # NFL play-by-play data
library(nflplotR) # NFL team logos and plotting functions
# Load play-by-play data for 2023 season
# This downloads comprehensive data for every play in the season
# Data is automatically cached to speed up subsequent loads
pbp <- load_pbp(2023)
# Calculate team offensive EPA (Expected Points Added)
# We group by possession team to measure offensive efficiency
team_epa <- pbp %>%
# Filter to plays with valid EPA values and known possession team
filter(!is.na(epa), !is.na(posteam)) %>%
# Group by offensive team
group_by(posteam) %>%
# Calculate average EPA per play for each team's offense
summarise(
off_epa = mean(epa), # Higher is better for offense
.groups = "drop"
) %>%
# Rename for clearer joining
rename(team = posteam) %>%
# Join with defensive EPA calculations
left_join(
pbp %>%
# Filter to plays with valid EPA values and known defensive team
filter(!is.na(epa), !is.na(defteam)) %>%
# Group by defensive team
group_by(defteam) %>%
# Calculate average EPA allowed (negative means good defense)
# We negate EPA because from defense's perspective, lower opponent EPA is better
summarise(def_epa = -mean(epa), .groups = "drop") %>%
rename(team = defteam),
by = "team"
)
# Create scatter plot with team logos
# Each team is represented by their logo positioned at their offensive/defensive EPA
ggplot(team_epa, aes(x = off_epa, y = def_epa)) +
# Add horizontal line at average defensive EPA (splits good/bad defense)
geom_hline(yintercept = mean(team_epa$def_epa),
linetype = "dashed", alpha = 0.5, color = "gray40") +
# Add vertical line at average offensive EPA (splits good/bad offense)
geom_vline(xintercept = mean(team_epa$off_epa),
linetype = "dashed", alpha = 0.5, color = "gray40") +
# Plot team logos at their offensive/defensive EPA coordinates
# This is the key function that makes NFL visualizations instantly recognizable
geom_nfl_logos(aes(team_abbr = team), width = 0.06, alpha = 0.8) +
# Add descriptive labels
labs(
title = "2023 NFL Team Efficiency: Offense vs Defense",
subtitle = "Expected Points Added per Play | Top-right quadrant = elite on both sides",
x = "Offensive EPA/Play (Better →)",
y = "Defensive EPA/Play (Better →)",
caption = "Data: nflfastR | Each logo represents one team's average EPA"
) +
# Use clean minimal theme
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 12, color = "gray30"),
axis.title = element_text(size = 11, face = "bold"),
axis.text = element_text(size = 10),
panel.grid.minor = element_blank(), # Remove minor grid lines for clarity
panel.grid.major = element_line(color = "gray90"), # Subtle major grid
plot.caption = element_text(size = 9, color = "gray50", hjust = 0)
) +
# Add quadrant labels to help interpretation
# Elite teams (good offense AND defense) in top-right
annotate("text",
x = max(team_epa$off_epa) - 0.01,
y = max(team_epa$def_epa) - 0.01,
label = "Elite",
fontface = "bold",
size = 5,
color = "darkgreen",
hjust = 1,
vjust = 1) +
# Struggling teams (bad offense AND defense) in bottom-left
annotate("text",
x = min(team_epa$off_epa) + 0.01,
y = min(team_epa$def_epa) + 0.01,
label = "Struggling",
fontface = "bold",
size = 5,
color = "darkred",
hjust = 0,
vjust = 0)
📊 Visualization Output
The code above generates a visualization. To see the output, run this code in your R or Python environment. The resulting plot will help illustrate the concepts discussed in this section.
#| label: team-comparison-viz-py
#| message: false
#| warning: false
#| fig-width: 12
#| fig-height: 8
#| fig-cap: "NFL team offensive and defensive efficiency for 2023 season"
# Load required libraries
import pandas as pd
import numpy as np
import nfl_data_py as nfl
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import urllib.request
from PIL import Image
import io
# Load play-by-play data for 2023 season
# This function downloads data from nflfastR and returns it as a pandas DataFrame
pbp = nfl.import_pbp_data([2023])
# Calculate team offensive EPA (Expected Points Added)
# Group by possession team to measure offensive efficiency
off_epa = (pbp
# Filter to plays with valid EPA and known possession team
.query("epa.notna() & posteam.notna()")
# Group by offensive team
.groupby('posteam')['epa']
# Calculate mean EPA per play (higher is better for offense)
.mean()
# Convert to DataFrame and rename columns for clarity
.reset_index()
.rename(columns={'posteam': 'team', 'epa': 'off_epa'}))
# Calculate team defensive EPA
# We negate EPA because good defense means preventing opponent's EPA
def_epa = (pbp
# Filter to plays with valid EPA and known defensive team
.query("epa.notna() & defteam.notna()")
# Group by defensive team
.groupby('defteam')['epa']
# Calculate mean EPA and negate (lower opponent EPA = better defense)
.mean()
.apply(lambda x: -x) # Negate so higher is better (consistent with offense)
# Convert to DataFrame and rename columns
.reset_index()
.rename(columns={'defteam': 'team', 'epa': 'def_epa'}))
# Merge offensive and defensive EPA into single DataFrame
# Each row represents one team's offensive and defensive performance
team_epa = off_epa.merge(def_epa, on='team')
# Create the visualization
fig, ax = plt.subplots(figsize=(12, 8))
# Add quadrant lines showing league average performance
# Horizontal line separates above/below average defense
ax.axhline(y=team_epa['def_epa'].mean(),
color='gray', linestyle='--', alpha=0.5, linewidth=1.5)
# Vertical line separates above/below average offense
ax.axvline(x=team_epa['off_epa'].mean(),
color='gray', linestyle='--', alpha=0.5, linewidth=1.5)
# Plot each team as a point
# We'll add team labels since logo integration is more complex in matplotlib
scatter = ax.scatter(team_epa['off_epa'], team_epa['def_epa'],
s=200, alpha=0.6, color='steelblue', edgecolors='white', linewidth=2)
# Add team abbreviations as labels
# Position text at each team's coordinate for identification
for _, row in team_epa.iterrows():
ax.annotate(row['team'],
(row['off_epa'], row['def_epa']),
fontsize=8,
ha='center',
va='center',
fontweight='bold',
color='white') # White text on colored circles
# Set axis labels with clear indication of direction
ax.set_xlabel('Offensive EPA/Play (Better →)', fontsize=12, fontweight='bold')
ax.set_ylabel('Defensive EPA/Play (Better →)', fontsize=12, fontweight='bold')
# Add comprehensive title and subtitle
ax.set_title('2023 NFL Team Efficiency: Offense vs Defense\nExpected Points Added per Play',
fontsize=14, fontweight='bold', pad=20)
# Add quadrant labels for easy interpretation
# Elite quadrant (top-right): good offense AND defense
ax.text(0.98, 0.98, 'Elite', transform=ax.transAxes,
fontsize=14, fontweight='bold', color='darkgreen',
ha='right', va='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.7))
# Struggling quadrant (bottom-left): bad offense AND defense
ax.text(0.02, 0.02, 'Struggling', transform=ax.transAxes,
fontsize=14, fontweight='bold', color='darkred',
ha='left', va='bottom',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.7))
# Add data source caption
ax.text(0.99, 0.01, 'Data: nfl_data_py', transform=ax.transAxes,
fontsize=8, ha='right', va='bottom', style='italic', color='gray')
# Style the plot with subtle grid
ax.grid(True, alpha=0.3, linestyle=':', linewidth=0.5)
ax.set_facecolor('#f8f9fa') # Light gray background
# Adjust layout to prevent label cutoff
plt.tight_layout()
plt.show()
📊 Visualization Output
The code above generates a visualization. To see the output, run this code in your R or Python environment. The resulting plot will help illustrate the concepts discussed in this section.
This visualization establishes the foundation for effective football analytics communication. It's simple enough to understand in 30 seconds, sophisticated enough to convey meaningful information, and actionable enough to drive decisions about team strategy and personnel.
The key insight from this analysis is that team performance is multi-dimensional. A team might rank 5th in offense but 28th in defense, resulting in mediocre overall performance. Understanding both dimensions simultaneously helps decision-makers allocate resources appropriately. Should the GM prioritize offensive or defensive acquisitions in free agency? This chart provides data-driven guidance.
When presenting this to coaches, emphasize the actionable implications. If your team is in the bottom-right quadrant (good offense, poor defense), the strategic priority should be defensive improvement through coaching adjustments, scheme changes, or personnel moves. If you're in the top-left (good defense, poor offense), offensive investment is the priority. The visualization makes these strategic priorities visually obvious.
Customization for Your Team
To make this visualization more impactful for your specific organization: 1. **Highlight your team**: Make your team's logo larger and add a subtle circle or glow effect around it for emphasis 2. **Show division rivals**: Use different colors or sizes for division opponents to focus competitive comparison 3. **Add trend arrows**: If comparing to previous seasons, add arrows showing direction of movement 4. **Include contextual data**: Add tooltip information (in interactive versions) showing record, playoff status, or key players 5. **Annotate specific teams**: Add text labels for relevant teams ("Last year's Super Bowl winner", "Next opponent", "Division leader")Executive Dashboard Example
While static visualizations are useful for presentations and reports, modern football operations increasingly rely on interactive dashboards for real-time decision support. Dashboards allow decision-makers to explore data themselves, filter by specific criteria, and update automatically as new data arrives.
A well-designed dashboard serves multiple purposes in football organizations. It provides weekly performance summaries for coaches, tracks season-long trends for strategic planning, enables self-service analytics so coaches can answer their own questions, and creates a shared understanding of performance metrics across the organization.
The key difference between a dashboard and a static report is interactivity. Coaches want to filter to specific weeks, compare different time periods, drill down into specific situations (e.g., "Show me only red zone plays on third down"), and export specific views for their position groups. A thoughtfully designed dashboard enables all this without requiring analytics staff to create dozens of custom reports.
Here's a comprehensive weekly dashboard design that balances simplicity with depth:
#| label: weekly-dashboard
#| message: false
#| warning: false
#| fig-width: 14
#| fig-height: 10
#| fig-cap: "Comprehensive weekly performance dashboard showing offensive efficiency across multiple dimensions"
# Load required libraries for dashboard creation
library(tidyverse) # Data manipulation
library(nflfastR) # NFL data
library(nflplotR) # Team logos
library(gt) # Professional tables
library(gtExtras) # Enhanced table formatting
library(patchwork) # Combine multiple plots
# Load current season data
# In production, this would refresh automatically
pbp <- load_pbp(2023)
# Define team and opponent for this week's matchup
# In a real dashboard, these would be user-selectable parameters
team_abbr <- "KC"
opponent_abbr <- "PHI"
# Calculate team statistics for the season to date
# Filter to first 5 weeks as an example
team_stats <- pbp %>%
filter(week <= 5, !is.na(epa)) %>%
group_by(posteam) %>%
summarise(
plays = n(), # Total offensive plays
epa_play = mean(epa), # Average EPA per play
success_rate = mean(epa > 0), # % of plays with positive EPA
explosive_rate = mean(epa > 1), # % of plays with >1.0 EPA (explosive)
.groups = "drop"
) %>%
arrange(desc(epa_play)) # Sort by efficiency
# Create key metrics comparison table
# Compares this week's matchup teams on critical offensive metrics
key_metrics <- team_stats %>%
# Filter to only the two teams playing this week
filter(posteam %in% c(team_abbr, opponent_abbr)) %>%
# Calculate league rankings for context
mutate(
rank_epa = rank(-epa_play),
rank_success = rank(-success_rate)
) %>%
# Select and rename columns for presentation
select(Team = posteam,
`EPA/Play` = epa_play,
`Success Rate` = success_rate,
`Explosive Rate` = explosive_rate) %>%
# Create professional table using gt package
gt() %>%
# Add informative header
tab_header(
title = "Team Comparison: Week 6 Matchup",
subtitle = paste(team_abbr, "vs", opponent_abbr)
) %>%
# Format EPA with 3 decimal places
fmt_number(
columns = c(`EPA/Play`),
decimals = 3
) %>%
# Format percentages with 1 decimal place
fmt_percent(
columns = c(`Success Rate`, `Explosive Rate`),
decimals = 1
) %>%
# Add conditional coloring to highlight better/worse performance
# Green for good, red for poor, white for average
data_color(
columns = c(`EPA/Play`, `Success Rate`),
colors = scales::col_numeric(
palette = c("red", "white", "darkgreen"),
domain = NULL
)
) %>%
# Style the table for readability
tab_options(
table.font.size = 14,
heading.title.font.size = 18,
heading.subtitle.font.size = 14
)
# Create EPA trend plot showing weekly offensive performance
# Helps identify consistency and trends over the season
epa_trend <- pbp %>%
filter(posteam == team_abbr, week <= 5, !is.na(epa)) %>%
group_by(week) %>%
summarise(epa_play = mean(epa), .groups = "drop") %>%
ggplot(aes(x = week, y = epa_play)) +
# Line connects weekly points to show trend
geom_line(color = "steelblue", size = 1.5, linewidth = 1.5) +
# Points emphasize each week's performance
geom_point(size = 4, color = "steelblue") +
# Reference line at zero (neutral performance)
geom_hline(yintercept = 0, linetype = "dashed", alpha = 0.5) +
# Set x-axis to show each week clearly
scale_x_continuous(breaks = 1:5) +
labs(
title = paste(team_abbr, "Offensive Efficiency Trend"),
x = "Week",
y = "EPA per Play"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 12),
panel.grid.minor = element_blank()
)
# Analyze success rate by down
# Critical for understanding down-specific efficiency
success_by_down <- pbp %>%
filter(posteam == team_abbr, week <= 5, !is.na(epa), !is.na(down)) %>%
# Focus on first three downs (4th down is analyzed separately)
group_by(down) %>%
summarise(
success_rate = mean(epa > 0), # % of plays that gain EPA
plays = n(), # Sample size for context
.groups = "drop"
) %>%
# Filter to standard downs
filter(down <= 3) %>%
ggplot(aes(x = factor(down), y = success_rate)) +
# Bar chart makes comparison easy
geom_col(fill = "steelblue", alpha = 0.8) +
# Add percentage labels on bars for precise values
geom_text(aes(label = scales::percent(success_rate, accuracy = 1)),
vjust = -0.5, fontface = "bold", size = 4) +
# Format y-axis as percentages
scale_y_continuous(labels = scales::percent, limits = c(0, 0.7)) +
labs(
title = "Success Rate by Down",
x = "Down",
y = "Success Rate"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 12),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank() # Remove vertical grid lines
)
# Analyze play type distribution
# Shows offensive balance between pass and run
play_dist <- pbp %>%
filter(posteam == team_abbr, week <= 5,
play_type %in% c("pass", "run")) %>%
count(play_type) %>%
mutate(pct = n / sum(n)) %>% # Calculate percentages
ggplot(aes(x = "", y = pct, fill = play_type)) +
# Pie chart created by stacking bars and using polar coordinates
geom_col(width = 1, color = "white", size = 1) +
coord_polar("y") + # Convert to circular layout
# Use distinct colors for pass vs run
scale_fill_manual(
values = c("pass" = "#00BFC4", "run" = "#F8766D"),
labels = c("Pass", "Run")
) +
# Add percentage labels on pie slices
geom_text(aes(label = scales::percent(pct, accuracy = 1)),
position = position_stack(vjust = 0.5),
fontface = "bold", size = 5, color = "white") +
labs(
title = "Play Type Distribution",
fill = "Play Type"
) +
# Remove all axes and backgrounds for clean pie chart
theme_void() +
theme(
plot.title = element_text(face = "bold", size = 12, hjust = 0.5),
legend.position = "bottom"
)
# Combine all plots into cohesive dashboard layout
# patchwork syntax: | means side-by-side, / means stacked
combined_plots <- (epa_trend | success_by_down) / play_dist +
# Add overall title to entire dashboard
plot_annotation(
title = paste(team_abbr, "Weekly Performance Dashboard - Week 6"),
subtitle = "First 5 Weeks of 2023 Season",
theme = theme(
plot.title = element_text(size = 16, face = "bold"),
plot.subtitle = element_text(size = 12)
)
)
# Display the combined dashboard
print(combined_plots)
📊 Visualization Output
The code above generates a visualization. To see the output, run this code in your R or Python environment. The resulting plot will help illustrate the concepts discussed in this section.
#| label: weekly-dashboard-py
#| message: false
#| warning: false
#| fig-width: 14
#| fig-height: 10
#| fig-cap: "Weekly performance dashboard showing multiple dimensions of offensive efficiency"
# Load required libraries
import pandas as pd
import numpy as np
import nfl_data_py as nfl
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
# Load play-by-play data for 2023 season
pbp = nfl.import_pbp_data([2023])
# Define team and opponent for analysis
team_abbr = "KC"
opponent_abbr = "PHI"
# Create figure with custom subplot layout using GridSpec
# GridSpec allows flexible arrangement of multiple plots
fig = plt.figure(figsize=(14, 10))
gs = GridSpec(3, 2, figure=fig, hspace=0.3, wspace=0.3)
# Add overall title to dashboard
fig.suptitle(f'{team_abbr} Weekly Performance Dashboard - Week 6\nFirst 5 Weeks of 2023 Season',
fontsize=16, fontweight='bold', y=0.98)
# Plot 1: EPA Trend Over Time
# Shows offensive efficiency trajectory across the season
ax1 = fig.add_subplot(gs[0, :]) # Spans full width of top row
epa_trend = (pbp
# Filter to team's offensive plays through week 5
.query(f"posteam == '{team_abbr}' & week <= 5 & epa.notna()")
# Calculate average EPA per week
.groupby('week')['epa']
.mean()
.reset_index()
.rename(columns={'epa': 'epa_play'}))
# Create line plot with markers
ax1.plot(epa_trend['week'], epa_trend['epa_play'],
marker='o', linewidth=2.5, markersize=8, color='steelblue')
# Add reference line at neutral performance (0 EPA)
ax1.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
ax1.set_xlabel('Week', fontsize=11)
ax1.set_ylabel('EPA per Play', fontsize=11)
ax1.set_title(f'{team_abbr} Offensive Efficiency Trend',
fontweight='bold', fontsize=12)
ax1.grid(True, alpha=0.3)
# Set x-axis to show each week
ax1.set_xticks(range(1, 6))
# Plot 2: Success Rate by Down
# Shows effectiveness on each down (1st, 2nd, 3rd)
ax2 = fig.add_subplot(gs[1, 0]) # Left plot in middle row
success_by_down = (pbp
# Filter to team's plays with valid down information
.query(f"posteam == '{team_abbr}' & week <= 5 & epa.notna() & down.notna() & down <= 3")
.groupby('down')
# Calculate success rate (% of plays with positive EPA)
.agg(success_rate=('epa', lambda x: (x > 0).mean()))
.reset_index())
# Create bar chart
bars = ax2.bar(success_by_down['down'], success_by_down['success_rate'],
color='steelblue', alpha=0.8)
ax2.set_xlabel('Down', fontsize=11)
ax2.set_ylabel('Success Rate', fontsize=11)
ax2.set_title('Success Rate by Down', fontweight='bold', fontsize=12)
ax2.set_ylim(0, 0.7)
# Format y-axis as percentages
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f'{y:.0%}'))
# Add percentage labels on top of bars
for bar in bars:
height = bar.get_height()
ax2.text(bar.get_x() + bar.get_width()/2., height,
f'{height:.1%}', ha='center', va='bottom', fontweight='bold')
# Plot 3: Play Type Distribution
# Shows offensive balance between passing and running
ax3 = fig.add_subplot(gs[1, 1]) # Right plot in middle row
play_dist = (pbp
# Filter to pass and run plays only
.query(f"posteam == '{team_abbr}' & week <= 5 & play_type.isin(['pass', 'run'])")
.groupby('play_type')
.size()
.reset_index(name='count'))
# Calculate percentages
play_dist['pct'] = play_dist['count'] / play_dist['count'].sum()
# Define colors for each play type
colors = {'pass': '#00BFC4', 'run': '#F8766D'}
# Create pie chart
wedges, texts, autotexts = ax3.pie(
play_dist['pct'],
labels=['Pass', 'Run'],
autopct='%1.1f%%', # Show percentages
colors=[colors[pt] for pt in play_dist['play_type']],
startangle=90, # Start at top
textprops={'fontweight': 'bold', 'fontsize': 11}
)
ax3.set_title('Play Type Distribution', fontweight='bold', fontsize=12)
# Plot 4: Team Comparison Table
# Shows key metrics for both teams in this week's matchup
ax4 = fig.add_subplot(gs[2, :]) # Spans full width of bottom row
ax4.axis('tight') # Remove axes
ax4.axis('off') # Hide axis labels
# Calculate team statistics
team_stats = (pbp
.query("week <= 5 & epa.notna() & posteam.isin(@[team_abbr, opponent_abbr])")
.groupby('posteam')
.agg(
epa_play=('epa', 'mean'), # Average EPA
success_rate=('epa', lambda x: (x > 0).mean()), # Success rate
explosive_rate=('epa', lambda x: (x > 1).mean()) # Explosive play rate
)
.reset_index()
.rename(columns={'posteam': 'Team'}))
# Format data for table display
team_stats_display = team_stats.copy()
team_stats_display['epa_play'] = team_stats_display['epa_play'].apply(lambda x: f'{x:.3f}')
team_stats_display['success_rate'] = team_stats_display['success_rate'].apply(lambda x: f'{x:.1%}')
team_stats_display['explosive_rate'] = team_stats_display['explosive_rate'].apply(lambda x: f'{x:.1%}')
# Create professional table
table = ax4.table(
cellText=team_stats_display.values,
colLabels=['Team', 'EPA/Play', 'Success Rate', 'Explosive Rate'],
cellLoc='center',
loc='center',
colWidths=[0.15, 0.25, 0.25, 0.25]
)
# Style the table
table.auto_set_font_size(False)
table.set_fontsize(11)
table.scale(1, 2.5) # Make rows taller for readability
# Style header row with color
for i in range(4):
table[(0, i)].set_facecolor('#4CAF50')
table[(0, i)].set_text_props(weight='bold', color='white')
# Add alternating row colors for readability
for i in range(1, len(team_stats_display) + 1):
for j in range(4):
if i % 2 == 0:
table[(i, j)].set_facecolor('#f0f0f0')
# Finalize layout and display
plt.tight_layout()
plt.show()
📊 Visualization Output
The code above generates a visualization. To see the output, run this code in your R or Python environment. The resulting plot will help illustrate the concepts discussed in this section.
This dashboard transforms raw play-by-play data into actionable intelligence. Instead of overwhelming coaches with 40,000 rows of data, it distills the essential patterns into four complementary visualizations that can be absorbed in under two minutes. This is the power of effective analytics communication—making complexity accessible without sacrificing insight.
Dashboard Design Checklist
When building dashboards for football decision-makers, ensure you meet these criteria: **Content:** - [ ] Shows the 3-5 most important metrics (not everything possible) - [ ] Provides context through league averages, historical trends, or peer comparisons - [ ] Highlights actionable insights prominently - [ ] Updates automatically or with minimal manual effort - [ ] Filters to relevant time periods and situations **Design:** - [ ] Clean, uncluttered layout with clear visual hierarchy - [ ] Consistent color scheme, preferably using team colors - [ ] Readable on mobile devices and tablets - [ ] Prints clearly in black and white for handouts - [ ] Uses appropriate chart types for each data relationship - [ ] Limits to 4-6 visualizations per page (more causes overload) **Usability:** - [ ] Can be understood in under 30 seconds by first-time viewers - [ ] Interactive elements (filters, drill-downs) are intuitive - [ ] Data sources are clearly cited for credibility - [ ] Last updated timestamp is visible - [ ] Download or export functionality available - [ ] Mobile-responsive for coaches traveling or in stadium boxes **Football-specific:** - [ ] Uses football terminology, not statistics jargon - [ ] Aligns with coaching staff's decision timeline (weekly, daily, in-game) - [ ] Addresses questions coaches actually ask - [ ] Integrates with existing tools (film software, practice planning systems) - [ ] Respects organizational culture and communication normsDashboard Design for Football Operations
Modern football operations increasingly rely on interactive dashboards for real-time decision support. Unlike static reports that provide a snapshot in time, dashboards enable continuous monitoring, self-service exploration, and dynamic filtering. A well-designed dashboard becomes a critical tool in the analytics workflow, used daily by coaches, weekly by executives, and throughout the year for strategic planning.
The transition from static reports to interactive dashboards represents a fundamental shift in how analytics integrates with football operations. Instead of analysts producing dozens of custom reports for every request, decision-makers can explore data themselves. This scales analytics impact, reduces bottlenecks, and empowers coaches to answer their own questions in real-time.
This section demonstrates how to build production-quality dashboards using Shiny (R) and Streamlit (Python)—the two most popular frameworks for analytics dashboards. Both frameworks allow you to build sophisticated, interactive applications with minimal web development expertise.
Executive Dashboard with Shiny
Shiny is R's premier framework for building interactive web applications. It's particularly popular in sports analytics because it integrates seamlessly with the tidyverse ecosystem and nflplotR visualizations. Many NFL teams use Shiny dashboards for internal analytics tools.
The dashboard we'll build includes:
- Team selection and week range filters
- Key performance metrics with league rankings
- Multiple tabs for different analysis perspectives (trends, situational, play types, league comparison)
- Interactive plotly charts that respond to user selections
- Professional styling appropriate for executive presentation
#| label: shiny-dashboard
#| eval: false
#| echo: true
# This is a complete Shiny application for NFL team performance analysis
# Save this code to a file named "app.R" and run with shiny::runApp()
library(shiny)
library(tidyverse)
library(nflfastR)
library(nflplotR)
library(gt)
library(plotly)
# ===========================
# USER INTERFACE DEFINITION
# ===========================
# This defines what the user sees and interacts with
ui <- fluidPage(
# Application title
titlePanel("NFL Team Performance Dashboard"),
# Sidebar layout splits page into controls (sidebar) and content (main)
sidebarLayout(
# Sidebar panel with user inputs
sidebarPanel(
width = 3, # Narrow sidebar to maximize chart space
# Team selection dropdown
# Choices populated dynamically from data in server
selectInput(
"team",
"Select Team:",
choices = NULL, # Will be populated by server
selected = "KC"
),
# Week range slider for filtering time period
# Allows users to focus on specific parts of season
sliderInput(
"weeks",
"Week Range:",
min = 1,
max = 18,
value = c(1, 5), # Default to first 5 weeks
step = 1
),
# Primary metric selection for main visualizations
selectInput(
"metric",
"Primary Metric:",
choices = c(
"EPA per Play" = "epa",
"Success Rate" = "success_rate",
"Explosive Play Rate" = "explosive_rate",
"Points per Drive" = "ppd"
),
selected = "epa"
),
hr(), # Horizontal line separator
# Display last data update time
textOutput("last_updated")
),
# Main panel with dashboard content
mainPanel(
width = 9,
# Top row: Key metrics summary boxes
# These provide at-a-glance performance indicators
fluidRow(
column(3,
div(class = "metric-box",
h4("EPA/Play"),
h2(textOutput("epa_value")),
p(textOutput("epa_rank"))
)
),
column(3,
div(class = "metric-box",
h4("Success Rate"),
h2(textOutput("success_value")),
p(textOutput("success_rank"))
)
),
column(3,
div(class = "metric-box",
h4("Explosive %"),
h2(textOutput("explosive_value")),
p(textOutput("explosive_rank"))
)
),
column(3,
div(class = "metric-box",
h4("Total Plays"),
h2(textOutput("total_plays")),
p(textOutput("plays_rank"))
)
)
),
br(),
# Tabbed interface for different analysis views
# Allows organizing complex information without overwhelming users
tabsetPanel(
type = "tabs",
# Tab 1: Performance trends over time
tabPanel(
"Trends",
br(),
plotlyOutput("trend_plot", height = "400px"),
br(),
plotlyOutput("weekly_comparison", height = "400px")
),
# Tab 2: Situational performance breakdown
tabPanel(
"Situational",
br(),
fluidRow(
column(6, plotOutput("down_plot", height = "350px")),
column(6, plotOutput("distance_plot", height = "350px"))
),
br(),
fluidRow(
column(6, plotOutput("quarter_plot", height = "350px")),
column(6, plotOutput("field_position_plot", height = "350px"))
)
),
# Tab 3: Play type analysis
tabPanel(
"Play Types",
br(),
fluidRow(
column(6, plotOutput("playtype_plot", height = "350px")),
column(6, plotOutput("playtype_epa", height = "350px"))
),
br(),
gt_output("playtype_table")
),
# Tab 4: League-wide comparison
tabPanel(
"League Comparison",
br(),
plotOutput("league_scatter", height = "600px"),
br(),
gt_output("league_table")
)
)
)
),
# Custom CSS styling for professional appearance
tags$head(
tags$style(HTML("
.metric-box {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
text-align: center;
margin-bottom: 10px;
}
.metric-box h4 {
margin-top: 0;
color: #6c757d;
font-size: 14px;
font-weight: 600;
}
.metric-box h2 {
margin: 10px 0;
color: #212529;
font-weight: bold;
font-size: 28px;
}
.metric-box p {
margin-bottom: 0;
color: #6c757d;
font-size: 12px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
"))
)
)
# ===========================
# SERVER LOGIC DEFINITION
# ===========================
# This defines how the app responds to user interactions
server <- function(input, output, session) {
# Load data reactively
# Reactive means it only recalculates when dependencies change
pbp <- reactive({
load_pbp(2023) # In production, make season selectable
})
# Populate team choices after data loads
observe({
teams <- sort(unique(pbp()$posteam[!is.na(pbp()$posteam)]))
updateSelectInput(session, "team", choices = teams)
})
# Filter data based on user selections
# This reactive updates whenever team or week range changes
team_data <- reactive({
req(input$team) # Ensure team is selected before proceeding
pbp() %>%
filter(
posteam == input$team,
week >= input$weeks[1],
week <= input$weeks[2],
!is.na(epa)
)
})
# Calculate key metrics for selected team
team_metrics <- reactive({
team_data() %>%
summarise(
epa_play = mean(epa),
success_rate = mean(epa > 0),
explosive_rate = mean(epa > 1),
total_plays = n()
)
})
# Calculate league-wide metrics for ranking context
league_metrics <- reactive({
pbp() %>%
filter(
week >= input$weeks[1],
week <= input$weeks[2],
!is.na(epa),
!is.na(posteam)
) %>%
group_by(posteam) %>%
summarise(
epa_play = mean(epa),
success_rate = mean(epa > 0),
explosive_rate = mean(epa > 1),
total_plays = n(),
.groups = "drop"
) %>%
mutate(
# Calculate rankings (lower rank number = better performance)
epa_rank = rank(-epa_play),
success_rank = rank(-success_rate),
explosive_rank = rank(-explosive_rate),
plays_rank = rank(-total_plays)
)
})
# ===========================
# METRIC BOX OUTPUTS
# ===========================
# EPA/Play value
output$epa_value <- renderText({
sprintf("%.3f", team_metrics()$epa_play)
})
# EPA rank among all teams
output$epa_rank <- renderText({
rank <- league_metrics() %>%
filter(posteam == input$team) %>%
pull(epa_rank)
paste("Rank:", rank, "of 32")
})
# Success Rate value
output$success_value <- renderText({
sprintf("%.1f%%", team_metrics()$success_rate * 100)
})
# Success Rate rank
output$success_rank <- renderText({
rank <- league_metrics() %>%
filter(posteam == input$team) %>%
pull(success_rank)
paste("Rank:", rank, "of 32")
})
# Explosive Play Rate value
output$explosive_value <- renderText({
sprintf("%.1f%%", team_metrics()$explosive_rate * 100)
})
# Explosive Play Rate rank
output$explosive_rank <- renderText({
rank <- league_metrics() %>%
filter(posteam == input$team) %>%
pull(explosive_rank)
paste("Rank:", rank, "of 32")
})
# Total Plays count
output$total_plays <- renderText({
format(team_metrics()$total_plays, big.mark = ",")
})
# Total Plays rank
output$plays_rank <- renderText({
rank <- league_metrics() %>%
filter(posteam == input$team) %>%
pull(plays_rank)
paste("Rank:", rank, "of 32")
})
# ===========================
# VISUALIZATION OUTPUTS
# ===========================
# Interactive trend plot using plotly
# Plotly adds hover tooltips and zoom/pan capabilities
output$trend_plot <- renderPlotly({
weekly_data <- team_data() %>%
group_by(week) %>%
summarise(
epa_play = mean(epa),
success_rate = mean(epa > 0),
explosive_rate = mean(epa > 1),
.groups = "drop"
)
# Create plotly line chart
plot_ly(weekly_data, x = ~week) %>%
add_lines(y = ~epa_play, name = "EPA/Play",
line = list(color = "steelblue", width = 3)) %>%
add_markers(y = ~epa_play, name = "EPA/Play",
marker = list(color = "steelblue", size = 8),
showlegend = FALSE) %>%
layout(
title = paste(input$team, "Weekly Trend"),
xaxis = list(title = "Week"),
yaxis = list(title = "EPA per Play"),
hovermode = "x unified" # Shows all metrics for a week on hover
)
})
# Success rate by down plot
output$down_plot <- renderPlot({
team_data() %>%
filter(!is.na(down), down <= 4) %>%
group_by(down) %>%
summarise(
success_rate = mean(epa > 0),
.groups = "drop"
) %>%
ggplot(aes(x = factor(down), y = success_rate)) +
geom_col(fill = "steelblue", alpha = 0.8) +
geom_text(aes(label = scales::percent(success_rate, accuracy = 0.1)),
vjust = -0.5, fontface = "bold") +
scale_y_continuous(labels = scales::percent, limits = c(0, 1)) +
labs(
title = "Success Rate by Down",
x = "Down",
y = "Success Rate"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid.major.x = element_blank()
)
})
# League comparison scatter plot
# Shows selected team's position relative to all NFL teams
output$league_scatter <- renderPlot({
# Calculate offensive EPA by team
league_data <- pbp() %>%
filter(
week >= input$weeks[1],
week <= input$weeks[2],
!is.na(epa),
!is.na(posteam)
) %>%
group_by(posteam) %>%
summarise(
off_epa = mean(epa),
.groups = "drop"
)
# Calculate defensive EPA by team
def_data <- pbp() %>%
filter(
week >= input$weeks[1],
week <= input$weeks[2],
!is.na(epa),
!is.na(defteam)
) %>%
group_by(defteam) %>%
summarise(
def_epa = -mean(epa), # Negate so higher is better
.groups = "drop"
) %>%
rename(posteam = defteam)
# Combine offense and defense
combined <- league_data %>%
left_join(def_data, by = "posteam")
# Create scatter plot with team logos
ggplot(combined, aes(x = off_epa, y = def_epa)) +
# Add quadrant lines at league average
geom_hline(yintercept = mean(combined$def_epa),
linetype = "dashed", alpha = 0.5) +
geom_vline(xintercept = mean(combined$off_epa),
linetype = "dashed", alpha = 0.5) +
# Plot all team logos
geom_nfl_logos(aes(team_abbr = posteam), width = 0.06, alpha = 0.7) +
# Highlight selected team with larger logo
geom_nfl_logos(
data = combined %>% filter(posteam == input$team),
aes(team_abbr = posteam),
width = 0.08,
alpha = 1
) +
labs(
title = "League-Wide Team Performance",
subtitle = paste("Weeks", input$weeks[1], "-", input$weeks[2]),
x = "Offensive EPA/Play (Better →)",
y = "Defensive EPA/Play (Better →)"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 16),
plot.subtitle = element_text(size = 12)
)
})
# Last updated timestamp
output$last_updated <- renderText({
paste("Last updated:", format(Sys.time(), "%Y-%m-%d %H:%M"))
})
}
# ===========================
# RUN THE APPLICATION
# ===========================
shinyApp(ui = ui, server = server)
#| label: streamlit-dashboard-py
#| eval: false
#| echo: true
# This is a complete Streamlit application for NFL team performance analysis
# Save this code to a file named "dashboard.py" and run with: streamlit run dashboard.py
import streamlit as st
import pandas as pd
import numpy as np
import nfl_data_py as nfl
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime
# ===========================
# PAGE CONFIGURATION
# ===========================
# Must be first Streamlit command
st.set_page_config(
page_title="NFL Team Performance Dashboard",
page_icon="🏈",
layout="wide", # Use full screen width
initial_sidebar_state="expanded"
)
# ===========================
# DATA LOADING
# ===========================
# Cache data loading to avoid reloading on every interaction
# Streamlit re-runs the entire script on each interaction, so caching is critical
@st.cache_data
def load_data(season):
"""Load play-by-play data with caching for performance"""
return nfl.import_pbp_data([season])
# Load 2023 season data
pbp = load_data(2023)
# ===========================
# SIDEBAR CONTROLS
# ===========================
st.sidebar.header("Filters")
# Team selection dropdown
# Get unique teams from data and sort alphabetically
teams = sorted(pbp['posteam'].dropna().unique())
selected_team = st.sidebar.selectbox(
"Select Team",
teams,
index=teams.index('KC') if 'KC' in teams else 0
)
# Week range slider
week_range = st.sidebar.slider(
"Week Range",
min_value=1,
max_value=18,
value=(1, 5), # Default to first 5 weeks
step=1
)
# Metric selection for primary analysis
metric_options = {
"EPA per Play": "epa",
"Success Rate": "success_rate",
"Explosive Play Rate": "explosive_rate"
}
selected_metric = st.sidebar.selectbox(
"Primary Metric",
list(metric_options.keys())
)
# Divider and metadata
st.sidebar.markdown("---")
st.sidebar.caption(f"Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# ===========================
# MAIN CONTENT
# ===========================
# Application title
st.title("🏈 NFL Team Performance Dashboard")
# ===========================
# DATA FILTERING
# ===========================
# Filter data based on user selections
team_data = pbp[
(pbp['posteam'] == selected_team) &
(pbp['week'] >= week_range[0]) &
(pbp['week'] <= week_range[1]) &
(pbp['epa'].notna())
].copy()
# ===========================
# METRIC CALCULATIONS
# ===========================
def calculate_metrics(data):
"""Calculate key performance metrics"""
return {
'epa_play': data['epa'].mean(),
'success_rate': (data['epa'] > 0).mean(),
'explosive_rate': (data['epa'] > 1).mean(),
'total_plays': len(data)
}
# Calculate metrics for selected team
team_metrics = calculate_metrics(team_data)
# Calculate league-wide metrics for ranking context
league_data = pbp[
(pbp['week'] >= week_range[0]) &
(pbp['week'] <= week_range[1]) &
(pbp['epa'].notna()) &
(pbp['posteam'].notna())
].groupby('posteam').apply(
lambda x: pd.Series(calculate_metrics(x))
).reset_index()
# Calculate rankings
league_data['epa_rank'] = league_data['epa_play'].rank(ascending=False)
league_data['success_rank'] = league_data['success_rate'].rank(ascending=False)
league_data['explosive_rank'] = league_data['explosive_rate'].rank(ascending=False)
# Get selected team's ranks
team_ranks = league_data[league_data['posteam'] == selected_team].iloc[0]
# ===========================
# KEY METRICS ROW
# ===========================
# Display key metrics in columns across top of page
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(
"EPA/Play",
f"{team_metrics['epa_play']:.3f}",
delta=f"Rank: {int(team_ranks['epa_rank'])} of {len(league_data)}",
delta_color="off" # Don't color-code rank changes
)
with col2:
st.metric(
"Success Rate",
f"{team_metrics['success_rate']:.1%}",
delta=f"Rank: {int(team_ranks['success_rank'])} of {len(league_data)}",
delta_color="off"
)
with col3:
st.metric(
"Explosive %",
f"{team_metrics['explosive_rate']:.1%}",
delta=f"Rank: {int(team_ranks['explosive_rank'])} of {len(league_data)}",
delta_color="off"
)
with col4:
st.metric(
"Total Plays",
f"{team_metrics['total_plays']:,}",
delta=f"{team_metrics['total_plays'] / (week_range[1] - week_range[0] + 1):.0f} per week",
delta_color="off"
)
# Divider before tabbed content
st.markdown("---")
# ===========================
# TABBED CONTENT
# ===========================
# Create tabs for different analysis views
tab1, tab2, tab3, tab4 = st.tabs([
"📈 Trends",
"🎯 Situational",
"🏈 Play Types",
"📊 League Comparison"
])
# TAB 1: PERFORMANCE TRENDS
with tab1:
st.subheader("Weekly Performance Trends")
# Calculate weekly aggregates
weekly_data = team_data.groupby('week').agg({
'epa': ['mean', lambda x: (x > 0).mean(), lambda x: (x > 1).mean()]
}).reset_index()
weekly_data.columns = ['week', 'epa_play', 'success_rate', 'explosive_rate']
# Create interactive line chart
fig = go.Figure()
fig.add_trace(go.Scatter(
x=weekly_data['week'],
y=weekly_data['epa_play'],
mode='lines+markers',
name='EPA/Play',
line=dict(color='steelblue', width=3),
marker=dict(size=10)
))
fig.update_layout(
title=f"{selected_team} Weekly EPA Trend",
xaxis_title="Week",
yaxis_title="EPA per Play",
hovermode='x unified',
height=400
)
st.plotly_chart(fig, use_container_width=True)
# Success and Explosive rates side-by-side
col1, col2 = st.columns(2)
with col1:
fig_success = px.bar(
weekly_data,
x='week',
y='success_rate',
title='Weekly Success Rate',
labels={'success_rate': 'Success Rate', 'week': 'Week'}
)
fig_success.update_traces(marker_color='steelblue')
fig_success.update_yaxes(tickformat='.0%')
st.plotly_chart(fig_success, use_container_width=True)
with col2:
fig_explosive = px.bar(
weekly_data,
x='week',
y='explosive_rate',
title='Weekly Explosive Play Rate',
labels={'explosive_rate': 'Explosive Rate', 'week': 'Week'}
)
fig_explosive.update_traces(marker_color='coral')
fig_explosive.update_yaxes(tickformat='.0%')
st.plotly_chart(fig_explosive, use_container_width=True)
# TAB 2: SITUATIONAL PERFORMANCE
with tab2:
st.subheader("Situational Performance")
col1, col2 = st.columns(2)
with col1:
# Performance by down
down_data = team_data[
team_data['down'].notna() & (team_data['down'] <= 4)
].groupby('down').agg({
'epa': ['mean', lambda x: (x > 0).mean(), 'count']
}).reset_index()
down_data.columns = ['down', 'epa_play', 'success_rate', 'plays']
fig_down = px.bar(
down_data,
x='down',
y='success_rate',
title='Success Rate by Down',
text=down_data['success_rate'].apply(lambda x: f'{x:.1%}'),
labels={'success_rate': 'Success Rate', 'down': 'Down'}
)
fig_down.update_traces(marker_color='steelblue', textposition='outside')
fig_down.update_yaxes(tickformat='.0%')
st.plotly_chart(fig_down, use_container_width=True)
with col2:
# Performance by quarter
quarter_data = team_data[team_data['qtr'].notna()].groupby('qtr').agg({
'epa': ['mean', lambda x: (x > 0).mean(), 'count']
}).reset_index()
quarter_data.columns = ['qtr', 'epa_play', 'success_rate', 'plays']
fig_qtr = px.bar(
quarter_data,
x='qtr',
y='epa_play',
title='EPA/Play by Quarter',
text=quarter_data['epa_play'].apply(lambda x: f'{x:.2f}'),
labels={'epa_play': 'EPA/Play', 'qtr': 'Quarter'}
)
fig_qtr.update_traces(marker_color='coral', textposition='outside')
st.plotly_chart(fig_qtr, use_container_width=True)
# Field position analysis
team_data['yardline_100_bin'] = pd.cut(
team_data['yardline_100'],
bins=[0, 20, 40, 60, 80, 100],
labels=['Own 20', 'Own 40', 'Midfield', 'Opp 40', 'Opp 20']
)
field_data = team_data.groupby('yardline_100_bin').agg({
'epa': ['mean', 'count']
}).reset_index()
field_data.columns = ['field_position', 'epa_play', 'plays']
fig_field = px.bar(
field_data,
x='field_position',
y='epa_play',
title='EPA/Play by Field Position',
text=field_data['epa_play'].apply(lambda x: f'{x:.2f}'),
labels={'epa_play': 'EPA/Play', 'field_position': 'Field Position'}
)
fig_field.update_traces(marker_color='green', textposition='outside')
st.plotly_chart(fig_field, use_container_width=True)
# TAB 3: PLAY TYPE ANALYSIS
with tab3:
st.subheader("Play Type Analysis")
# Calculate play type statistics
playtype_data = team_data[
team_data['play_type'].isin(['pass', 'run'])
].groupby('play_type').agg({
'epa': ['mean', 'count', lambda x: (x > 0).mean(), lambda x: (x > 1).mean()]
}).reset_index()
playtype_data.columns = ['play_type', 'epa_play', 'plays', 'success_rate', 'explosive_rate']
playtype_data['pct'] = playtype_data['plays'] / playtype_data['plays'].sum()
col1, col2 = st.columns(2)
with col1:
# Pie chart of play distribution
fig_pie = px.pie(
playtype_data,
values='plays',
names='play_type',
title='Play Type Distribution',
color='play_type',
color_discrete_map={'pass': '#00BFC4', 'run': '#F8766D'}
)
st.plotly_chart(fig_pie, use_container_width=True)
with col2:
# EPA comparison by play type
fig_epa = px.bar(
playtype_data,
x='play_type',
y='epa_play',
title='EPA/Play by Play Type',
text=playtype_data['epa_play'].apply(lambda x: f'{x:.3f}'),
color='play_type',
color_discrete_map={'pass': '#00BFC4', 'run': '#F8766D'}
)
fig_epa.update_traces(textposition='outside', showlegend=False)
st.plotly_chart(fig_epa, use_container_width=True)
# Detailed table
st.subheader("Play Type Details")
playtype_display = playtype_data.copy()
playtype_display['Play Type'] = playtype_display['play_type'].str.title()
playtype_display['Plays'] = playtype_display['plays']
playtype_display['%'] = playtype_display['pct'].apply(lambda x: f'{x:.1%}')
playtype_display['EPA/Play'] = playtype_display['epa_play'].apply(lambda x: f'{x:.3f}')
playtype_display['Success Rate'] = playtype_display['success_rate'].apply(lambda x: f'{x:.1%}')
playtype_display['Explosive Rate'] = playtype_display['explosive_rate'].apply(lambda x: f'{x:.1%}')
st.dataframe(
playtype_display[['Play Type', 'Plays', '%', 'EPA/Play', 'Success Rate', 'Explosive Rate']],
hide_index=True,
use_container_width=True
)
# TAB 4: LEAGUE COMPARISON
with tab4:
st.subheader("League-Wide Comparison")
# Calculate offensive and defensive EPA for all teams
off_data = pbp[
(pbp['week'] >= week_range[0]) &
(pbp['week'] <= week_range[1]) &
(pbp['epa'].notna()) &
(pbp['posteam'].notna())
].groupby('posteam')['epa'].mean().reset_index()
off_data.columns = ['team', 'off_epa']
def_data = pbp[
(pbp['week'] >= week_range[0]) &
(pbp['week'] <= week_range[1]) &
(pbp['epa'].notna()) &
(pbp['defteam'].notna())
].groupby('defteam')['epa'].mean().apply(lambda x: -x).reset_index()
def_data.columns = ['team', 'def_epa']
# Merge offensive and defensive data
scatter_data = off_data.merge(def_data, on='team')
scatter_data['is_selected'] = scatter_data['team'] == selected_team
# Create scatter plot
fig_scatter = px.scatter(
scatter_data,
x='off_epa',
y='def_epa',
text='team',
title='Team Efficiency: Offense vs Defense',
labels={'off_epa': 'Offensive EPA/Play (Better →)',
'def_epa': 'Defensive EPA/Play (Better →)'},
color='is_selected',
color_discrete_map={True: 'red', False: 'steelblue'},
size='is_selected',
size_discrete_map={True: 15, False: 10}
)
# Add quadrant lines at league average
fig_scatter.add_hline(
y=scatter_data['def_epa'].mean(),
line_dash="dash",
line_color="gray",
opacity=0.5
)
fig_scatter.add_vline(
x=scatter_data['off_epa'].mean(),
line_dash="dash",
line_color="gray",
opacity=0.5
)
fig_scatter.update_traces(textposition='top center')
fig_scatter.update_layout(showlegend=False, height=600)
st.plotly_chart(fig_scatter, use_container_width=True)
# Rankings table
st.subheader("League Rankings")
league_table = league_data.copy()
league_table = league_table.sort_values('epa_play', ascending=False)
league_table['Rank'] = range(1, len(league_table) + 1)
league_table['Team'] = league_table['posteam']
league_table['EPA/Play'] = league_table['epa_play'].apply(lambda x: f'{x:.3f}')
league_table['Success Rate'] = league_table['success_rate'].apply(lambda x: f'{x:.1%}')
league_table['Explosive Rate'] = league_table['explosive_rate'].apply(lambda x: f'{x:.1%}')
league_table['Plays'] = league_table['total_plays'].astype(int)
# Highlight selected team
def highlight_row(row):
if row['Team'] == selected_team:
return ['background-color: #ffeb3b'] * len(row)
return [''] * len(row)
st.dataframe(
league_table[['Rank', 'Team', 'EPA/Play', 'Success Rate', 'Explosive Rate', 'Plays']],
hide_index=True,
use_container_width=True,
height=600
)
# ===========================
# FOOTER
# ===========================
st.markdown("---")
st.caption("Data source: nfl_data_py | Dashboard built with Streamlit")
These dashboards represent the state-of-the-art in football analytics communication. They transform static analysis into interactive tools that empower decision-makers to explore data on their own terms, answer their own questions, and develop data-informed intuitions that improve decision-making even without the dashboard present.
The investment in building these tools pays dividends throughout the season. Instead of analysts spending hours creating custom reports for every request, coaches can self-serve. Instead of presentations that quickly become outdated, dashboards automatically update. Instead of one-way communication (analyst to coach), dashboards enable two-way engagement where coaches ask questions and discover insights themselves.
Running the Dashboards
To deploy these dashboards in your organization, follow these implementation steps:
For the Shiny dashboard (R):
# 1. Install required packages (one-time setup)
install.packages(c("shiny", "tidyverse", "nflfastR", "nflplotR", "gt", "plotly"))
# 2. Save the Shiny code to a file named "app.R" in a dedicated folder
# Example: C:/analytics/team_dashboard/app.R
# 3. Run the dashboard locally for testing
shiny::runApp("C:/analytics/team_dashboard")
# 4. For production deployment, upload to RStudio Connect or Shiny Server
# This allows team members to access via web browser without running R
For the Streamlit dashboard (Python):
# 1. Install required packages (one-time setup)
pip install streamlit pandas numpy nfl-data-py plotly
# 2. Save the Streamlit code to a file named "dashboard.py"
# Example: C:/analytics/team_dashboard/dashboard.py
# 3. Run the dashboard locally for testing
streamlit run C:/analytics/team_dashboard/dashboard.py
# 4. For production deployment, use Streamlit Cloud or deploy to cloud server
# Streamlit automatically creates a shareable URL
Best practices for deployment:
- Test thoroughly with multiple users before organization-wide rollout
- Create a brief user guide with screenshots showing key features
- Hold training sessions for coaching staff to demonstrate capabilities
- Start with read-only dashboards before allowing data input or modifications
- Monitor usage analytics to identify which features get used (and which don't)
- Gather feedback regularly and iterate based on user needs
- Establish a process for requesting new features or reporting bugs
Security Considerations for Production Dashboards
When deploying dashboards that contain sensitive team information: 1. **Authentication**: Implement password protection or SSO integration 2. **Access levels**: Different roles (coaches, scouts, executives) may need different views 3. **Data privacy**: Ensure play-by-play data doesn't include proprietary internal metrics 4. **Secure hosting**: Use HTTPS and deploy behind VPN for remote access 5. **Audit logs**: Track who accesses what data and when 6. **Data retention**: Define policies for how long historical data remains accessible 7. **Compliance**: Ensure adherence to NFL's data usage policies and league regulations Never deploy dashboards with sensitive team information to public servers or share access credentials broadly. Treat analytics tools with the same security rigor as film systems and playbooks.Presenting Recommendations and Insights
Building dashboards and creating visualizations are important skills, but the highest-value communication moments in football analytics occur during presentations—when you're recommending specific strategic changes to coaches and executives. These presentations can influence millions of dollars in roster decisions, alter game plans, and ultimately impact win-loss records.
Presenting analytics recommendations requires a different skillset than building models or creating dashboards. You're not just sharing information; you're persuading decision-makers to change behavior, allocate resources differently, or adopt new strategies. This requires understanding human psychology, organizational dynamics, and the art of persuasion—all while maintaining analytical rigor and intellectual honesty.
This section outlines a proven structure for presenting analytics recommendations in football contexts, with specific examples and templates you can adapt for your organization.
Structure of an Effective Presentation
Effective analytics presentations follow a consistent structure that respects decision-makers' time while building a persuasive case. This structure has been refined through decades of organizational psychology research and sports analytics practice.
1. Executive Summary (1 slide, 30-60 seconds)
Start with the bottom line. Busy executives need to know immediately whether this presentation is worth their attention. Answer three questions upfront:
- What decision or question does this address? Connect to strategic priorities or known pain points
- What's the recommendation? Be specific and actionable, not vague
- What's the expected impact? Quantify in terms decision-makers care about (wins, points, cap savings)
Example executive summary slide:
RECOMMENDATION: Increase Fourth Down Attempts by 40%
- Current state: Going for it on 4th down 18% of the time (league: 25%)
- Recommendation: Increase to 25% (league average) using analytics-informed decision matrix
- Expected impact: +0.3 wins per season, +8-10 points across full season
- Implementation timeline: Weeks 1-2 training, Week 5 full deployment
2. Context and Motivation (1-2 slides, 1-2 minutes)
Explain why this matters and why now. Decision-makers need to understand the strategic importance before diving into analysis.
- What problem are we solving? Frame as opportunity or threat
- Why is this important to our goals? Connect to team objectives (make playoffs, develop young QB, etc.)
- What data did we analyze? Establish credibility through comprehensiveness
- Why should we trust this analysis? Briefly mention methodology and validation
Example context slide:
Why This Matters Now
- NFL teams are increasingly aggressive on 4th down based on analytics (league average up from 18% to 25% in 3 years)
- We're leaving expected points on the field through conservative decision-making
- Our upcoming schedule includes 5 opponents who are aggressive on 4th down—we need to match their efficiency
- Data Analyzed: 180,000+ fourth down situations from 2020-2023 NFL seasons, our team's 250+ fourth down decisions from 2021-2023, validated against three independent win probability models
3. Key Findings (2-3 slides, 2-4 minutes)
Present the evidence using visuals, not dense tables. Each finding should build toward your recommendation.
- Use one visualization per slide, not multiple
- Show comparisons that make patterns obvious (us vs. league, optimal vs. actual, before vs. after)
- Highlight the 2-3 most important insights that drive your recommendation
- Use annotations to guide interpretation
- Anticipate the "so what" question for each finding
Example findings slides:
- Finding 1: "We're Punting in High Win Probability Situations" (scatter plot showing field position vs. yards to go, with our punts marked in situations where analytics favored going for it)
- Finding 2: "Our Conversion Rate Exceeds League Average" (bar chart comparing our 4th down conversion rate 63% vs. league 58%, broken down by distance)
- Finding 3: "Conservative Decisions Cost Us 1 Win Last Season" (cumulative win probability impact chart showing ~4% WP loss across season)
4. Recommendation (1 slide, 1-2 minutes)
Be specific and actionable. Vague recommendations don't get implemented.
- What exactly should we do? Provide clear decision rules or frameworks
- When should we do it? Specify situations or triggers
- Who is responsible? Identify decision-makers and implementers
- What resources are needed? Acknowledge costs (time, staff, tools)
Example recommendation slide:
Implement Analytics-Informed 4th Down Decision Tool
Decision Framework:
- 4th-and-1 anywhere past our own 40-yard line → GO
- 4th-and-2 or less in opponent territory → GO
- Any 4th down when trailing by 14+ in 4th quarter → GO
- In edge cases, follow real-time Win Probability guidance (provided via headset from booth)Decision Support Tools:
- Laminated decision matrix card for sideline reference
- Real-time WP calculations communicated via headset from analytics booth
- Weekly practice of identified high-probability 4th down situationsResponsibility:
- Head Coach: Final decision authority (as always)
- Analytics: Provide real-time WP calculations during games
- Offensive Coordinator: Design and practice 4th down play menu
5. Expected Impact (1 slide, 1 minute)
Quantify the benefits in multiple dimensions. Different stakeholders care about different outcomes.
- On-field impact: Points, yards, time of possession
- Win probability impact: Expected wins added, playoff probability increase
- Strategic impact: Field position advantages, opponent adjustments
- Development impact: Player growth opportunities, scheme evolution (if relevant)
- Financial impact: For roster decisions, include cap implications
Example expected impact slide:
Projected Outcomes Over 17-Game Season
Offensive Production:
- +12 yards per game (from successful conversions extending drives)
- +0.8 points per game (longer drives = more scoring opportunities)
- -3 opponent possessions per game (sustaining drives rather than punting)Win Impact:
- +0.3 expected wins per season (based on historical WP model)
- +12-15% playoff probability (for teams on playoff bubble)Strategic Benefits:
- Improved average starting field position (opponents can't pin us as deep)
- Forces opponents to defend full field (can't sell out on 3rd down)
- Psychological momentum from aggressive approach
6. Implementation Plan (1 slide, 1 minute)
Show you've thought through execution. Good recommendations fail due to poor implementation planning.
- Timeline: Specific phases with dates
- Responsibilities: Who does what
- Success metrics: How we'll measure if it's working
- Contingency plans: What if it doesn't work as expected?
Example implementation slide:
Implementation Timeline and Metrics
Weeks 1-2: Training Phase
- Install decision matrix with coaching staff
- Practice 4th-and-short situations daily
- Run simulated game scenarios in practiceWeeks 3-4: Pilot Phase
- Use analytics guidance for non-critical 4th downs only
- Gather feedback from coaching staff
- Refine decision matrix based on learningsWeek 5+: Full Implementation
- Apply decision framework to all appropriate situations
- Weekly review of 4th down decisions and outcomesSuccess Metrics:
- 4th down attempt rate reaches 25% (from current 18%)
- Decision alignment with model ≥80% (acknowledging coaches override in some situations)
- Conversion rate maintains ≥60% (current 63%)Contingency Plan:
- If conversion rate drops below 55% through Week 10, pause and reassess
- If coaching staff uncomfortable with implementation, revert to conservative approach and analyze what went wrong
7. Questions and Discussion (Open-ended)
The presentation ends with slides, but the persuasion continues in Q&A. Anticipate objections and prepare thoughtful responses.
Common questions to prepare for (addressed in detail later in this chapter):
- "How do you know this will work for our team?"
- "What about momentum and intangibles?"
- "Won't this make us too predictable?"
- "What if we're wrong?"
- "How does this account for [specific situational factor]?"
Presentation Design Principles
**Slide Design Best Practices**: - **One main idea per slide**: Don't cram multiple findings onto one slide - **Minimal text**: <50 words per slide; use visuals to communicate - **High-contrast**: Ensure readability in various lighting conditions (meeting rooms, stadium boxes) - **Consistent formatting**: Use same fonts, colors, and layout throughout - **Progressive disclosure**: Build complexity gradually; start simple, add nuance **Presentation Delivery Tips**: - **Rehearse extensively**: Practice until you can present without reading slides - **Time yourself**: Ensure you can deliver in allotted time with buffer for questions - **Prepare backup slides**: Have detailed methodology slides ready if asked - **Test technology**: Ensure your presentation works on the display system you'll use - **Bring handouts**: Physical copies allow note-taking and serve as reference after the meeting **What NOT to do**: - ❌ Read bullet points verbatim from slides - ❌ Apologize for slide quality or analysis limitations upfront - ❌ Use tables with tiny unreadable numbers - ❌ Include every analysis you ran (focus on what matters) - ❌ Use jargon or acronyms without defining them - ❌ Go over your allotted time - ❌ Get defensive when challenged(Continue with additional sections...)
Summary
Communicating analytics insights to decision-makers is both an art and a science. Success requires:
Understanding your audience:
- Know their background, priorities, and preferences
- Tailor your message to each stakeholder group
- Speak their language, not statistics jargon
Presenting effectively:
- Start with the "so what"—the actionable insight
- Use visualizations over tables
- Tell stories, not just share data
- Structure presentations for quick comprehension
Building tools:
- Create dashboards for real-time decision support
- Automate routine reporting
- Make insights accessible on any device
- Integrate with existing workflows
Handling resistance:
- Anticipate objections and address them proactively
- Acknowledge the value of football expertise
- Frame analytics as a tool, not a replacement for judgment
- Be humble about limitations
Building credibility:
- Start small and build trust over time
- Be right more than wrong
- Embed yourself in football operations
- Show ROI and celebrate wins
The most brilliant analysis is useless if it doesn't influence decisions. Mastering communication is what separates good analysts from great ones.
Exercises
Conceptual Questions
-
Audience Analysis: You need to present findings about red zone inefficiency to three audiences: (a) the head coach, (b) the offensive coordinator, and (c) the general manager. Write a one-paragraph summary for each, tailored to their specific perspective and priorities.
-
Objection Handling: A coach says, "This analytics recommendation might work on average, but I know my players, and it won't work for us." How would you respond?
-
Storytelling: Take this data finding: "Teams that pass on first down in the opponent's territory average 0.23 more EPA than teams that run." Turn this into a compelling one-paragraph story.
Further Reading
Books
- Tufte, E. R. (2001). The Visual Display of Quantitative Information (2nd ed.). Graphics Press.
- Cairo, A. (2016). The Truthful Art: Data, Charts, and Maps for Communication. New Riders.
- Nussbaumer Knaflic, C. (2015). Storytelling with Data: A Data Visualization Guide for Business Professionals. Wiley.
- Berinato, S. (2016). Good Charts: The HBR Guide to Making Smarter, More Persuasive Data Visualizations. Harvard Business Review Press.
Articles and Papers
- Silver, N. (2012). "The Signal and the Noise: Why So Many Predictions Fail—But Some Don't." Penguin Press.
- Few, S. (2006). "Information Dashboard Design: The Effective Visual Communication of Data." O'Reilly Media.
Online Resources
- Flowing Data - Data visualization examples and tutorials
- Information is Beautiful - Inspiration for visual communication
- r/dataisbeautiful - Community-shared visualizations
References
:::