Skip to Content
Course content

221: Sports Data Visualization Techniques

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When I first started working with sports analytics, I fell into a trap that almost every beginner does: I thought that "more data on the screen equals more insight." I'd take a season's worth of NBA shot coordinates and dump every single point onto a plot of the court. I thought I was providing a comprehensive view of a player's game. In reality, I was just creating a "blob" of overlapping dots that told the coach absolutely nothing.

The "More Dots Equal More Insight" Fallacy

The misconception is that a raw scatter plot of event coordinates—whether it's a shot chart in basketball or a pass map in soccer—is the gold standard for sports visualization. It feels honest because you're showing every data point. But here is why that's wrong: sports data is incredibly dense. When you plot 500 shots on a 50x47 foot court, the points overlap. You can't tell if a cluster of dots represents ten shots or a hundred. You lose the density, which is actually the piece of information you're looking for.

# The WRONG way: Just dumping points
# This creates a messy 'blob' where you can't see density
ggplot(nba_shots, aes(x = x_coord, y = y_coord)) + 
  geom_point(color = "blue") + 
  coord_fixed()

If you do this, you're forcing the viewer to guess. In a high-pressure environment like a front office or a coaching staff, "guessing" is a failure of the visualization.

Turning Noise into Spatial Intelligence

To fix this, we need to stop thinking about points and start thinking about probability zones. Instead of plotting every shot, we use hexbinning or 2D density plots. This transforms the visualization from a list of events into a heatmap of tendencies. I personally prefer stat_summary_2d because it allows us to calculate the actual field goal percentage within a specific grid square, rather than just counting how many shots were taken there.

Another critical detail I've learned the hard way: always use coord_fixed(). If you don't, R will stretch your court to fit the window, and your "corner three" will suddenly look like it's being shot from the parking lot. It's a small line of code, but it's the difference between a professional tool and a toy.

# The RIGHT way: Using binned summaries for efficiency
ggplot(nba_shots, aes(x = x_coord, y = y_coord, z = shot_made_indicator)) + 
  stat_summary_2d(fun = mean, bins = 20) + 
  scale_fill_gradient(low = "red", high = "green") + 
  coord_fixed() + 
  labs(title = "Player Shot Efficiency Map", fill = "FG%")

By mapping the z aesthetic to the outcome (1 for make, 0 for miss) and using mean as the function, the color now represents the actual percentage of shots made in that area. Now, the coach can see at a glance that the player is lethal from the left elbow but struggles in the paint, regardless of how many shots were taken.

Layering Contextual Overlays

A heatmap is great, but it's still just math on a blank canvas. To make it "sports data," you need to overlay the geometry of the game. I usually do this by creating a separate dataframe for the court lines. Instead of searching for a complex library, just define the lines as segments. It keeps your code portable and your render times fast.

  • The Baseline: A simple segment from (-25, -4) to (25, -4).
  • The Sidelines: Segments connecting the baselines.
  • The Key: A series of rectangles or lines to define the restricted area.

When you layer these geom_segment calls on top of your density plot, the data suddenly gains a physical home. You aren't just looking at a coordinate system; you're looking at a basketball court.




📋 Practical Task

Build a High-Value Target Map for a Quarterback

You have been provided with a dataset qb_passes containing the following columns: target_x, target_y (coordinates on the field), and completion_success (1 for complete, 0 for incomplete).

Your task is to create a visualization that reveals the QB's "hot zones." Do not use a simple scatter plot of points. Instead, implement the following:

  1. Use stat_summary_2d to create a heatmap where the color represents the completion percentage of passes in that area.
  2. Ensure the color scale ranges from a "cold" color (e.g., "steelblue") for low percentage to a "hot" color (e.g., "orange") for high percentage.
  3. Force the aspect ratio to be equal using coord_fixed() so the football field maintains its real-world proportions.
  4. Add a geom_hline at y = 0 to represent the line of scrimmage.

Your final plot should allow a coach to identify exactly where the QB is most efficient at completing passes, without the visual clutter of overlapping individual pass targets.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.