WNBA Team Performance Analytics
Beginner
10 min read
0 views
Nov 27, 2025
Team Performance Evaluation
WNBA team analytics follows similar principles to NBA analysis but accounts for differences in pace, physicality, and strategic emphasis. The Four Factors framework—shooting, turnovers, rebounding, and free throws—remains highly predictive of team success.
Core Team Metrics
- Offensive Rating (ORtg): Points scored per 100 possessions
- Defensive Rating (DRtg): Points allowed per 100 possessions
- Net Rating: ORtg minus DRtg (best predictor of wins)
- Pace: Possessions per 40 minutes
- Four Factors: Shooting, turnovers, rebounding, free throws
Python: WNBA Team Analytics
Python: Comprehensive Team Performance Analysis
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Sample WNBA team data (normally loaded from API/database)
team_stats = pd.DataFrame({
'team': ['Las Vegas Aces', 'New York Liberty', 'Connecticut Sun',
'Minnesota Lynx', 'Seattle Storm', 'Phoenix Mercury',
'Dallas Wings', 'Atlanta Dream', 'Chicago Sky',
'Indiana Fever', 'Washington Mystics', 'Los Angeles Sparks'],
'wins': [34, 32, 27, 25, 20, 19, 18, 15, 13, 10, 9, 8],
'losses': [6, 8, 13, 15, 20, 21, 22, 25, 27, 30, 31, 32],
'points_for': [2840, 2795, 2650, 2588, 2520, 2490, 2455, 2380, 2350, 2280, 2260, 2200],
'points_against': [2520, 2550, 2590, 2600, 2680, 2720, 2750, 2820, 2890, 2950, 2980, 3020],
'fgm': [998, 985, 940, 920, 895, 880, 870, 845, 835, 810, 800, 785],
'fga': [2180, 2160, 2140, 2120, 2100, 2090, 2080, 2070, 2060, 2050, 2040, 2030],
'fg3m': [285, 295, 268, 255, 248, 242, 238, 230, 225, 218, 215, 208],
'fg3a': [785, 810, 750, 720, 710, 705, 700, 695, 690, 685, 680, 675],
'ftm': [560, 545, 505, 495, 485, 490, 480, 465, 460, 445, 450, 425],
'fta': [720, 710, 680, 670, 665, 675, 665, 655, 650, 645, 660, 625],
'orb': [385, 370, 395, 380, 375, 365, 360, 355, 350, 345, 340, 335],
'drb': [1185, 1165, 1155, 1145, 1130, 1120, 1110, 1100, 1085, 1070, 1060, 1050],
'ast': [680, 695, 645, 630, 615, 605, 595, 580, 570, 555, 550, 540],
'tov': [495, 505, 485, 490, 510, 515, 520, 535, 545, 560, 570, 580],
'stl': [285, 295, 280, 275, 268, 265, 260, 255, 250, 245, 240, 235],
'blk': [165, 155, 175, 168, 160, 158, 155, 150, 145, 140, 138, 135],
'opp_fgm': [890, 900, 915, 920, 945, 960, 970, 990, 1010, 1035, 1045, 1060],
'opp_fga': [2050, 2060, 2070, 2080, 2090, 2100, 2110, 2120, 2130, 2140, 2150, 2160]
})
# =============================================================================
# 1. Calculate Advanced Team Metrics
# =============================================================================
def calculate_team_efficiency(team_df):
"""Calculate offensive and defensive efficiency metrics"""
# Estimate possessions (simplified formula)
# Poss = FGA + 0.44*FTA - ORB + TOV
team_df['possessions'] = (
team_df['fga'] + 0.44 * team_df['fta'] -
team_df['orb'] + team_df['tov']
)
# Offensive Rating (points per 100 possessions)
team_df['ortg'] = (team_df['points_for'] / team_df['possessions']) * 100
# Defensive Rating (points allowed per 100 possessions)
team_df['drtg'] = (team_df['points_against'] / team_df['possessions']) * 100
# Net Rating
team_df['net_rtg'] = team_df['ortg'] - team_df['drtg']
# Pace (possessions per 40 minutes, estimated)
games = team_df['wins'] + team_df['losses']
team_df['pace'] = (team_df['possessions'] / games) / 40 * 40
# Win percentage
team_df['win_pct'] = team_df['wins'] / games
return team_df
team_stats = calculate_team_efficiency(team_stats)
print("=== Team Efficiency Ratings ===")
print(team_stats[['team', 'wins', 'losses', 'ortg', 'drtg', 'net_rtg']]
.sort_values('net_rtg', ascending=False))
# =============================================================================
# 2. Four Factors Analysis
# =============================================================================
def calculate_four_factors(team_df):
"""Calculate Dean Oliver's Four Factors for team success"""
# Factor 1: Effective Field Goal % (eFG%)
team_df['efg_pct'] = (team_df['fgm'] + 0.5 * team_df['fg3m']) / team_df['fga']
team_df['opp_efg_pct'] = (team_df['opp_fgm'] + 0.5 * (team_df['points_against'] -
team_df['opp_fgm'] * 2 - team_df['fgm'])) / team_df['opp_fga']
# Factor 2: Turnover Rate (TOV%)
team_df['tov_pct'] = team_df['tov'] / team_df['possessions']
# Factor 3: Offensive Rebounding Rate (ORB%)
total_rebounds = team_df['orb'] + team_df['drb']
team_df['orb_pct'] = team_df['orb'] / (team_df['orb'] + team_df['drb'])
# Factor 4: Free Throw Rate (FT/FGA)
team_df['ft_rate'] = team_df['ftm'] / team_df['fga']
return team_df
team_stats = calculate_four_factors(team_stats)
print("\n=== Four Factors Rankings ===")
print("Shooting (eFG%):")
print(team_stats[['team', 'efg_pct']].sort_values('efg_pct', ascending=False).head(5))
print("\nTurnover Rate (lower is better):")
print(team_stats[['team', 'tov_pct']].sort_values('tov_pct').head(5))
print("\nOffensive Rebounding %:")
print(team_stats[['team', 'orb_pct']].sort_values('orb_pct', ascending=False).head(5))
print("\nFree Throw Rate:")
print(team_stats[['team', 'ft_rate']].sort_values('ft_rate', ascending=False).head(5))
# =============================================================================
# 3. Predict Win Percentage from Net Rating
# =============================================================================
from scipy.stats import pearsonr
correlation, p_value = pearsonr(team_stats['net_rtg'], team_stats['win_pct'])
print(f"\n=== Net Rating vs Win Percentage ===")
print(f"Correlation: {correlation:.3f}")
print(f"P-value: {p_value:.4f}")
# Linear regression for win prediction
from sklearn.linear_model import LinearRegression
X = team_stats[['net_rtg']].values
y = team_stats['win_pct'].values
model = LinearRegression()
model.fit(X, y)
team_stats['predicted_win_pct'] = model.predict(X)
team_stats['win_diff'] = team_stats['win_pct'] - team_stats['predicted_win_pct']
print("\nTeams Over/Under-performing Expectations:")
print(team_stats[['team', 'win_pct', 'predicted_win_pct', 'win_diff']]
.sort_values('win_diff', ascending=False))
# =============================================================================
# 4. Offensive vs Defensive Efficiency Scatter
# =============================================================================
fig, ax = plt.subplots(figsize=(10, 8))
scatter = ax.scatter(team_stats['ortg'], team_stats['drtg'],
s=team_stats['win_pct']*1000,
alpha=0.6, c=team_stats['net_rtg'],
cmap='RdYlGn')
# Add team labels
for idx, row in team_stats.iterrows():
ax.annotate(row['team'],
(row['ortg'], row['drtg']),
fontsize=8, ha='center')
# Add reference lines (league average)
ax.axhline(team_stats['drtg'].mean(), color='gray', linestyle='--', alpha=0.5)
ax.axvline(team_stats['ortg'].mean(), color='gray', linestyle='--', alpha=0.5)
ax.set_xlabel('Offensive Rating (points per 100 poss)', fontsize=12)
ax.set_ylabel('Defensive Rating (points allowed per 100 poss)', fontsize=12)
ax.set_title('WNBA Team Efficiency Map\n(Size = Win%, Color = Net Rating)', fontsize=14)
ax.invert_yaxis() # Lower DRtg is better
plt.colorbar(scatter, label='Net Rating')
plt.tight_layout()
# plt.show()
# =============================================================================
# 5. Team Style Analysis
# =============================================================================
def categorize_team_style(row):
"""Categorize team playing style"""
if row['pace'] > team_stats['pace'].median():
tempo = "Fast"
else:
tempo = "Slow"
if row['fg3a'] / row['fga'] > 0.35:
shooting = "Three-Heavy"
else:
shooting = "Inside-Focused"
return f"{tempo}-{shooting}"
team_stats['style'] = team_stats.apply(categorize_team_style, axis=1)
print("\n=== Team Style Distribution ===")
print(team_stats[['team', 'pace', 'style', 'net_rtg']]
.sort_values('net_rtg', ascending=False))
# Aggregate statistics by style
style_performance = team_stats.groupby('style').agg({
'net_rtg': 'mean',
'win_pct': 'mean',
'ortg': 'mean',
'drtg': 'mean'
}).round(2)
print("\n=== Performance by Playing Style ===")
print(style_performance)
print("\n=== Team Analytics Complete ===")
print("Metrics calculated:")
print("✓ Offensive/Defensive Ratings")
print("✓ Net Rating and Pace")
print("✓ Four Factors (Shooting, TOV, REB, FT)")
print("✓ Win prediction models")
print("✓ Team style classification")
R: WNBA Team Analytics with wehoop
library(wehoop)
library(tidyverse)
library(scales)
library(ggrepel)
# Load WNBA team box scores
team_box <- wehoop::load_wnba_team_box(seasons = 2024)
# =============================================================================
# 1. Calculate Season-Long Team Statistics
# =============================================================================
team_season_stats <- team_box %>%
group_by(team_id, team_display_name) %>%
summarise(
games = n(),
wins = sum(team_winner, na.rm = TRUE),
points_for = sum(team_score, na.rm = TRUE),
points_against = sum(opponent_team_score, na.rm = TRUE),
# Shooting
fgm = sum(field_goals_made, na.rm = TRUE),
fga = sum(field_goals_attempted, na.rm = TRUE),
fg3m = sum(three_point_field_goals_made, na.rm = TRUE),
fg3a = sum(three_point_field_goals_attempted, na.rm = TRUE),
ftm = sum(free_throws_made, na.rm = TRUE),
fta = sum(free_throws_attempted, na.rm = TRUE),
# Other stats
orb = sum(offensive_rebounds, na.rm = TRUE),
drb = sum(defensive_rebounds, na.rm = TRUE),
trb = sum(total_rebounds, na.rm = TRUE),
ast = sum(assists, na.rm = TRUE),
stl = sum(steals, na.rm = TRUE),
blk = sum(blocks, na.rm = TRUE),
tov = sum(turnovers, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
# Win percentage
win_pct = wins / games,
# Estimate possessions
possessions = fga + 0.44 * fta - orb + tov,
# Efficiency ratings
ortg = (points_for / possessions) * 100,
drtg = (points_against / possessions) * 100,
net_rtg = ortg - drtg,
# Pace (possessions per game)
pace = possessions / games,
# Four Factors
efg_pct = (fgm + 0.5 * fg3m) / fga,
tov_pct = tov / possessions,
orb_pct = orb / (orb + drb),
ft_rate = ftm / fga,
# Shooting percentages
fg_pct = fgm / fga,
three_pct = fg3m / fg3a,
ft_pct = ftm / fta,
# Per game averages
ppg = points_for / games,
rpg = trb / games,
apg = ast / games
)
cat("=== WNBA Team Efficiency Rankings ===\n")
print(team_season_stats %>%
select(team_display_name, wins, games, ortg, drtg, net_rtg) %>%
arrange(desc(net_rtg)))
# =============================================================================
# 2. Four Factors Analysis
# =============================================================================
cat("\n=== Four Factors Leaders ===\n\n")
cat("1. Shooting (eFG%):\n")
print(team_season_stats %>%
select(team_display_name, efg_pct) %>%
arrange(desc(efg_pct)) %>%
head(5))
cat("\n2. Turnover Rate (lower is better):\n")
print(team_season_stats %>%
select(team_display_name, tov_pct) %>%
arrange(tov_pct) %>%
head(5))
cat("\n3. Offensive Rebounding %:\n")
print(team_season_stats %>%
select(team_display_name, orb_pct) %>%
arrange(desc(orb_pct)) %>%
head(5))
cat("\n4. Free Throw Rate:\n")
print(team_season_stats %>%
select(team_display_name, ft_rate) %>%
arrange(desc(ft_rate)) %>%
head(5))
# =============================================================================
# 3. Net Rating vs Win Percentage Correlation
# =============================================================================
correlation <- cor(team_season_stats$net_rtg, team_season_stats$win_pct)
cat(sprintf("\n=== Net Rating vs Win%% Correlation: %.3f ===\n", correlation))
# Predict win percentage from net rating
win_model <- lm(win_pct ~ net_rtg, data = team_season_stats)
team_season_stats <- team_season_stats %>%
mutate(
predicted_win_pct = predict(win_model, newdata = .),
win_diff = win_pct - predicted_win_pct
)
cat("\nTeams Over/Under-performing Net Rating:\n")
print(team_season_stats %>%
select(team_display_name, win_pct, predicted_win_pct, win_diff) %>%
arrange(desc(win_diff)))
# =============================================================================
# 4. Visualization: Offensive vs Defensive Efficiency
# =============================================================================
ggplot(team_season_stats,
aes(x = ortg, y = drtg, size = win_pct, color = net_rtg)) +
geom_point(alpha = 0.7) +
geom_text_repel(aes(label = team_display_name), size = 3) +
geom_hline(yintercept = mean(team_season_stats$drtg),
linetype = "dashed", alpha = 0.5) +
geom_vline(xintercept = mean(team_season_stats$ortg),
linetype = "dashed", alpha = 0.5) +
scale_color_gradient2(low = "red", mid = "yellow", high = "darkgreen",
midpoint = 0, name = "Net Rating") +
scale_size_continuous(name = "Win %", labels = percent_format()) +
scale_y_reverse() + # Lower DRtg is better
labs(
title = "WNBA Team Efficiency Map 2024",
subtitle = "Offensive vs Defensive Rating (size = Win%)",
x = "Offensive Rating (pts per 100 poss)",
y = "Defensive Rating (pts allowed per 100 poss)"
) +
theme_minimal()
# =============================================================================
# 5. Team Style Classification
# =============================================================================
team_season_stats <- team_season_stats %>%
mutate(
tempo = if_else(pace > median(pace), "Fast", "Slow"),
shooting_style = if_else(fg3a / fga > 0.35, "Three-Heavy", "Inside"),
style = paste0(tempo, "-", shooting_style)
)
cat("\n=== Team Playing Styles ===\n")
print(team_season_stats %>%
select(team_display_name, pace, style, net_rtg, win_pct) %>%
arrange(desc(net_rtg)))
# Style performance comparison
style_summary <- team_season_stats %>%
group_by(style) %>%
summarise(
teams = n(),
avg_net_rtg = mean(net_rtg),
avg_win_pct = mean(win_pct),
avg_ortg = mean(ortg),
avg_drtg = mean(drtg),
.groups = "drop"
)
cat("\n=== Performance by Style ===\n")
print(style_summary)
# =============================================================================
# 6. Four Factors Correlation with Winning
# =============================================================================
four_factors_impact <- team_season_stats %>%
summarise(
efg_cor = cor(efg_pct, win_pct),
tov_cor = cor(-tov_pct, win_pct), # Negative because lower is better
orb_cor = cor(orb_pct, win_pct),
ft_cor = cor(ft_rate, win_pct)
)
cat("\n=== Four Factors Correlation with Winning ===\n")
cat(sprintf("Shooting (eFG%%): %.3f\n", four_factors_impact$efg_cor))
cat(sprintf("Turnovers (low TOV%%): %.3f\n", four_factors_impact$tov_cor))
cat(sprintf("Rebounding (ORB%%): %.3f\n", four_factors_impact$orb_cor))
cat(sprintf("Free Throws (FT Rate): %.3f\n", four_factors_impact$ft_cor))
# =============================================================================
# 7. League-Wide Trends
# =============================================================================
cat("\n=== WNBA League Averages ===\n")
league_averages <- team_season_stats %>%
summarise(
avg_pace = mean(pace),
avg_ortg = mean(ortg),
avg_ppg = mean(ppg),
avg_3pa_rate = mean(fg3a / fga),
avg_efg = mean(efg_pct),
.groups = "drop"
)
print(league_averages)
cat("\n=== Team Analytics Complete ===\n")
cat("✓ Efficiency ratings (ORtg, DRtg, Net Rating)\n")
cat("✓ Four Factors analysis\n")
cat("✓ Win prediction models\n")
cat("✓ Playing style classification\n")
cat("✓ League trend analysis\n")
Four Factors Hierarchy
Research shows that in basketball, the Four Factors have different predictive weights for team success. Shooting efficiency (eFG%) is typically the most important factor, followed by turnovers, rebounding, and free throw rate. This hierarchy generally holds true in the WNBA as well.
Team Evaluation Best Practices
- Use per-possession metrics (ratings) instead of raw totals
- Prioritize net rating as the best single predictor of team quality
- Analyze Four Factors to identify specific team strengths/weaknesses
- Consider pace-adjusted stats when comparing teams
- Examine home/away splits for full context
Common Analysis Pitfalls
- Overvaluing raw point totals without considering pace
- Ignoring defensive efficiency in favor of offensive stats
- Not adjusting for strength of schedule
- Focusing on single-game performance instead of season trends
Discussion
Have questions or feedback? Join our community discussion on
Discord or
GitHub Discussions.
Table of Contents
Related Topics
Quick Actions