All-NBA Predict #8 – Exploration of Historical NBA Team Rating Margins

This will be a quick post where I’m just curious to see which teams in history had the largest ORtg – DRtg margin. Theoretically, these were the most dominant teams because they played the balance between the best offense and the best defense, and should have won the most games and performed the absolute best in terms of W/L%. Remember back in the first post where we drew the Rtg Margin vs W/L% line? It was pretty much exactly linear.

In [79]:
%load_ext rpy2.ipython
The rpy2.ipython extension is already loaded. To reload it, use:
  %reload_ext rpy2.ipython
In [80]:
# Load libraries & initial config
In [81]:
# Load libraries & initial config
%matplotlib nbagg
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import boto3
from StringIO import StringIO
import warnings
In [82]:
# Retrieve team stats from S3
teamAggDfToAnalyze = pd.read_csv('', index_col = 0)

pd.set_option('display.max_rows', len(teamAggDfToAnalyze.dtypes))
print teamAggDfToAnalyze.dtypes
baseStats_Season                object
perGameStats_Tm                 object
baseStats_W                      int64
baseStats_L                      int64
baseStats_WLPerc               float64
baseStats_SRS                  float64
baseStats_Pace                 float64
baseStats_Rel_Pace             float64
baseStats_ORtg                 float64
baseStats_Rel_ORtg             float64
baseStats_DRtg                 float64
baseStats_Rel_DRtg             float64
perGameStats_Age               float64
perGameStats_MP                float64
perGameStats_FG                float64
perGameStats_FGA               float64
perGameStats_FGPerc            float64
perGameStats_2P                float64
perGameStats_2PA               float64
perGameStats_2PPerc            float64
perGameStats_3P                float64
perGameStats_3PA               float64
perGameStats_3PPerc            float64
perGameStats_FT                float64
perGameStats_FTA               float64
perGameStats_FTPerc            float64
perGameStats_ORB               float64
perGameStats_DRB               float64
perGameStats_TRB               float64
perGameStats_AST               float64
perGameStats_STL               float64
perGameStats_BLK               float64
perGameStats_TOV               float64
perGameStats_PF                float64
perGameStats_PTS               float64
opponentPerGameStats_FG        float64
opponentPerGameStats_FGA       float64
opponentPerGameStats_FGPerc    float64
opponentPerGameStats_2P        float64
opponentPerGameStats_2PA       float64
opponentPerGameStats_2PPerc    float64
opponentPerGameStats_3P        float64
opponentPerGameStats_3PA       float64
opponentPerGameStats_3PPerc    float64
opponentPerGameStats_FT        float64
opponentPerGameStats_FTA       float64
opponentPerGameStats_FTPerc    float64
opponentPerGameStats_ORB       float64
opponentPerGameStats_DRB       float64
opponentPerGameStats_TRB       float64
opponentPerGameStats_AST       float64
opponentPerGameStats_STL       float64
opponentPerGameStats_BLK       float64
opponentPerGameStats_TOV       float64
opponentPerGameStats_PF        float64
opponentPerGameStats_PTS       float64
season_start_year                int64
rtg_diff                       float64
dtype: object
In [83]:
teamAggDfToAnalyze['rtg_margin'] = teamAggDfToAnalyze['baseStats_ORtg'] - teamAggDfToAnalyze['baseStats_DRtg']
# Scatter of Rtg margin vs W/L%
teamAggDfToAnalyze.plot(kind = 'scatter', x = 'rtg_margin', y = 'baseStats_WLPerc', title = 'Rtg Margin vs W/L%')
<matplotlib.axes._subplots.AxesSubplot at 0xd6ffc18>

Doing our quick sanity check, we see that the largest margin on this graph definitely corresponds to the highest win percentages, and vice versa. I’m going to also take note of the ORtg and DRtg’s to see whether the best offensive or defensive teams that we analyzed earlier are also in these top 10s.

In [84]:
# This function adjusts the input stats for pace and outputs per 100 possession metrics
def paceConversion(df, listOfFields):
    for field in listOfFields:
        df['{}_per_100_poss'.format(field)] = (100/df['baseStats_Pace'])*(48/(df['perGameStats_MP']/5))*df[field]
    return df

# Select a subset of columns to manage size of dataframe
teamAggDfToAnalyzeSelectedColumns = teamAggDfToAnalyze[[

# Pace adjust the following metrics
teamAggDfToAnalyzePaceAdjusted = paceConversion(
In [85]:
# Drop all seasons before the 80's
teamAggDfToAnalyzePaceAdjusted19791998Excl = teamAggDfToAnalyzePaceAdjusted[(teamAggDfToAnalyzePaceAdjusted['season_start_year'] >= 1979) & (teamAggDfToAnalyzePaceAdjusted['season_start_year'] != 1998)]
print 'There have been {} teams in NBA history starting from the 79-80 season, excluding the 98-99 season'.format(teamAggDfToAnalyzePaceAdjusted19791998Excl.shape[0])
There have been 1015 teams in NBA history starting from the 79-80 season, excluding the 98-99 season
In [89]:
# Set variable top N teams to take
topN = 10

# Order by DRtg ascending (lower DRtg is better) and capture DRtg ranking throughout teams in history
teamAggDfToAnalyzePaceAdjusted19791998Excl.sort_values('baseStats_DRtg', ascending = True, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.reset_index(inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.drop('index', 1, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl['drtg_ranking'] = teamAggDfToAnalyzePaceAdjusted19791998Excl.index

# Order by ORtg descending (higher ORtg is better) and capture ORtg ranking throughout teams in history
teamAggDfToAnalyzePaceAdjusted19791998Excl.sort_values('baseStats_ORtg', ascending = False, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.reset_index(inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.drop('index', 1, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl['ortg_ranking'] = teamAggDfToAnalyzePaceAdjusted19791998Excl.index

# # Order by W/L% descending (higher % is better) and capture W/L% ranking throughout teams in history
teamAggDfToAnalyzePaceAdjusted19791998Excl.sort_values('baseStats_WLPerc', ascending = False, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.reset_index(inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.drop('index', 1, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl['wlperc_ranking'] = teamAggDfToAnalyzePaceAdjusted19791998Excl.index

# Order by ORtg-DRtg margin ascending (higher margin is better)
teamAggDfToAnalyzePaceAdjusted19791998Excl.sort_values('rtg_margin', ascending = False, inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.reset_index(inplace = True)
teamAggDfToAnalyzePaceAdjusted19791998Excl.drop('index', 1, inplace = True)

# We'll give each team a unique identifier based on the year and team because some teams repeat (CHI)
teamAggDfToAnalyzePaceAdjusted19791998Excl['unique_team_id'] = teamAggDfToAnalyzePaceAdjusted19791998Excl['season_start_year'].astype(str) + '-' + teamAggDfToAnalyzePaceAdjusted19791998Excl['perGameStats_Tm'].astype(str)

# Extract top 10 teams
teamAggDfToAnalyzeTiersTop = teamAggDfToAnalyzePaceAdjusted19791998Excl.head(topN)

# Extract bottom 10 teams
teamAggDfToAnalyzeTiersBottom = teamAggDfToAnalyzePaceAdjusted19791998Excl.tail(topN)

print teamAggDfToAnalyzeTiersTop[[
  unique_team_id  rtg_margin  baseStats_ORtg  ortg_ranking  baseStats_DRtg  \
0       1995-CHI        13.4           115.2             5           101.8   
1       1996-CHI        12.0           114.4            14           102.4   
2       2016-GSW        11.7           115.7             0           104.0   
3       2015-SAS        11.3           110.3           169            99.0   
4       2007-BOS        11.3           110.2           174            98.9   
5       1991-CHI        11.0           115.5             2           104.5   
6       2015-GSW        10.7           114.5            13           103.8   
7       2014-GSW        10.2           111.6            87           101.4   
8       2008-CLE        10.0           112.4            55           102.4   
9       2012-OKC         9.8           112.4            52           102.6   

   drtg_ranking  baseStats_WLPerc  wlperc_ranking  
0            97             0.878               1  
1           126             0.841               2  
2           237             0.821               3  
3            19             0.817               7  
4            17             0.805              11  
5           279             0.817               5  
6           219             0.890               0  
7            78             0.817               9  
8           125             0.805              10  
9           143             0.732              57  
In [110]:
%%R -i teamAggDfToAnalyzeTiersTop

# Opponent 2PA Scatter
        x = baseStats_ORtg,
        y = baseStats_DRtg,
        color = rtg_margin,
        label = unique_team_id
) + 
geom_point(size = 7) +
geom_text(hjust = -0.3) +
ggtitle("Top 10 Historical Teams by ORtg / DRtg") +
scale_x_continuous(expand = c(0, 2)) +

The graph above shows where the teams ranked all-time in terms of offensive and defensive rating rankings. E.g. the 2016-GSW were 3rd all time offensively, and 237th all time defensively. The 1995-bulls are the closest to being the best offensive and defensive team of all time simultaneously.

Notes in no particular order:

  • None of the top 10 defensive teams are on this list, while a few of the top 10 offensive teams are
  • Teams on this list generally rank higher in ORtg than DRtg
  • Michael Jordan & Steph Curry account for 60% of this list… in general all-time elite offense and great defense
  • BOS & SAS teams made this list off all-time elite defense and great offense
  • CLE & OKC teams made this list off great offense and great defense

I need to watch a Chicago game and a warriors game… brb.


Basically, the warriors have literally been a top 10 team for 3 years running now. Seriously, I hear it all the time, but for me to be watching one of the best regular season*** dynasties develop right in front my EYES? LITTLE OLD ME? It just seems a bit surreal. But no, there you go, GSW is up there right with the likes of Chicago.

A quick shoutout to the 2015 Spurs as well though. While the Warriors were going 73-9, the Spurs destroying their opponents by an EVEN LARGER MARGIN IN THE SAME SEASON. Goodness, we truly were blessed to be watching basketball that year (or redditing). Neither ORtg nor DRtg nor margin between the two actually dictates winning precisely. That SAS team trailed the warriors in wins that season (although still racking up a 67 wins themselves) despite leading the warriors here, and look no further than that OKC team as the only team who didn’t win 80% of their games in the top 10 in rating margin. This means that when they won, they were blowing teams out. When they lost, they had lost by a slim margin, but they lost more than other teams on here.

Now, back to the warriors. I just watched a warriors clippers regular season game from December 2016 after the warriors picked up KD, and oh my goodness. It’s a different beast watching these guys compared to your SAS or DET.

First thing I noticed: PACE. How many times did GSW jack up a 3 well before the shot clock expired? There’s one play in the first quarter where KD put up a shot from like 30-35 ft out… WHY?! So many offensive weapons on that team and THAT’S the shot you go with? These GSW and HOU teams have really spoiled us, but you still can’t shake that some of these are just horrible shots. Anyways, these seem to speed up the pace and definitely cause more running up and down the court. These guys are conditioned, which leads me to my second observation:

TRANSITION POINTS… My god, how many transition points did the warriors get here. A poke here, a block there, just pushing and pushing the ball up the court in every scenario before the clippers had any chance of setting up their defense. I feel like the first half was mostly transition points. I’m sure that’s a bias I have as an excited viewer and, in reality, there probably weren’t as many as I remember, but man I just remember the warriors running circles around the clippers. The warriors ended up winning this game by 17 this game, and they were showing signs of dominance from the get go.

The warriors shot 60% from 2PA. I think that’s a clear sign of the types of shots they were getting… i.e. tons of layups, close shots, easy shots. They actually weren’t really hitting their 3’s, Steph going 0-8 and KD going 1-5, but I guess shooting 60% on the rest of their FGA made up for that.

That’s only one side of the story. Look at the defense. 11 steals and 5 blocks to the clipper’s 3 and 2. That was the root of most of those transition points (not to mention on certain inbounds they just pushed it and Steph Curry is so goddamn quick that he either got the shot he wanted or found the right guy open). The warriors still had 10 turnovers, but only 3 of those were clipper steals. That means on 7 of those turnovers, the clippers inbounded the ball, the warriors got a chance to set up their defense, and played beautiful set defense as well. Really great switching, and on one play where (I think) KD missed a switch or a box out, you could see Draymond ragging on him right after. Great defensive presence both physically and mentally.

When the warriors destroy the best defensive team in the NBA at the time by nearly 20 while Steph goes 0-8… one can infer how great these warriors are and start to see why they belong in the top 10 all time.


After watching the warriors game, I watched the 1996 finals game 6 between the bulls and the supersonics. This was actually the first game I’ve ever watched of these supersonics. I actually didn’t even know they made it to the finals at one point. I’m sure I had read that somewhere in the past, but it really didn’t stick with me. I guess there’s a reason why GP and Kemp were such big names, other than GP getting a ton of defensive accolades. Anyways, back to the game. Boy, was the game different back then. I didn’t feel there was a sense of pushing the ball that much. Chicago played guys like Luc Longley and John Salley, while the sonics played Sam Perkins at center most of the time. These guys moved so slow, and when you compare them to a GSW team today with guys like Green and even McGee in the middle, they just look longer and more athletic than those guys. Now, you always hear the saying the game was different the game was different, and you could kind of see that transition happen – even when I watched the Shaq game of the mid 2000’s when he was on Miami, you could see how quick Shaq was and that he could keep up with guys like Dwyane Wade, Eddie Jones, Keyon Dooling… Even their backup Alonzo Mourning was quick and athletic. I look at a guy like Luc Longley and, while I don’t know exactly how important he was to those bulls teams (he looked skilled around the rim), I wonder about how he would have fared against a modern day GSW. I guess that’s why people talk about how teams can match the modern day small ball.

First thing I notice, PACE. The score here was 87-75. The score in the warriors-clippers game was 115-98. Stark difference here for sure.

Let’s adjust for pace. Chicago had an ORtg / DRtg of 107-92. The warriors had 120-102.

One thing I noticed in the bulls-sonics game was that both teams were shooting pretty badly. Jordan missed a ton of shots. Perkins missed a ton of shots. Both teams shot around 40% overall. Two key differences between the two teams, however:

  • Jordan got to the line and made 11-12. Huge.
  • Dennis Goddamn Rodman with 11 offensive boards…

Here we go, let’s start another offensive boards discussion. Honestly, you REALLY have to watch a game to understand the impact of offensive boards. This sucks for me because this seems like something that numbers will never be able to explain. Every time Dennis Rodman did anything, the crowd goes nuts. The number of ORB, tip ins, charges taken… he almost single-handedly drove the crowd and got into the opponent’s heads. At one point, Kemp shoved Rodman for an offensive foul and it was clearly a shove of frustration. Rodman gets up, laughs, claps his hands, runs the other way just smiling. That’s some gangster shit, and to me, that was one of the moments in the game that solidified the win for me. It wasn’t a shot, it wasn’t a block, it wasn’t a steal, but it was something someone did that will never really be decipherable in the box score.

How much of a factor did this play in CHI’s entire season? Was Rodman doing this the whole time? Truth be told, I didn’t feel either team’s defense was that great this game. I’ll admit naivety here because, again, I haven’t watched that many 80’s and 90’s games. Everybody looked pretty slow to me, there were usually guys open which means people got caught on doubles or switches. I’m not ready to say that modern day GSW could defend these teams because I’d be stupid to make such a statement without doing more research, but, again, the teams just seemed slow. Whether that was good coaching, or just the style of play at the time, or what the players had been conditioned to do, it seemed lacking. On the flip side, the mind game seemed to have more of an impact on the game than defense.

It’s worth just taking a second and looking at Rodman’s statline this game, no explanation, just marvel:

  • 4-9 FG, 11 ORB, 8 DRB, 5 AST, 3 STL, 1 BLK

Back to ORtg, both teams had ~20 turnovers (yikes…), and the bulls had about ~10 more scoring opportunities, pretty much directly correlating to the ~10 ORB that bulls had over the sonics. I thought I’d be going on a huge rant about Jordan, but instead it’s clearly about Rodman. I feel like Jordan in his HOF speech.


Anyways, this was really a side-exercise for me to dive a bit deeper into offensive and defensive efficiencies and further get a sense of the benchmarks I should be looking for in terms of measures and the values of those measures. It also allowed me to put some explanations to those numbers in seeing what leads up to those numbers and what follows those numbers in real game situations. I think I’ll start taking a look at maybe the coaches or the players next. We’ll see.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s