Player Efficiency Rating (PER)
Player Efficiency Rating (PER): The Complete Guide
Player Efficiency Rating (PER) is one of basketball's most widely recognized advanced statistics, designed to boil down all of a player's contributions into a single number. Created by basketball statistician John Hollinger, PER has become a staple metric for player evaluation, contract negotiations, and MVP debates. This comprehensive guide explores everything you need to know about PER, from its mathematical foundation to its practical applications and limitations.
What is Player Efficiency Rating (PER)?
Player Efficiency Rating is an all-in-one basketball rating that attempts to measure a player's per-minute performance while adjusting for pace. The metric combines all of a player's positive accomplishments (points, rebounds, assists, steals, blocks) and subtracts negative ones (missed shots, turnovers, personal fouls) into a single comprehensive number.
The key features of PER include:
- Per-Minute Basis: PER is calculated per minute of play, making it easier to compare players with different playing times
- Pace Adjustment: The metric adjusts for team pace, allowing fair comparisons across different eras and playing styles
- League Average Baseline: PER is calibrated so that the league average is always exactly 15.0
- Comprehensive Scope: Unlike simple stats like points per game, PER incorporates all major box score statistics
PER has been particularly influential in the analytics revolution in basketball, helping teams identify undervalued players and providing fans with a quick snapshot of player production. However, as we'll explore later, it's important to understand both its strengths and limitations.
John Hollinger and the Creation of PER
John Hollinger developed PER in the early 2000s while working as a basketball analyst and writer. His goal was to create a single metric that could capture overall player value in a way that traditional statistics couldn't. Before joining ESPN as their NBA insider, Hollinger published several books on basketball analytics, including "Pro Basketball Forecast" and "Pro Basketball Prospectus," where he refined and popularized the PER formula.
Hollinger's innovation came at a crucial time in basketball history. The sport was generating more detailed statistics than ever before, but analysts lacked a unified framework for player evaluation. PER filled this gap by providing a standardized measure that could be calculated from standard box score data.
In 2012, Hollinger left ESPN to join the Memphis Grizzlies' front office as Vice President of Basketball Operations, bringing his analytical approach directly into NBA decision-making. While he's no longer with the Grizzlies, his PER metric remains widely used throughout basketball analytics, even as newer metrics have emerged to complement or challenge it.
The Complete PER Formula and Components
The PER calculation is complex, involving multiple steps and adjustments. Here's a breakdown of the complete formula:
Step 1: Calculate Unadjusted PER (uPER)
The unadjusted PER is calculated using the following formula:
uPER = (1 / MP) × [
3P
+ (2/3) × AST
+ (2 - factor × (team_AST / team_FG)) × FG
+ (FT × 0.5 × (1 + (1 - (team_AST / team_FG)) + (2/3) × (team_AST / team_FG)))
- VOP × TOV
- VOP × DRB% × (FGA - FG)
- VOP × 0.44 × (0.44 + (0.56 × DRB%)) × (FTA - FT)
+ VOP × (1 - DRB%) × (TRB - ORB)
+ VOP × DRB% × ORB
+ VOP × STL
+ VOP × DRB% × BLK
- PF × ((lg_FT / lg_PF) - 0.44 × (lg_FTA / lg_PF) × VOP)
]
Where:
- MP: Minutes Played
- 3P: 3-Point Field Goals Made
- AST: Assists
- FG: Field Goals Made
- FT: Free Throws Made
- TOV: Turnovers
- FGA: Field Goals Attempted
- FTA: Free Throws Attempted
- TRB: Total Rebounds
- ORB: Offensive Rebounds
- STL: Steals
- BLK: Blocks
- PF: Personal Fouls
Step 2: Calculate Supporting Variables
Value of Possession (VOP):
VOP = lg_PTS / (lg_FGA - lg_ORB + lg_TOV + 0.44 × lg_FTA)
Defensive Rebound Percentage (DRB%):
DRB% = (lg_TRB - lg_ORB) / lg_TRB
Factor:
factor = (2/3) - (0.5 × (lg_AST / lg_FG)) / (2 × (lg_FG / lg_FT))
Step 3: Adjust for Pace
The pace-adjusted PER accounts for differences in team pace:
aPER = (lg_Pace / team_Pace) × uPER
Step 4: Normalize to League Average
Finally, PER is scaled so that the league average is exactly 15.0:
PER = aPER × (15 / lg_aPER)
This ensures that PER remains consistent across seasons, with 15.0 always representing average performance.
League Average PER is Always 15.0
One of the most important features of PER is its calibration to a league average of 15.0. This standardization serves several crucial purposes:
- Consistent Baseline: No matter which season you're examining, 15.0 always means league-average performance
- Easy Interpretation: You can immediately tell if a player is above or below average without additional context
- Cross-Era Comparisons: The 15.0 baseline makes it easier to compare players from different eras, though pace adjustments still matter
- Quick Assessment: A player with PER of 18.0 is clearly above average, while 12.0 indicates below-average production
The normalization process ensures this constant average. After calculating pace-adjusted PER for all players, the league average is computed, and all individual PERs are scaled proportionally to make that average exactly 15.0. This mathematical constraint means that if offensive production increases league-wide, individual PERs won't inflate—they'll be scaled back to maintain the 15.0 average.
PER Benchmarks: What the Numbers Mean
Understanding PER benchmarks helps contextualize player performance. Here's a comprehensive guide to PER ratings:
Elite Tier (25.0+)
- MVP Candidates: Players consistently posting 25+ PER are typically in MVP conversations
- Historical Context: Only a handful of players reach this level in any given season
- Examples: Peak LeBron James, Michael Jordan, Shaquille O'Neal, Giannis Antetokounmpo
- Impact: These players are franchise cornerstones and game-changers
All-Star Tier (20.0-24.9)
- All-Star Level: Most All-Star selections come from this range
- High-End Starters: Elite players who may not be MVP-level but are critical to team success
- Examples: All-Star guards, elite role players, second-tier superstars
- Impact: Players teams build around or acquire as key pieces
Quality Starter Tier (15.0-19.9)
- Above Average Starters: Solid contributors who provide consistent value
- 15.0-17.0: Average to slightly above-average starters
- 17.0-19.9: Good starters, potential All-Star consideration
- Impact: Players who start on playoff teams and contribute meaningfully
Rotation Player Tier (11.0-14.9)
- Below Average Starters: Players who may start but provide limited efficiency
- Quality Bench Players: Sixth men and key reserves often fall here
- Role Players: Specialists who contribute in specific areas
- Impact: Depth pieces essential for team rotation but not primary options
Marginal Player Tier (Below 11.0)
- End of Bench: Limited-minute players or those struggling with efficiency
- Developing Players: Young players still learning the game
- Specialists: Defensive specialists whose value isn't captured by PER
- Impact: Players who may not stay in the league long without improvement
Strengths of PER
Despite criticisms, PER offers several significant advantages that explain its enduring popularity:
1. Comprehensive Offensive Metric
PER captures virtually all offensive contributions in the box score, from scoring efficiency to playmaking to offensive rebounding. This provides a more complete picture than traditional stats like points per game.
2. Accessibility and Simplicity
While the formula is complex, the resulting number is easy to understand. A single PER value communicates a player's overall production instantly, making it valuable for quick comparisons and casual fans.
3. Historical Data Availability
Because PER uses only box score statistics, it can be calculated for players throughout NBA history. This enables historical comparisons that newer metrics (requiring more detailed tracking data) cannot provide.
4. Pace Adjustment
By adjusting for team pace, PER allows meaningful comparisons between players from different eras and teams. A player in the high-pace 1980s can be fairly compared to one from the slower-paced 2000s.
5. Per-Minute Basis
Calculating efficiency per minute played removes playing time as a confounding variable, helping identify players who produce at high levels even in limited minutes.
6. Standardized Scale
The 15.0 league average provides an intuitive reference point that makes PER interpretable without additional context.
Criticisms and Limitations of PER
While PER has value, it's crucial to understand its significant limitations:
1. Defensive Deficiencies
PER's most glaring weakness is its inability to adequately measure defense. While it includes steals and blocks, these represent only a fraction of defensive value. The metric completely misses:
- On-ball defense and perimeter containment
- Help defense and rotations
- Communication and defensive leadership
- Deterrence effects (contested shots that don't result in blocks)
- Defensive positioning and awareness
This means elite defenders who don't accumulate steals and blocks (like Kawhi Leonard in his prime) may have PER ratings that undervalue their true impact.
2. Pace and Volume Bias
Despite pace adjustments, PER still tends to favor players on faster-paced teams and those with high usage rates. Players who dominate the ball and take many shots can accumulate higher PERs even if they're not necessarily more valuable to winning.
3. Rebound Overvaluation
PER treats all rebounds equally, but not all rebounds are created equal. Uncontested defensive rebounds by big men are valued the same as contested offensive rebounds, even though the latter requires more skill and provides more value.
4. Missing Context
PER doesn't account for:
- Quality of teammates (easier to get assists with better scorers)
- Quality of opponents
- Clutch performance
- Leadership and intangibles
- Spacing and gravity effects (drawing defenders without touching the ball)
5. Position Bias
PER tends to favor big men who can accumulate rebounds, blocks, and high-percentage shots near the basket. Guards and perimeter players may be undervalued relative to their true impact on winning.
6. Team Success Disconnect
A player can have a high PER while playing for a losing team, or vice versa. PER measures individual production but doesn't directly correlate with winning, which is ultimately what matters.
Historical PER Leaders
Throughout NBA history, certain players have dominated PER rankings, often coinciding with their peak performance years:
Single-Season PER Records
- Nikola Jokic (2021-22): 32.85 - The highest PER in NBA history, during his second MVP season
- Giannis Antetokounmpo (2019-20): 31.86 - Peak efficiency during his dominant MVP campaign
- Wilt Chamberlain (1962-63): 31.82 - Legendary season with 44.8 PPG
- Wilt Chamberlain (1961-62): 31.74 - The famous 50.4 PPG season
- LeBron James (2008-09): 31.67 - First MVP season in Cleveland
- LeBron James (2012-13): 31.59 - Peak Miami Heat dominance
- Michael Jordan (1987-88): 31.71 - MVP and Defensive Player of the Year season
- Michael Jordan (1990-91): 31.63 - First championship season
Career PER Leaders (Minimum 10,000 Minutes)
- Michael Jordan: 27.91 - The all-time career PER leader
- LeBron James: 27.11 - Sustained excellence across two decades
- Shaquille O'Neal: 26.43 - Dominant interior force
- Wilt Chamberlain: 26.13 - Statistical dominance of the 1960s
- Nikola Jokic: 25.95 - Modern efficiency master (career ongoing)
- Giannis Antetokounmpo: 25.72 - Two-way dominance (career ongoing)
- Magic Johnson: 24.11 - Revolutionary point guard
- Kareem Abdul-Jabbar: 24.58 - All-time scoring leader with efficiency
Notable Observations
The historical PER leaders reveal interesting patterns about basketball excellence:
- Elite big men tend to post higher PERs due to rebounding and high-percentage shooting
- Recent seasons show higher peak PERs, possibly due to rule changes favoring offense
- Sustained excellence (high career PER) is rarer than single-season peaks
- The metric confirms the dominance of players widely regarded as all-time greats
Comparing PER to Other Advanced Metrics
PER exists in an ecosystem of advanced basketball statistics. Understanding how it compares to alternatives helps determine when to use each metric:
PER vs. Box Plus/Minus (BPM)
Box Plus/Minus estimates a player's contribution per 100 possessions relative to a league-average player. Key differences:
- Defensive Coverage: BPM includes a defensive component (DBPM) that attempts to estimate defensive impact from box score stats, making it more comprehensive than PER
- Scale: BPM is centered at 0 (league average), while PER uses 15.0
- Modern Development: BPM was created more recently with access to play-by-play data and regression analysis
- Accuracy: Studies suggest BPM correlates better with team success than PER
When to use BPM: When you need a more balanced view of offensive and defensive contributions, or when evaluating two-way players.
PER vs. Value Over Replacement Player (VORP)
VORP measures a player's total value compared to a replacement-level player (bench player). Key differences:
- Cumulative vs. Rate: VORP is cumulative (total value over a season), while PER is a rate stat (per-minute efficiency)
- Playing Time: VORP rewards players who maintain efficiency across heavy minutes, while PER treats all minutes equally
- Baseline: VORP compares to replacement level, not league average, making it more useful for roster decisions
When to use VORP: When evaluating overall season value or making roster/contract decisions about total contribution.
PER vs. Win Shares (WS)
Win Shares estimates the number of wins a player contributes to their team. Key differences:
- Win-Oriented: WS directly ties to team wins, while PER measures individual efficiency
- Offensive and Defensive: WS includes both offensive and defensive win shares
- Team Context: WS is influenced by team quality and minutes played
- Historical Use: WS has roots in baseball's sabermetrics tradition
When to use Win Shares: When you want to connect individual performance to team success, or evaluate players in team context.
PER vs. True Shooting Percentage (TS%)
True Shooting Percentage measures shooting efficiency accounting for 2-pointers, 3-pointers, and free throws. Key differences:
- Narrow Focus: TS% only measures scoring efficiency, while PER includes all box score contributions
- Simplicity: TS% is straightforward to calculate and interpret
- Complementary: TS% works well alongside PER to understand scoring efficiency specifically
When to use TS%: When evaluating shooting and scoring efficiency in isolation.
Modern Alternatives: EPM, RAPTOR, LEBRON
Newer metrics like Estimated Plus-Minus (EPM), RAPTOR, and LEBRON use player tracking data and sophisticated statistical models:
- Data Requirements: These metrics require tracking data not available for historical seasons
- Complexity: More sophisticated modeling but less transparent calculations
- Accuracy: Generally better at predicting future performance and team success
- Defensive Measurement: Far superior at quantifying defensive impact
When to use modern metrics: For contemporary player evaluation and when defensive impact is crucial to the analysis.
Python Code Examples
Example 1: Calculating PER from Box Score Stats
import pandas as pd
import numpy as np
def calculate_per(player_stats, team_stats, league_stats):
"""
Calculate Player Efficiency Rating (PER) for a player.
Parameters:
player_stats: dict with player statistics
team_stats: dict with team statistics
league_stats: dict with league averages
Returns:
float: Player's PER
"""
# Calculate factor
factor = (2/3) - (0.5 * (league_stats['ast'] / league_stats['fg'])) / \
(2 * (league_stats['fg'] / league_stats['ft']))
# Calculate VOP (Value of Possession)
vop = league_stats['pts'] / (league_stats['fga'] - league_stats['orb'] +
league_stats['tov'] + 0.44 * league_stats['fta'])
# Calculate DRB% (Defensive Rebound Percentage)
drb_pct = (league_stats['trb'] - league_stats['orb']) / league_stats['trb']
# Unadjusted PER calculation
uper_components = [
player_stats['fg3'], # 3-point field goals
(2/3) * player_stats['ast'], # Assists
(2 - factor * (team_stats['ast'] / team_stats['fg'])) * player_stats['fg'],
player_stats['ft'] * 0.5 * (1 + (1 - (team_stats['ast'] / team_stats['fg'])) +
(2/3) * (team_stats['ast'] / team_stats['fg'])),
-vop * player_stats['tov'], # Turnovers (negative)
-vop * drb_pct * (player_stats['fga'] - player_stats['fg']), # Missed FG
-vop * 0.44 * (0.44 + 0.56 * drb_pct) * (player_stats['fta'] - player_stats['ft']),
vop * (1 - drb_pct) * (player_stats['trb'] - player_stats['orb']),
vop * drb_pct * player_stats['orb'],
vop * player_stats['stl'], # Steals
vop * drb_pct * player_stats['blk'], # Blocks
-player_stats['pf'] * ((league_stats['ft'] / league_stats['pf']) -
0.44 * (league_stats['fta'] / league_stats['pf']) * vop)
]
uper = sum(uper_components) / player_stats['mp']
# Pace adjustment
pace_adj = (league_stats['pace'] / team_stats['pace'])
aper = pace_adj * uper
# Normalize to league average of 15.0
per = aper * (15 / league_stats['aper'])
return per
# Example usage
player = {
'name': 'LeBron James',
'mp': 2316, # Minutes played
'fg': 643,
'fga': 1303,
'fg3': 128,
'ft': 310,
'fta': 434,
'orb': 65,
'trb': 498,
'ast': 562,
'stl': 93,
'blk': 51,
'tov': 268,
'pf': 120
}
team = {
'fg': 3234,
'ast': 1845,
'pace': 100.5
}
league = {
'pts': 110.6,
'fga': 88.6,
'orb': 10.1,
'tov': 14.2,
'fta': 22.3,
'trb': 43.5,
'ast': 24.5,
'fg': 40.2,
'ft': 17.1,
'pf': 19.8,
'pace': 99.2,
'aper': 15.0 # League average aPER
}
per_rating = calculate_per(player, team, league)
print(f"{player['name']}'s PER: {per_rating:.2f}")
Example 2: Fetching PER Data from Basketball Reference
import requests
from bs4 import BeautifulSoup
import pandas as pd
def fetch_season_per(season='2024'):
"""
Fetch PER data for all players in a given season from Basketball Reference.
Parameters:
season: str, the ending year of the season (e.g., '2024' for 2023-24)
Returns:
DataFrame with player names, teams, and PER ratings
"""
url = f'https://www.basketball-reference.com/leagues/NBA_{season}_advanced.html'
try:
# Send request with user agent
headers = {'User-Agent': 'Mozilla/5.0'}
response = requests.get(url, headers=headers)
response.raise_for_status()
# Parse HTML
soup = BeautifulSoup(response.content, 'html.parser')
table = soup.find('table', {'id': 'advanced_stats'})
# Convert to DataFrame
df = pd.read_html(str(table))[0]
# Clean data
df = df[df['Player'] != 'Player'] # Remove header rows
df['PER'] = pd.to_numeric(df['PER'], errors='coerce')
# Select relevant columns
per_data = df[['Player', 'Tm', 'PER', 'MP']].copy()
per_data.columns = ['Player', 'Team', 'PER', 'Minutes']
# Sort by PER
per_data = per_data.sort_values('PER', ascending=False)
per_data = per_data.reset_index(drop=True)
return per_data
except Exception as e:
print(f"Error fetching data: {e}")
return None
# Example: Get top 10 players by PER
season_per = fetch_season_per('2024')
if season_per is not None:
print("\nTop 10 Players by PER (2023-24 Season):")
print(season_per.head(10).to_string(index=False))
# Calculate statistics
print(f"\nLeague Average PER: {season_per['PER'].mean():.2f}")
print(f"Highest PER: {season_per['PER'].max():.2f}")
print(f"Players above 20 PER: {len(season_per[season_per['PER'] > 20])}")
Example 3: Comparing Players Across Eras
import pandas as pd
import matplotlib.pyplot as plt
def compare_era_pers(players_dict):
"""
Compare PER ratings of players from different eras.
Parameters:
players_dict: dict with player names as keys and their career PER as values
Returns:
Visualization comparing player PERs
"""
# Create DataFrame
df = pd.DataFrame(list(players_dict.items()), columns=['Player', 'Career_PER'])
df = df.sort_values('Career_PER', ascending=True)
# Create horizontal bar chart
plt.figure(figsize=(12, 8))
bars = plt.barh(df['Player'], df['Career_PER'], color='steelblue', edgecolor='navy')
# Add league average line
plt.axvline(x=15.0, color='red', linestyle='--', linewidth=2, label='League Average (15.0)')
# Customize plot
plt.xlabel('Career PER', fontsize=12, fontweight='bold')
plt.ylabel('Player', fontsize=12, fontweight='bold')
plt.title('Career PER Comparison: All-Time Greats', fontsize=14, fontweight='bold', pad=20)
plt.grid(axis='x', alpha=0.3)
plt.legend(fontsize=10)
# Add value labels
for i, bar in enumerate(bars):
width = bar.get_width()
plt.text(width + 0.3, bar.get_y() + bar.get_height()/2,
f'{width:.2f}', ha='left', va='center', fontweight='bold')
plt.tight_layout()
return plt
# Historical greats with career PER
all_time_pers = {
'Michael Jordan': 27.91,
'LeBron James': 27.11,
'Shaquille O\'Neal': 26.43,
'Wilt Chamberlain': 26.13,
'Nikola Jokic': 25.95,
'Giannis Antetokounmpo': 25.72,
'Kareem Abdul-Jabbar': 24.58,
'Magic Johnson': 24.11,
'Kawhi Leonard': 22.41,
'Tim Duncan': 21.27,
'Kobe Bryant': 22.90,
'Stephen Curry': 23.79
}
plot = compare_era_pers(all_time_pers)
plt.savefig('career_per_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
# Statistical analysis
df = pd.DataFrame(list(all_time_pers.items()), columns=['Player', 'Career_PER'])
print("\nCareer PER Statistics:")
print(f"Mean: {df['Career_PER'].mean():.2f}")
print(f"Median: {df['Career_PER'].median():.2f}")
print(f"Std Dev: {df['Career_PER'].std():.2f}")
print(f"Range: {df['Career_PER'].min():.2f} - {df['Career_PER'].max():.2f}")
Example 4: Visualizing PER vs Other Advanced Metrics
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
def create_per_comparison_dashboard(player_data):
"""
Create a comprehensive dashboard comparing PER to other metrics.
Parameters:
player_data: DataFrame with columns ['Player', 'PER', 'BPM', 'VORP', 'WS']
"""
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
fig.suptitle('Player Efficiency Rating vs Other Advanced Metrics',
fontsize=16, fontweight='bold', y=1.00)
# PER vs BPM
axes[0, 0].scatter(player_data['PER'], player_data['BPM'],
alpha=0.6, s=100, color='steelblue', edgecolors='navy')
axes[0, 0].set_xlabel('PER', fontsize=11, fontweight='bold')
axes[0, 0].set_ylabel('Box Plus/Minus (BPM)', fontsize=11, fontweight='bold')
axes[0, 0].set_title('PER vs BPM', fontsize=12, fontweight='bold')
axes[0, 0].grid(alpha=0.3)
# Add trend line
z = np.polyfit(player_data['PER'], player_data['BPM'], 1)
p = np.poly1d(z)
axes[0, 0].plot(player_data['PER'], p(player_data['PER']),
"r--", alpha=0.8, linewidth=2)
# Calculate correlation
corr_per_bpm = player_data['PER'].corr(player_data['BPM'])
axes[0, 0].text(0.05, 0.95, f'Correlation: {corr_per_bpm:.3f}',
transform=axes[0, 0].transAxes, fontsize=10,
verticalalignment='top', bbox=dict(boxstyle='round',
facecolor='wheat', alpha=0.5))
# PER vs VORP
axes[0, 1].scatter(player_data['PER'], player_data['VORP'],
alpha=0.6, s=100, color='forestgreen', edgecolors='darkgreen')
axes[0, 1].set_xlabel('PER', fontsize=11, fontweight='bold')
axes[0, 1].set_ylabel('Value Over Replacement (VORP)', fontsize=11, fontweight='bold')
axes[0, 1].set_title('PER vs VORP', fontsize=12, fontweight='bold')
axes[0, 1].grid(alpha=0.3)
z = np.polyfit(player_data['PER'], player_data['VORP'], 1)
p = np.poly1d(z)
axes[0, 1].plot(player_data['PER'], p(player_data['PER']),
"r--", alpha=0.8, linewidth=2)
corr_per_vorp = player_data['PER'].corr(player_data['VORP'])
axes[0, 1].text(0.05, 0.95, f'Correlation: {corr_per_vorp:.3f}',
transform=axes[0, 1].transAxes, fontsize=10,
verticalalignment='top', bbox=dict(boxstyle='round',
facecolor='wheat', alpha=0.5))
# PER vs Win Shares
axes[1, 0].scatter(player_data['PER'], player_data['WS'],
alpha=0.6, s=100, color='darkorange', edgecolors='orangered')
axes[1, 0].set_xlabel('PER', fontsize=11, fontweight='bold')
axes[1, 0].set_ylabel('Win Shares (WS)', fontsize=11, fontweight='bold')
axes[1, 0].set_title('PER vs Win Shares', fontsize=12, fontweight='bold')
axes[1, 0].grid(alpha=0.3)
z = np.polyfit(player_data['PER'], player_data['WS'], 1)
p = np.poly1d(z)
axes[1, 0].plot(player_data['PER'], p(player_data['PER']),
"r--", alpha=0.8, linewidth=2)
corr_per_ws = player_data['PER'].corr(player_data['WS'])
axes[1, 0].text(0.05, 0.95, f'Correlation: {corr_per_ws:.3f}',
transform=axes[1, 0].transAxes, fontsize=10,
verticalalignment='top', bbox=dict(boxstyle='round',
facecolor='wheat', alpha=0.5))
# PER Distribution
axes[1, 1].hist(player_data['PER'], bins=20, color='mediumpurple',
edgecolor='indigo', alpha=0.7)
axes[1, 1].axvline(15.0, color='red', linestyle='--',
linewidth=2, label='League Avg (15.0)')
axes[1, 1].axvline(player_data['PER'].mean(), color='green',
linestyle='--', linewidth=2, label=f"Sample Avg ({player_data['PER'].mean():.1f})")
axes[1, 1].set_xlabel('PER', fontsize=11, fontweight='bold')
axes[1, 1].set_ylabel('Frequency', fontsize=11, fontweight='bold')
axes[1, 1].set_title('PER Distribution', fontsize=12, fontweight='bold')
axes[1, 1].legend(fontsize=9)
axes[1, 1].grid(axis='y', alpha=0.3)
plt.tight_layout()
return fig
# Sample data for top players
player_metrics = pd.DataFrame({
'Player': ['Nikola Jokic', 'Giannis Antetokounmpo', 'Luka Doncic',
'Joel Embiid', 'Damian Lillard', 'Stephen Curry',
'Kevin Durant', 'LeBron James', 'Anthony Davis',
'Jayson Tatum', 'Jimmy Butler', 'Kawhi Leonard'],
'PER': [32.85, 31.86, 27.80, 31.07, 23.56, 24.18,
24.76, 25.64, 27.42, 22.18, 23.45, 25.91],
'BPM': [13.7, 11.1, 8.9, 11.2, 5.8, 6.4,
6.7, 7.8, 7.9, 5.2, 5.6, 7.3],
'VORP': [9.8, 8.1, 6.7, 7.8, 4.2, 4.5,
4.8, 5.1, 5.3, 3.8, 3.9, 4.4],
'WS': [15.2, 13.4, 10.8, 12.6, 7.3, 8.1,
8.5, 8.9, 9.2, 7.1, 7.4, 8.3]
})
dashboard = create_per_comparison_dashboard(player_metrics)
plt.savefig('per_metric_comparison_dashboard.png', dpi=300, bbox_inches='tight')
plt.show()
# Print correlation matrix
print("\nCorrelation Matrix:")
print(player_metrics[['PER', 'BPM', 'VORP', 'WS']].corr().round(3))
R Code Examples
Example 1: Calculating PER in R
library(dplyr)
library(ggplot2)
# Function to calculate PER
calculate_per <- function(player_stats, team_stats, league_stats) {
# Calculate factor
factor <- (2/3) - (0.5 * (league_stats$ast / league_stats$fg)) /
(2 * (league_stats$fg / league_stats$ft))
# Calculate VOP (Value of Possession)
vop <- league_stats$pts / (league_stats$fga - league_stats$orb +
league_stats$tov + 0.44 * league_stats$fta)
# Calculate DRB% (Defensive Rebound Percentage)
drb_pct <- (league_stats$trb - league_stats$orb) / league_stats$trb
# Unadjusted PER components
uper <- (1 / player_stats$mp) * (
player_stats$fg3 +
(2/3) * player_stats$ast +
(2 - factor * (team_stats$ast / team_stats$fg)) * player_stats$fg +
player_stats$ft * 0.5 * (1 + (1 - (team_stats$ast / team_stats$fg)) +
(2/3) * (team_stats$ast / team_stats$fg)) -
vop * player_stats$tov -
vop * drb_pct * (player_stats$fga - player_stats$fg) -
vop * 0.44 * (0.44 + 0.56 * drb_pct) * (player_stats$fta - player_stats$ft) +
vop * (1 - drb_pct) * (player_stats$trb - player_stats$orb) +
vop * drb_pct * player_stats$orb +
vop * player_stats$stl +
vop * drb_pct * player_stats$blk -
player_stats$pf * ((league_stats$ft / league_stats$pf) -
0.44 * (league_stats$fta / league_stats$pf) * vop)
)
# Pace adjustment
aper <- (league_stats$pace / team_stats$pace) * uper
# Normalize to league average of 15.0
per <- aper * (15 / league_stats$aper)
return(per)
}
# Example usage
player <- list(
name = "Nikola Jokic",
mp = 2476,
fg = 778,
fga = 1367,
fg3 = 106,
ft = 459,
fta = 531,
orb = 241,
trb = 903,
ast = 589,
stl = 86,
blk = 56,
tov = 221,
pf = 171
)
team <- list(
fg = 3456,
ast = 2134,
pace = 99.8
)
league <- list(
pts = 114.2,
fga = 89.4,
orb = 10.3,
tov = 13.8,
fta = 23.1,
trb = 44.1,
ast = 25.3,
fg = 41.1,
ft = 17.8,
pf = 20.1,
pace = 99.5,
aper = 15.0
)
per_result <- calculate_per(player, team, league)
cat(sprintf("%s's PER: %.2f\n", player$name, per_result))
Example 2: Analyzing PER Trends Over Time in R
library(dplyr)
library(ggplot2)
library(tidyr)
# Create sample data for PER trends
per_trends <- data.frame(
Season = rep(2014:2024, each = 5),
Player = rep(c("LeBron James", "Stephen Curry", "Kevin Durant",
"Giannis Antetokounmpo", "Nikola Jokic"), 11),
PER = c(
# 2014-15
25.3, 23.8, 28.2, 19.5, 18.2,
# 2015-16
27.5, 28.0, 25.4, 21.8, 19.7,
# 2016-17
27.6, 26.4, 27.7, 24.6, 20.8,
# 2017-18
28.6, 26.4, 25.8, 26.8, 22.1,
# 2018-19
27.5, 24.5, 26.4, 29.3, 23.4,
# 2019-20
25.6, 23.9, 26.9, 31.9, 26.6,
# 2020-21
25.4, 25.5, 27.8, 31.1, 31.3,
# 2021-22
24.8, 24.6, 25.3, 29.4, 32.9,
# 2022-23
23.7, 24.3, 26.1, 28.8, 29.2,
# 2023-24
22.9, 23.1, 25.4, 27.3, 28.6,
# 2024-25 (projected)
21.5, 22.8, 24.9, 26.5, 27.8
)
)
# Create line plot
ggplot(per_trends, aes(x = Season, y = PER, color = Player, group = Player)) +
geom_line(size = 1.2) +
geom_point(size = 3) +
geom_hline(yintercept = 15, linetype = "dashed", color = "red", size = 1) +
annotate("text", x = 2014.5, y = 15.5, label = "League Avg (15.0)",
color = "red", size = 3.5, hjust = 0) +
scale_x_continuous(breaks = 2014:2024) +
scale_color_manual(values = c("#552583", "#1D428A", "#006BB6", "#00471B", "#0E2240")) +
labs(
title = "PER Trends: Elite Players (2014-2024)",
subtitle = "Tracking efficiency of NBA superstars over a decade",
x = "Season",
y = "Player Efficiency Rating (PER)",
color = "Player"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 16, face = "bold"),
plot.subtitle = element_text(size = 11),
axis.title = element_text(size = 12, face = "bold"),
legend.position = "bottom",
legend.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
)
# Summary statistics
per_summary <- per_trends %>%
group_by(Player) %>%
summarise(
Avg_PER = mean(PER),
Max_PER = max(PER),
Min_PER = min(PER),
SD_PER = sd(PER),
Trend = ifelse(PER[Season == 2024] > PER[Season == 2014], "Improving", "Declining")
) %>%
arrange(desc(Avg_PER))
print(per_summary)
Example 3: Position-Based PER Analysis in R
library(dplyr)
library(ggplot2)
library(tidyr)
# Sample data for positional PER analysis
position_per <- data.frame(
Player = c("Luka Doncic", "Damian Lillard", "Trae Young", "Stephen Curry",
"Devin Booker", "Jimmy Butler", "Kawhi Leonard", "Paul George",
"Kevin Durant", "Giannis Antetokounmpo", "LeBron James",
"Jayson Tatum", "Joel Embiid", "Nikola Jokic", "Anthony Davis",
"Bam Adebayo", "Rudy Gobert", "Draymond Green", "Jarrett Allen"),
Position = c("PG", "PG", "PG", "PG", "SG", "SG", "SG", "SF", "SF",
"SF", "SF", "SF", "C", "C", "C", "C", "C", "PF", "C"),
PER = c(27.8, 23.6, 22.4, 24.2, 21.3, 23.5, 25.9, 22.7, 24.8,
31.9, 25.6, 22.2, 31.1, 32.9, 27.4, 20.8, 19.2, 16.8, 18.9),
Minutes = c(2561, 2287, 2345, 2102, 2398, 2156, 1845, 2234, 2211,
2325, 2316, 2445, 2183, 2476, 2298, 2367, 2156, 2134, 2089)
)
# Boxplot by position
ggplot(position_per, aes(x = Position, y = PER, fill = Position)) +
geom_boxplot(alpha = 0.7, outlier.shape = NA) +
geom_jitter(width = 0.2, size = 3, alpha = 0.6) +
geom_hline(yintercept = 15, linetype = "dashed", color = "red", size = 1) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "PER Distribution by Position",
subtitle = "Centers tend to post higher PER due to rebounding and efficiency",
x = "Position",
y = "Player Efficiency Rating (PER)"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 14, face = "bold"),
plot.subtitle = element_text(size = 10),
axis.title = element_text(size = 11, face = "bold"),
legend.position = "none"
)
# Statistical summary by position
position_summary <- position_per %>%
group_by(Position) %>%
summarise(
Count = n(),
Mean_PER = mean(PER),
Median_PER = median(PER),
SD_PER = sd(PER),
Min_PER = min(PER),
Max_PER = max(PER)
) %>%
arrange(desc(Mean_PER))
print(position_summary)
# ANOVA to test if position affects PER
anova_result <- aov(PER ~ Position, data = position_per)
print(summary(anova_result))
Example 4: PER vs Win Share Regression in R
library(dplyr)
library(ggplot2)
library(broom)
# Sample data
player_metrics <- data.frame(
Player = c("Nikola Jokic", "Giannis Antetokounmpo", "Joel Embiid",
"Luka Doncic", "Stephen Curry", "LeBron James", "Kevin Durant",
"Anthony Davis", "Damian Lillard", "Kawhi Leonard", "Jimmy Butler",
"Jayson Tatum", "Devin Booker", "Trae Young", "Bam Adebayo"),
PER = c(32.85, 31.86, 31.07, 27.80, 24.18, 25.64, 24.76,
27.42, 23.56, 25.91, 23.45, 22.18, 21.30, 22.40, 20.80),
WS = c(15.2, 13.4, 12.6, 10.8, 8.1, 8.9, 8.5,
9.2, 7.3, 8.3, 7.4, 7.1, 6.5, 6.8, 6.2),
Position = c("C", "SF", "C", "PG", "PG", "SF", "SF",
"C", "PG", "SG", "SG", "SF", "SG", "PG", "C")
)
# Fit linear regression model
model <- lm(WS ~ PER, data = player_metrics)
model_summary <- summary(model)
# Create scatter plot with regression line
ggplot(player_metrics, aes(x = PER, y = WS)) +
geom_point(aes(color = Position), size = 4, alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE, color = "darkblue",
fill = "lightblue", alpha = 0.3) +
geom_text(aes(label = Player), vjust = -0.8, size = 3, check_overlap = TRUE) +
labs(
title = "PER vs Win Shares: Linear Relationship",
subtitle = sprintf("R² = %.3f, p-value < 0.001", model_summary$r.squared),
x = "Player Efficiency Rating (PER)",
y = "Win Shares (WS)",
color = "Position"
) +
theme_minimal() +
theme(
plot.title = element_text(size = 14, face = "bold"),
plot.subtitle = element_text(size = 10),
axis.title = element_text(size = 11, face = "bold"),
legend.position = "right"
)
# Print regression summary
print(tidy(model))
print(glance(model))
# Predictions
player_metrics$Predicted_WS <- predict(model, player_metrics)
player_metrics$Residual <- player_metrics$WS - player_metrics$Predicted_WS
# Players who outperform/underperform PER expectations
over_performers <- player_metrics %>%
arrange(desc(Residual)) %>%
head(5) %>%
select(Player, PER, WS, Predicted_WS, Residual)
under_performers <- player_metrics %>%
arrange(Residual) %>%
head(5) %>%
select(Player, PER, WS, Predicted_WS, Residual)
cat("\nTop 5 Overperformers (WS > Expected from PER):\n")
print(over_performers)
cat("\nTop 5 Underperformers (WS < Expected from PER):\n")
print(under_performers)
Practical Applications of PER
Understanding when and how to use PER is crucial for effective basketball analysis:
Contract Negotiations and Player Value
Teams often reference PER when evaluating player contracts, particularly for offensive-minded players. A player with consistently high PER (20+) can justify maximum or near-maximum contracts, while those with declining PER may face reduced offers.
MVP and Award Voting
PER frequently appears in MVP discussions, with most winners posting PER above 27. While not the sole criterion, exceptionally high PER (30+) strengthens MVP candidacy.
Trade Analysis
When evaluating trades, PER provides a quick comparison of player value. However, it should be combined with factors like age, contract status, team fit, and defensive ability for comprehensive assessment.
Draft Evaluation
College and international player PER can help identify prospects, though differences in competition level require careful interpretation. Translating college PER to NBA expectations involves significant adjustment.
Rotation Decisions
Coaches may reference PER when determining playing time, though on-court chemistry, defensive needs, and matchup considerations often override pure PER rankings.
Best Practices for Using PER
To use PER effectively, follow these guidelines:
- Never Use PER Alone: Always combine PER with other metrics (BPM, VORP, Win Shares) and qualitative analysis
- Account for Defense: Remember that PER is primarily an offensive metric; supplement with defensive metrics for complete evaluation
- Consider Context: Factor in team quality, role, playing time, and competition level
- Watch Sample Size: PER for players with very limited minutes can be misleading
- Understand Position Effects: Centers naturally post higher PERs; adjust expectations by position
- Track Trends: Single-season PER can fluctuate; look at multi-year trends for stability
- Combine with Film: Statistical analysis should complement, not replace, watching players perform
Conclusion
Player Efficiency Rating remains a valuable tool in the basketball analytics toolkit despite its limitations. Its simplicity, historical applicability, and comprehensive approach to offensive production make it a useful starting point for player evaluation. However, modern basketball analysis requires a multi-metric approach that accounts for defense, context, and factors PER cannot measure.
For analysts, the key is understanding what PER measures well (offensive production, per-minute efficiency, pace-adjusted performance) and what it misses (defense, team impact, intangibles). When used appropriately alongside other metrics and qualitative analysis, PER contributes meaningful insights to player evaluation and basketball understanding.
As the sport continues to evolve with new tracking technologies and analytical methods, PER's role may diminish, but its historical significance and the framework it established for thinking about player efficiency will remain important. For now, it continues to serve as a widely understood common language for discussing player performance across the basketball community.