The National Basketball Association, more commonly known as the NBA, is the world’s most premier basketball league with a staggering global presence, showcasing talent from all across the world. With teams in the United States and Canada, the association has elevated the sport from a game one plays outside with their friends to elite competitive displays of rhythm, art, strategy, and pure athleticism.
All 30 NBA Teams have one goal and desire in mind for every season, to win the championship. That is simply no easy task, though. 29 teams each year will always end the season unhappy. But without trying to construct a great, well-rounded, strong NBA team, that city’s franchise will go nowhere.
It is imperative that all 30 NBA General Managers fulfill their due dillegence to the players, the coaches, and the fans to display the best basketball tema that they can construct, the team that will have success, and hopefully the team that will win the NBA Finals, the championship.
The emergence of analytics has permitted team executives to have confidence in future moves that they desire to make in an effort to strategically improve the team, whether in the short- or long-run. With these analytics, these five constructed visualizations are informative for making decisions to win now, not for constructing the long plan with draft capital, but rather what elements of the sport are most necessary and imperative to team success, and have proven to be so.
Using Python, we have accessed a library called nba_api, which pulls live data from the official NBA stats website at stats.nba.com. Holding data from past seasons, and currently updating data from this ongoing season, 2025-26, we have a variety of important descriptive statistics to keep in mind.
Ther is data from 578 playres in this season, meaning 578 people were at one point a part of an NBA rsoter in 2025-26. There are a total of 30 NBA teams, which has been such since 2004-05.
The following calculatiosn are for NBA players who average at least 15 minutes a game out of the 48 total minutes. In 2025-26 (as of Apr. 5, 2026), an NBA player’s average field goal percentage and 3-point percentage are 47.2% and 33.2%, respectively. The average points per game is 12. Stocks is a collaquial term used for a player or team’s steals and blocks in a game. A player’s average stocks per game is 1.37.
The NBA team’s average offensive rating is 114.71, and the average defensive rating is 114.68. The average net rating for a team is 0.04.
Through the five visualizations below, I display the top priorities that an NBA team’s GM should have at the nexus of every move that they make in order for them to construct a winning NBA team sufficient to win the NBA Championship. These visualizations explain why these elements of the game have such high importance to team success, and have proven such success.
Our first visualization is a dual axis bar chart which shows, by position, us the average field goald percentage and average 3-point percentage for a player. As shown on the x-axis, the NBA labels their positions as either a Guard (G), Forward (F), or Center (C). Several players fit into two categories of positions; the first letter is the preferred positon of the two for that specific player (hence why G-F and F-G are two different categorizations).
A team wants players who can put the ball in the hoop, that is a given. But it is also a given that the three point shot is simply a more effieicnt shot than the long two-pointer or the mid-range. The positions on the x-axis are ordered from smallest to tallest. The discrepancy in 3-point percentage between the guards and forwards compared to the centers is notably large, and spacing the floor in today’s game is imperative. Having a big man who can reliably shoot the three pointer at a consistently above average rate, at the least, will act as a differentiator between your team and the rest.
Many successful teams as of late have had a center or power forward who cant stretch the floor, give more space to the ball handlers to operate, and create more shots that are more open. the 2018-19 Raptors, 2019-20 Lakers, 2022-23 Nuggets, and 2023-24 Celtics all had a center or power forward who had the green light to shoot threes, which helped propel each of those teams to a championship in recent years.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import plotly.graph_objects as go
import plotly.express as px
import time
import warnings
import requests
from nba_api.stats.endpoints import leaguedashplayerstats
stats = leaguedashplayerstats.LeagueDashPlayerStats(season='2025-26',
season_type_all_star='Regular Season')
df_2026 = stats.get_data_frames()[0]
df_2026_new = df_2026[['PLAYER_ID', 'PLAYER_NAME', 'TEAM_ID', 'TEAM_ABBREVIATION','GP', 'MIN', 'FGM', 'FGA', 'FG_PCT', 'FG3M', 'FG3A',
'FG3_PCT', 'FT_PCT', 'OREB', 'DREB', 'REB', 'AST', 'TOV', 'STL', 'BLK', 'BLKA',
'PF', 'PTS', 'PLUS_MINUS']]
from nba_api.stats.endpoints import playerindex
players = playerindex.PlayerIndex(season='2025-26')
players_df = players.get_data_frames()[0]
df_2026_pos = df_2026_new.merge(players_df[['PERSON_ID','POSITION']], left_on='PLAYER_ID', right_on='PERSON_ID', how='left')
df_2026_pos = df_2026_pos.drop(columns=['PERSON_ID'])
df_2026_pos = df_2026_pos.drop(columns=['BLKA'])
df_2026_pos['PPG'] = (df_2026_pos['PTS'] / df_2026_pos['GP']).round(1)
df_2026_pos['RPG'] = (df_2026_pos['REB'] / df_2026_pos['GP']).round(1)
df_2026_pos['APG'] = (df_2026_pos['AST'] / df_2026_pos['GP']).round(1)
df_2026_pos['SPG'] = (df_2026_pos['STL'] / df_2026_pos['GP']).round(1)
df_2026_pos['BPG'] = (df_2026_pos['BLK'] / df_2026_pos['GP']).round(1)
df_2026_pos['TPG'] = (df_2026_pos['TOV'] / df_2026_pos['GP']).round(1)
df_2026_pos['PFPG'] = (df_2026_pos['PF'] / df_2026_pos['GP']).round(1)
df_2026_pos = df_2026_pos[['PLAYER_NAME', 'POSITION', 'TEAM_ABBREVIATION', 'GP', 'MIN',
'FGM', 'FG_PCT', 'FG3M', 'FG3_PCT', 'FT_PCT', 'PPG', 'RPG', 'APG', 'SPG', 'BPG', 'TPG',
'PFPG','OREB', 'DREB', 'REB', 'PTS', 'AST', 'TOV', 'STL', 'BLK', 'PF',
'PLUS_MINUS', 'PLAYER_ID', 'TEAM_ID']]
df_2026_pos = df_2026_pos.rename(columns={'TEAM_ABBREVIATION': 'TEAM'})
df_2026_pos['MIN'] = df_2026_pos['MIN'].astype(int)
unique_positions = df_2026_pos['POSITION'].unique()
df_filtered = df_2026_pos[df_2026_pos['PPG'] >= 5]
def autolabel(these_bars, this_ax, piece_of_decimals, symbol):
for each_bar in these_bars:
height = each_bar.get_height()
this_ax.text(each_bar.get_x() + each_bar.get_width()/2, height * 1.01, symbol + format(height, piece_of_decimals),
fontsize = 11, color = 'black', ha = 'center', va = 'bottom')
custom_order = ['G', 'G-F', 'F-G', 'F', 'F-C', 'C-F', 'C']
df_filtered['POSITION'] = pd.Categorical(df_filtered['POSITION'], categories=custom_order, ordered=True)
fig = plt.figure(figsize = (18, 12))
ax1 = fig.add_subplot(1, 1, 1)
ax2 = ax1.twinx()
bar_width = 0.4
fg_by_pos = df_filtered.groupby('POSITION')['FG_PCT'].mean()
fg3_by_pos = df_filtered.groupby('POSITION')['FG3_PCT'].mean()
positions = fg_by_pos.index
fg_pct = fg_by_pos.values
fg3_pct = fg3_by_pos.values
x = np.arange(len(positions))
fg_bars = ax1.bar(x - (0.5 * bar_width), fg_pct, bar_width, color = 'blue',
edgecolor = 'black', label = 'Average FG%')
fg3_bars = ax2.bar(x + (0.5 * bar_width), fg3_pct, bar_width, color = 'orange',
edgecolor = 'black', label = 'Average 3FG%')
autolabel(fg_bars, ax1, '.3f', '')
autolabel(fg3_bars, ax2, '.3f', '')
fg = mpatches.Patch(color = 'blue', label = 'Average FG%')
three_fg = mpatches.Patch(color = 'orange', label = 'Average 3P FG%')
ax1.legend(loc = 'upper left', handles = [fg, three_fg], fontsize = 14)
ax1.set_ylim(0, .65)
ax2.set_ylim(0, .65)
ax1.set_xticks(x)
ax1.set_xticklabels(positions, fontsize = 14)
ax1.set_ylabel('Average %', fontsize = 15)
ax1.set_xlabel('Position', fontsize = 15)
ax1.set_title('Average FG% by Position', fontsize = 20)
ax2.set_ylabel('Average 3P%', fontsize = 15)
plt.show()
Our second vislauization travels to a team outlook on three-point shooting. Many games are played in n NBA season, 82 to be exact, and therefore numbers tend to average out to a tighter scale after so many games are played between all the teams. Yet, three point shooting prevals as an indicator of an at least good team.
The visualization shows all the three point percentage of all NBA teams this season, ordered by the winningnest teams at the top and the worst teams by record at the bottom. The teams are ordered on the y-axis by win percentage descending, so the best team in the NBA record, the Oklahoma City Thunder is at the top.
The average three point percentage for a team is shown by the vertical black dashed line. Not all the best teams surpass the average three point percentage for a team, but one can discover that all of the good teams, shonw in green, either teeter close to or surpass the average 3-point percentage of the league’s teams. Any noticeable dip below the average and you are now constructing an average to possibly one of the worst teams in the NBA.
There is no guarantee that only good 3-point shooting will take a team far in the playoffs, but it is certainly displayed to be a factor here since bad three point shooting is not present by any of the NBA’s best teams; they can all shoot.
But if three point shooting is not the end all be all, what else must a team pursue in today’s NBA to have any chance of success?
from nba_api.stats.endpoints import leaguestandingsv3
standings = leaguestandingsv3.LeagueStandingsV3(season='2025-26')
df_record = standings.get_data_frames()[0]
from nba_api.stats.endpoints import leaguedashteamstats
team_stats = leaguedashteamstats.LeagueDashTeamStats(season='2025-26', season_type_all_star='Regular Season')
df_team_stats = team_stats.get_data_frames()[0]
df_record = df_record.merge(df_team_stats[['TEAM_ID','FG3_PCT']], left_on='TeamID', right_on='TEAM_ID', how='left')
df_record.sort_values(by='WinPCT', ascending=False, inplace=True)
df_filtered_2 = df_record[['TeamName', 'FG3_PCT', 'WinPCT']]
def pick_colors_according_to_winpct(this_data):
colors = []
for each in this_data.WinPCT:
if each > 0.600:
colors.append('green')
elif each < 0.400:
colors.append('red')
else:
colors.append('skyblue')
return colors
winpct_colors = pick_colors_according_to_winpct(df_filtered_2)
teams = df_filtered_2['TeamName']
three_pct = df_filtered_2['FG3_PCT']
Above = mpatches.Patch(color = 'green', label = 'Above .600 Win%')
At = mpatches.Patch(color = 'skyblue', label = '.600 – .400 Win%')
Below = mpatches.Patch(color = 'red', label = 'Below .400 Win%')
plt.figure(figsize = (15,12))
bars = plt.barh(teams, three_pct, color = winpct_colors, edgecolor = 'black')
plt.gca().invert_yaxis()
plt.xlim(0.2, 0.5)
tick_locations = np.arange(0.20, 0.50, 0.05)
plt.xticks(tick_locations, [f"{x:.0%}" for x in tick_locations])
plt.xlabel('3PT Percentage')
plt.title('NBA Team 3PT% (Ordered by Win%)')
plt.legend(handles = [Above, At, Below], fontsize = 11)
avg_3pt = three_pct.mean()
plt.axvline(x = avg_3pt, color = 'black', linestyle='dashed', linewidth = 1, label = f'League Avg: {avg_3pt:.2%}')
plt.text(avg_3pt + .005, -1, 'League Avg: ' f'{avg_3pt:.2%}', rotation = 0, fontsize = 10)
for i, v in enumerate(three_pct):
plt.text(v + 0.004, i, f"{v:.1%}", va = 'center', ha = 'left', fontsize = 9, fontweight = 'bold', backgroundcolor = 'white')
plt.show()
Our third visualization shows the importance of having a guy on your team who can simply score the ball at an elite clip. Yes, having a well-rounded lineup is important, yes, we are in an era that is less top-heavy roster dominant. But, that is not to be confused with the idea that a team is not supposed to have an elite scorer, one who is preferably serviceable on defense.
This visualization shows the top scorer by points per game for the top 10 NBA teams by highest win percentage. It also shows the players’ stocks per game, a new term for a player’s combined steals and blocks.
For scoring, many bad teams have propelled themselves to becoming good teams because during their stretch of mediocrity, they had a guy who could simply score well, and they kept that player whilst amending other problems on the team. The Thunder and Pistons are two great examples. The Thunder’s Shai Gigeous Alexander is statistically the most efficient scorer this season, averaging 1,6 points every time he takes a field goal attempt, and they just won the finals last year. And the Piston’s Cade Cunningham, who was present when the Pistons started the 2023-24 season at a record-worst 2-26; they are now the top team in the Eastern conference.
The black dashed line shows the average points per game for all 30 NBA teams’ top scorer. It is clear that all the bets teams have a player who, for the first option on an NBA team, is at least average at scoring, average for the best player on a team.
Though 11 seasons ago, an anomaly of a “successful” team going against this mantra were the 2014-2015 Atlanta Hawks, a 60-win team where four players received All-Star honors; no player averaged 17 or more points a game. They were the number 1 seed in the Eastern Conference. They were swept 4-0 by the Cleveland Cavaliers, a team missing their second best player, but certainly led by their best player, LeBron James; he is a guy that can score.
All of these players also have decently good numbers at the least for steals and blocks per game. One outlier is the Spurs’ Victor Wembanyama, a 7’4” elite defender who can shoot threes. He averages more blocks than the Miami Heat (yes, an entire team), and can spread the floor, a guy who clearly differentiates the Spurs from all other NBA teams, hence why they are second place in the Western Conference; he is a great example of how a stretch big can make a team really great, as noted in the discussion of our first visualization. Defense is critical, as lightly highlighted here, but its importance will be further highlighted in our final two visuals.
leading_scorers = df_2026_pos.loc[df_2026_pos.groupby('TEAM_ID')['PPG'].idxmax()]
leading_scorers = leading_scorers.reset_index(drop=True)
leading_scorers = leading_scorers.merge(df_record[['TeamID', 'WinPCT']], left_on='TEAM_ID', right_on='TeamID').drop(columns='TeamID')
leading_scorers = leading_scorers.sort_values('WinPCT', ascending=False).reset_index(drop=True)
leading_scorers_30 = leading_scorers
leading_scorers = leading_scorers.head(10).reset_index(drop=True)
leading_scorers = leading_scorers[['PLAYER_NAME', 'TEAM', 'PPG', 'SPG', 'BPG', 'WinPCT']]
leading_scorers['PLAYER_TEAM'] = leading_scorers['PLAYER_NAME'] + ' (' + leading_scorers['TEAM'] + ')'
leading_scorers = leading_scorers[['PLAYER_NAME', 'TEAM', 'PLAYER_TEAM', 'PPG', 'SPG', 'BPG', 'WinPCT']]
leading_scorers['STOCKSPG'] = leading_scorers['SPG'] + leading_scorers['BPG']
leading_scorers = leading_scorers[['PLAYER_NAME', 'TEAM', 'PLAYER_TEAM', 'PPG', 'STOCKSPG', 'WinPCT']]
leading_scorers = leading_scorers[['PLAYER_TEAM', 'PPG', 'STOCKSPG', 'WinPCT']]
x = np.arange(len(leading_scorers))
width = 0.35
fig = plt.figure(figsize=(20, 12))
ax = fig.add_subplot(1, 1, 1)
bars1 = ax.bar(x - width/2, leading_scorers['PPG'], width, label='PPG', color='coral')
bars2 = ax.bar(x + width/2, leading_scorers['STOCKSPG'] * 8, width, label='Stocks PG (STL+BLK)', color='steelblue')
ax.set_title("Top 10 Winningest NBA Teams' Leading Scorer PPG & Stocks PG (2025-26)", fontsize=18)
ax.set_xlabel('Player (Team)', fontsize=14)
ax.set_ylabel('Points Per Game', fontsize=14)
ax.set_xticks(x)
ax.set_xticklabels(leading_scorers['PLAYER_TEAM'], rotation=15, ha='right', fontsize=12)
ax.tick_params(axis='y', labelsize=13)
ax.legend(fontsize=12)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax2 = ax.twinx()
ax2.set_ylim(ax.get_ylim()[0] / 8, ax.get_ylim()[1] / 8)
ax2.set_ylabel('Stocks Per Game (STL + BLK)', fontsize=14)
ax2.tick_params(axis='y', labelsize=13)
ax2.spines['top'].set_visible(False)
for bar in bars1:
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f'{bar.get_height():.1f}', ha='center', va='bottom', fontsize=10, color='black')
for bar, val in zip(bars2, leading_scorers['STOCKSPG']):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f'{val:.1f}', ha='center', va='bottom', fontsize=10, color='black')
mean_ppg = leading_scorers_30['PPG'].mean()
ax.axhline(mean_ppg, color='black', linestyle='dashed', linewidth=1.5)
ax.text(x[-1] - 1.8, mean_ppg + 6.7, f"Mean: All Teams Top Scorer PPG: {mean_ppg:.1f}", fontsize=12, color='black', fontstyle = 'oblique')
plt.show()
Our fourth visualization shows the importance of both a team’s offensive and defensive rating, not simply going all in on one side of the game or the other. Both ends are of equal importance, but here, this graph shows how important defense has been this season.
Here, we have a hoverable scatter lot of all 30 NBA Teams’ Offensive rating (plotted on the x-axis) and Defensive rating (plotted on the y-axis). A team’s offensive rating is its points per 100 possessions, and its defensive rating is its points allowed per 100 possessions. The y-axis is inverse for visual purposes. The color shade and size of circle both indicate the same variable the winning percentage of that team.
The best teams veer toward the top right, which are the teams that have demonstrated the ability to both score the ball and stop the ball. If one takes a close look, there are only a few teams that are above the rest in height, indicating better defense, and these teams have large green circles. Whereas on the offensive end, the x-axis, there are a few more teams that are slightly above average, below the very high-scoring teams. Though they all are a shade of green, they are not fully green, indicating that their win percentage is just a tad bit under what is desired, which is an eliter record shown by an eliter win percenatge that can be carried on into the playoffs.
The largest, most green circles here have elite defense, which must be upheld, to some extent, by a team’s best player, and therefore by proxy its leader (as shown in the last visual), as well as elevated by all other players on the team’s roster. Defense excellence will push a team above its competition. The Thunder, the largest, most green circle, won the championship last year, as previously noted.
from nba_api.stats.endpoints import LeagueDashTeamStats
ratings = LeagueDashTeamStats(measure_type_detailed_defense='Advanced', season='2025-26')
df_ratings = ratings.get_data_frames()[0]
df_ratings = df_ratings[['TEAM_ID', 'TEAM_NAME', 'OFF_RATING', 'DEF_RATING', 'NET_RATING', 'W_PCT']].copy()
circle_size = df_ratings['W_PCT'] * 60 + 10
fig = go.Figure()
fig.add_trace(go.Scatter(x = df_ratings['OFF_RATING'], y = df_ratings['DEF_RATING'], mode='markers',
marker=dict(size=circle_size, color=df_ratings['W_PCT'], colorscale='RdYlGn',
cmin=0.0, cmax=1.0, opacity=0.85, line=dict(width=1, color='black'),
colorbar=dict(title=dict(text='Win %', font=dict(size=14), side = 'top'),
tickvals=[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
ticktext=['0.000', '0.100', '0.200', '0.300', '0.400', '0.500',
'0.600', '0.700', '0.800', '0.900', '1.000'],
tickfont=dict(size=12), thickness=20, len=0.75)),
hovertemplate=('<b>%{customdata[0]}</b><br>' + 'OFF Rating: %{x}<br>' + 'DEF Rating: %{y}<br>' +
'Win PCT: %{customdata[1]:.3f}<br>' + 'Net Rating: %{customdata[2]:.1f}<extra></extra>'),
customdata=list(zip(df_ratings['TEAM_NAME'], df_ratings['W_PCT'], df_ratings['NET_RATING']))))
fig.update_layout(width=800, height=600, title=dict(text='NBA Teams Offensive & Defensive Rating (2025-26)<br>Circle size & color = Win %',
font=dict(size=18), x=0.5),
xaxis=dict(title=dict(text='Offensive Rating', font=dict(size=15)), tickfont=dict(size=13)),
yaxis=dict(title=dict(text='Defensive Rating', font=dict(size=15)), tickfont=dict(size=13), autorange='reversed'),
template='simple_white', showlegend=False)
fig.write_html('fourth_visualization.html', include_plotlyjs='cdn')
Our fifth visual hammers in the importance of defense and expands on the offensive and defenseive rating, showing the metric that represents the difference of the two ratings, a team’s net rating.
On this heatmap, one can see a history of the last eight NBA seasons and the five teams that averaged the most stocks per game, as well as the five teams that averaged the least stocks per game. It is imperative to evaluate both ends of the spectrum, to not just see what successful teams have, but what they have that under-performing teams do not.
Each row is an NBA season, each box is an NBA team, each number is the stocks per game that that particular team averaged that season, and the color in that box signifies the strength of their net rating. The leftmost box is the team with the most stocks per gae that season, and the opposite is true for the rightmost box, representing the team with the lowest stocks per game.
As touched on earlier with offensive and defensive rating, the net rating of a team is the difference between the team’s offensive and defensive ratings, which therefore represents a teams point differential per 100 possessions. This metric removes any bias that a win in the win column may give, as point differential demonstrates dominance o the court (a win does not signify whether a team won by 2 or 30, this metric does show that on average).
It is evident that the left side of the heatmap filled with the teams which average the most stocks are more times than not a high-performing team, which more shades of green present over the past eight seasons. Teams with high stocks per game end up being rewarded by those contributions with less points allowed on defense and in turn a higher net rating, or at least the avoidance of a poor net rating, which no winning team ever has. A team that gets a lot of stocks is by proxy avoiding, at a much higher rate, having a bad net rating, as shown by the pool of red on the right side of the graph, with no evidence of string or slightly strong green shades, indicating truly good NBA teams. The defensive side of the ball is simply that crucial to tangible success.
seasons = ['2018-19', '2019-20', '2020-21', '2021-22', '2022-23', '2023-24', '2024-25', '2025-26']
# all_data = []
# for season in seasons:
# print(f'Fetching {season}...')
# base = LeagueDashTeamStats(season=season,
# measure_type_detailed_defense='Base',
# per_mode_detailed='PerGame')
# stocks_df = base.get_data_frames()[0][['TEAM_ID', 'TEAM_NAME', 'STL', 'BLK']]
# time.sleep(0.7)
#
# adv = LeagueDashTeamStats(season=season,
# measure_type_detailed_defense='Advanced')
# net_df = adv.get_data_frames()[0][['TEAM_ID', 'NET_RATING']]
# time.sleep(0.7)
#
# merged = stocks_df.merge(net_df, on='TEAM_ID')
# merged['STOCKS_PG'] = merged['STL'] + merged['BLK']
# merged['SEASON'] = season
# all_data.append(merged)
#
# df_stocks_net_years = pd.concat(all_data, ignore_index=True)
# df_stocks_net_years.to_csv('C:/Users/samme/stocks_net_years.csv', index=False)
df_stocks_net_years = pd.read_csv('C:/Users/samme/stocks_net_years.csv')
rows = []
for season in seasons:
s = (df_stocks_net_years[df_stocks_net_years['SEASON'] == season]
.sort_values('STOCKS_PG', ascending=False)
.reset_index(drop=True))
# top 5
for i, (_, row) in enumerate(s.head(5).iterrows()):
rows.append({**row, 'RANK_LABEL': f'Top {i+1}'})
# bottom 5
for i, (_, row) in enumerate(s.tail(5).sort_values('STOCKS_PG', ascending=False).iterrows()):
rows.append({**row, 'RANK_LABEL': f'Bot {i+1}'})
df_stocks_topbot5 = pd.DataFrame(rows)
df_stocks_topbot5 = df_stocks_topbot5.drop(columns=['STL', 'BLK'])
hm_stocks_net = df_stocks_topbot5.pivot(index='SEASON', columns='RANK_LABEL', values='NET_RATING')
col_order = ['Top 1','Top 2','Top 3','Top 4','Top 5','Bot 1','Bot 2','Bot 3','Bot 4','Bot 5']
hm_stocks_net = hm_stocks_net[col_order].iloc[::-1]
fig = px.imshow(hm_stocks_net, color_continuous_scale='RdYlGn', color_continuous_midpoint=0,
text_auto=False, aspect='auto',
title='Top 5 vs. Bottom 5 NBA Teams in Stocks Per Game (2018-19 to 2025-26),<br>Heat = Net Rating')
hover_text = df_stocks_topbot5.pivot(index='SEASON', columns='RANK_LABEL', values='TEAM_NAME')[col_order]
stocks_text = df_stocks_topbot5.pivot(index='SEASON', columns='RANK_LABEL', values='STOCKS_PG')[col_order]
fig.update_traces(text=stocks_text.values.round(2), texttemplate='%{text}', textfont=dict(size=13),
customdata=np.dstack([hover_text.values, stocks_text.values, hm_stocks_net.values]),
hovertemplate=('<b>%{customdata[0]}</b><br>'
'Stocks PG: %{customdata[1]:.2f}<br>'
'Net Rating: %{customdata[2]:.1f}<extra></extra>'))
fig.update_layout(width=800, height=600, title=dict(font=dict(size=14), x=0.5),
xaxis=dict(title='', side='top', tickfont=dict(size=12)),
yaxis=dict(title='Season', tickfont=dict(size=12), autorange='reversed'),
coloraxis_colorbar=dict(title=dict(text='Net Rating', side='top'), tickfont=dict(size=11)))
fig.add_shape(type='line', x0=4.5, x1=4.5, y0=-0.5, y1=len(seasons) - 0.5,
line=dict(color='black', width=4))
fig.write_html('fifth_visualization.html', include_plotlyjs='cdn')
There we have it, a bundle of visualizations that demonstrate the same shift of a GM’s prioritization of objectives as has been represented in the shift of successful NBA team-building.
Successful NBA teams are not top-heavy anymore with merely a couple of shooters on the floor, just as a GM should not just prioritize one specific element of the game whilst letting all other parts be only good enough. Nor am I asking a GM to realistically obtain the best everything.
Just as good teams are built nowadays, there must be an all-around outlook that hits on a handful of key elements (3-point shooting, points per game, stocks, offenseive, defensive, and net rating) whilst still having “that guy” on your team, the guys that you can rely on to take the last shot, the guy that you knwo can get you a bucket when down by two. But as just noted, those other key elements are crucial; they must be fulfilled to have any sort of tangible NBA success.
A team without any one of those key elements will have a difficult time making far in the playoffs. Without an elite scorer, good 3-pount shooting from most of your roster, or above average defensive presence, a good NBA team cannot be constructed. As a GM, which one of these elements will you hit on first to construct your successful, winning NBA team? Do you believe any of the elements are more important than the other in terms of creating success? Nonetheless, you, the GM, must tackle all these areas in order to manage a winning team in the NBA. Do you have what it takes?