Welcome to the primary ever basketball put up on this right here weblog! As introduced a couple of weeks again, CollegeBasketballData.com is now dwell. I’ve usually been requested about offering service for faculty basketball and have all the time been hesitant. For one, the sheer quantity of information is a number of instances higher than for soccer as a consequence of almost triple the variety of groups and triple the variety of video games per group. I’ve additionally been an enormous fan each of Bart Torvik and Ken Pomeroy and wasn’t certain there was a lot of want for a CFBD-like service for CBB with the stats and analytics these guys present.
That each one mentioned, I’ve been requested persistently over time from numerous customers and the CFBD web site and API refreshes have made me energized to offer CBB a go. I am excited to supply this service and if I have been part of your CFB analytics journey, I hope I can do the identical for CBB.
Now let’s dive into some charts!
We’re going to be plotting group shot charts on prime of a typical NCAA males’s courtroom utilizing Python and the CollegeBasketballData.com API together with a couple of widespread Python packages. When all is claimed and accomplished, we could have one thing that appears like this.
Earlier than we do something, we’d like to ensure now we have all dependencies put in. We are going to want the CBBD Python package deal and some others. Run the next code in terminal.
pip set up cbbd pandas numpy matplotlib seaborn
Now we have to concentrate on plotting a basketball courtroom. We shall be utilizing matplotlib to attain this. Go forward and run the next block to import all of dependencies we simply put in.
import cbbd
import pandas as pd
import numpy as np
import matplotlib.pylab as plt
import matplotlib as mpl
from matplotlib.patches import Circle, Rectangle, Arc
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import seaborn as sns
plt.type.use(‘seaborn-v0_8-dark-palette’)
As we did into plotting the courtroom, I first want to offer an enormous shout out to Rob Mulla, who wrote a sequence of helper features for plotting NCAA courts on Kaggle. His Kaggle article goes extra in-depth and even features a plot for a full measurement courtroom. We’ll simply be utilizing a half courtroom and duplicate/pasting a perform from that article.
def create_ncaa_half_court(ax=None, three_line=”mens”, court_color=”#dfbb85″,
lw=3, lines_color=”black”, lines_alpha=0.5,
paint_fill=”blue”, paint_alpha=0.4,
inner_arc=False):
“””
Model 2020.2.19
Creates NCAA Basketball Half Court docket
Dimensions are in toes (Court docket is 97×50 ft)
Created by: Rob Mulla / https://github.com/RobMulla
* Notice that this perform makes use of “toes” because the unit of measure.
* NCAA Knowledge is supplied on a x vary: 0, 100 and y-range 0 to 100
* To plot X/Y positions first convert to toes like this:
“`
Occasions[‘X_’] = (Occasions[‘X’] * (94/100))
Occasions[‘Y_’] = (Occasions[‘Y’] * (50/100))
“`
ax: matplotlib axes if None will get present axes utilizing `plt.gca`
three_line: ‘mens’, ‘womens’ or ‘each’ defines 3 level line plotted
court_color : (hex) Colour of the courtroom
lw : line width
lines_color : Colour of the traces
lines_alpha : transparency of traces
paint_fill : Colour contained in the paint
paint_alpha : transparency of the “paint”
inner_arc : paint the dotted internal arc
“””
if ax is None:
ax = plt.gca()
# Create Pathes for Court docket Traces
center_circle = Circle((50/2, 94/2), 6,
linewidth=lw, colour=lines_color, lw=lw,
fill=False, alpha=lines_alpha)
hoop = Circle((50/2, 5.25), 1.5 / 2,
linewidth=lw, colour=lines_color, lw=lw,
fill=False, alpha=lines_alpha)
# Paint – 18 Ft 10 inches which converts to 18.833333 toes – gross!
paint = Rectangle(((50/2)-6, 0), 12, 18.833333,
fill=paint_fill, alpha=paint_alpha,
lw=lw, edgecolor=None)
paint_boarder = Rectangle(((50/2)-6, 0), 12, 18.833333,
fill=False, alpha=lines_alpha,
lw=lw, edgecolor=lines_color)
arc = Arc((50/2, 18.833333), 12, 12, theta1=-
0, theta2=180, colour=lines_color, lw=lw,
alpha=lines_alpha)
block1 = Rectangle(((50/2)-6-0.666, 7), 0.666, 1,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
block2 = Rectangle(((50/2)+6, 7), 0.666, 1,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
ax.add_patch(block1)
ax.add_patch(block2)
l1 = Rectangle(((50/2)-6-0.666, 11), 0.666, 0.166,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
l2 = Rectangle(((50/2)-6-0.666, 14), 0.666, 0.166,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
l3 = Rectangle(((50/2)-6-0.666, 17), 0.666, 0.166,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
ax.add_patch(l1)
ax.add_patch(l2)
ax.add_patch(l3)
l4 = Rectangle(((50/2)+6, 11), 0.666, 0.166,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
l5 = Rectangle(((50/2)+6, 14), 0.666, 0.166,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
l6 = Rectangle(((50/2)+6, 17), 0.666, 0.166,
fill=True, alpha=lines_alpha,
lw=0, edgecolor=lines_color,
facecolor=lines_color)
ax.add_patch(l4)
ax.add_patch(l5)
ax.add_patch(l6)
# 3 Level Line
if (three_line == ‘mens’) | (three_line == ‘each’):
# 22′ 1.75″ distance to middle of hoop
three_pt = Arc((50/2, 6.25), 44.291, 44.291, theta1=12,
theta2=168, colour=lines_color, lw=lw,
alpha=lines_alpha)
# 4.25 toes max to sideline for mens
ax.plot((3.34, 3.34), (0, 11.20),
colour=lines_color, lw=lw, alpha=lines_alpha)
ax.plot((50-3.34, 50-3.34), (0, 11.20),
colour=lines_color, lw=lw, alpha=lines_alpha)
ax.add_patch(three_pt)
if (three_line == ‘womens’) | (three_line == ‘each’):
# womens 3
three_pt_w = Arc((50/2, 6.25), 20.75 * 2, 20.75 * 2, theta1=5,
theta2=175, colour=lines_color, lw=lw, alpha=lines_alpha)
# 4.25 inches max to sideline for mens
ax.plot( (4.25, 4.25), (0, 8), colour=lines_color,
lw=lw, alpha=lines_alpha)
ax.plot((50-4.25, 50-4.25), (0, 8.1),
colour=lines_color, lw=lw, alpha=lines_alpha)
ax.add_patch(three_pt_w)
# Add Patches
ax.add_patch(paint)
ax.add_patch(paint_boarder)
ax.add_patch(center_circle)
ax.add_patch(hoop)
ax.add_patch(arc)
if inner_arc:
inner_arc = Arc((50/2, 18.833333), 12, 12, theta1=180,
theta2=0, colour=lines_color, lw=lw,
alpha=lines_alpha, ls=”–“)
ax.add_patch(inner_arc)
# Restricted Space Marker
restricted_area = Arc((50/2, 6.25), 8, 8, theta1=0,
theta2=180, colour=lines_color, lw=lw,
alpha=lines_alpha)
ax.add_patch(restricted_area)
# Backboard
ax.plot(((50/2) – 3, (50/2) + 3), (4, 4),
colour=lines_color, lw=lw*1.5, alpha=lines_alpha)
ax.plot( (50/2, 50/2), (4.3, 4), colour=lines_color,
lw=lw, alpha=lines_alpha)
# Half Court docket Line
ax.axhline(94/2, colour=lines_color, lw=lw, alpha=lines_alpha)
# Plot Restrict
ax.set_xlim(0, 50)
ax.set_ylim(0, 94/2 + 2)
ax.set_facecolor(court_color)
ax.set_xticks([])
ax.set_yticks([])
ax.set_xlabel(”)
return ax
You may be aware that the code has a number of formatting choices and you may even change between a males’s and ladies’s courts. CBBD doesn’t at the moment provide NCAA ladies’s information, however that’s nonetheless a really good characteristic to have.
Go forward and run the perform with none choices specified.
create_ncaa_half_court()

Fairly fundamental and it simply works! We are able to add some formatting choices.
create_ncaa_half_court(three_line=”mens”, court_color=”black”, lines_color=”white”, paint_alpha=0, inner_arc=True)

Be happy to fiddle extra with completely different courtroom and magnificence combos.
We are going to seize shot location information from the CollegeBasketballData.com (CBBD) API. Particularly, we’ll be working with the cbbd Python package deal (imported above). First, configure your API key, changing your personal API key with the placeholder beneath. In case you want an API key, you may register for a free key through the CBBD foremost web site.
configuration = cbbd.Configuration(
access_token = ‘your_api_key_here’
)
Shot location information is included in play by play information. We are able to use the CBBD Performs API to seize all capturing performs for a selected group or participant. On this instance, we’ll seize team-level information. We are going to specify season and group parameters. We may even move in a shooting_plays_only flag to solely return capturing performs (i.e. filtering out issues like timeouts, rebounds, fouls, and many others). The code block beneath will seize capturing performs related to Dayton within the 2025 season. Be happy to modify up the group or season.
with cbbd.ApiClient(configuration) as api_client:
plays_api = cbbd.PlaysApi(api_client)
performs = plays_api.get_plays_by_team(season=2025, group=’Dayton’, shooting_plays_only=True)
performs[0]
Instance output of a capturing play:
PlayInfo(id=118229, source_id=’401715398101806301′, game_id=426, game_source_id=’401715398′, game_start_date=datetime.datetime(2024, 11, 9, 19, 30, tzinfo=datetime.timezone.utc), season=2025, season_type=, game_type=”STD”, play_type=”LayUpShot”, is_home_team=False, team_id=212, group=’Northwestern’, convention=”Huge Ten”, opponent_id=64, opponent=”Dayton”, opponent_conference=”A-10″, interval=1, clock=’19:36′, seconds_remaining=1176, home_score=0, away_score=0, home_win_probability=0.635, scoring_play=False, shooting_play=True, score_value=2, wallclock=None, play_text=”Ty Berry missed Layup.”, individuals=[PlayInfoParticipantsInner(name=”Ty Berry”, id=5452)], shot_info=ShotInfo(shooter=ShotInfoShooter(identify=”Ty Berry”, id=5452), made=False, vary=”rim”, assisted=False, assisted_by=ShotInfoShooter(identify=None, id=None), location=ShotInfoLocation(y=270, x=864.8)))
We are able to simply load this up right into a pandas DataFrame. The present scale for the x and y coordinates is 10 pts for each 1 foot. Dividing by 10, we will convert that into toes as we import right into a DataFrame, which is able to make it simpler to work with the half courtroom plot we ran by means of above. We may even filter out any capturing performs which may be lacking location information for no matter cause.
df = pd.DataFrame.from_records([
dict(
x=p.shot_info.location.x / 10,
y=p.shot_info.location.y / 10,
)
for p in plays
if p.shot_info is not None
and p.shot_info.location is not None
and p.shot_info.location.x is not None
and p.shot_info.location.y is not None
])
df.head()
x
y
0
76.14
29.5
1
22.56
41.0
2
26.32
8.5
3
81.78
31.5
4
69.56
9.5
We’ve one final step to take to get our information right into a usable state. We’re at the moment working with half courtroom plots, however these shot areas correspond to a full courtroom. We are going to convert the shot areas to half courtroom coordinates by translating areas from the lacking half over to the seen half of the courtroom.
df[‘x_half’] = df[‘x’]
df.loc[df[‘x’] > 47, ‘x_half’] = (94 – df[‘x’].loc[df[‘x’] > 47])
df[‘y_half’] = df[‘y’]
df.loc[df[‘x’] > 47, ‘y_half’] = (50 – df[‘y’].loc[df[‘x’] > 47])
# solid these to drift to keep away from typing points later
df[‘x_half’] = df[‘x_half’].astype(float)
df[‘y_half’] = df[‘y_half’].astype(float)
We are able to simply plot this information utilizing matplotlib. For instance, we will put it right into a scatter plot.
plt.scatter(df[‘y_half’], df[‘x_half’])

Not very fairly, however you may clearly see a basketball courtroom, together with the final define of the 3-point line.
We are able to enhance upon these by making a hexbin chart, which is able to bucket pictures into hexagonal areas of the courtroom to create a type of heatmap. The beneath code will create a hexbin plot utilizing the inferno colour map.
plt.hexbin(df[‘y_half’], df[‘x_half’], gridsize=20, cmap=’inferno’)
You may view extra colormaps right here and mess around with completely different colour schemes. Simply change inferno within the above snippet with the colormap of our alternative. You may as well kind in plt.cm. and use autocomplete to conveniently see what is offered.


I am a fan of gist_heat_r, so let’s test that one out. We’ll simply rerun the code from above, changing the colormap with that one.
plt.hexbin(df[‘y_half’], df[‘x_half’], gridsize=20, cmap=plt.cm.gist_heat_r)

You may as well fiddle with the gridsize parameter for decrease or greater decision. Right here I’ll improve the worth from 20 to 40.
plt.hexbin(df[‘y_half’], df[‘x_half’], gridsize=40, cmap=plt.cm.gist_heat_r)

We have plotted an empty half courtroom. We have plot precise shot location information factors. It is time to convey that every one collectively. Run the beneath snippet after which we’ll break it down line by line.
fig, ax = plt.subplots(figsize=(13.8, 14))
ax.hexbin(x=’y_half’, y=’x_half’, cmap=plt.cm.gist_heat_r, gridsize=40, information=df)
create_ncaa_half_court(ax, court_color=”white”,
lines_color=”black”, paint_alpha=0,
inner_arc=True)
plt.present()

Fairly good, huh? Let’s stroll by means of it.
On line 1, we’re setting the dimensions of the plot and returning the plot fig and ax objects.On line 2, we’re utilizing the ax object to create a hexbin plot, nearly similar to above.On line 3, we’re calling the create_ncaa_half_court perform with our desired styling choices. The colormap used right here works finest with a white background.Lastly, we present the courtroom with the plotted hex bins.
Let’s make this even cooler. We will use a library known as seaborn, which is constructed upon matplotlib. It accommodates most of the base plots discovered inside matplotlib, however with its personal tweaks and enhancements. It additionally gives a number of extra, extra superior varieties of plots. You may view the gallery right here. We’re going to be working with a jointplot, which is able to mix the hexbin chart we created with points of a bar chart.
It is fairly merely. Simply run the snippet beneath to see what it seems like.
sns.jointplot(information=df, x=’y_half’, y=’x_half’,
variety=’hex’, area=0, colour=plt.cm.gist_heat_r(.2), cmap=plt.cm.gist_heat_r)

Now put all of it collectively and let’s plot the jointplot on prime of our half courtroom plot.
cmap = plt.cm.gist_heat_r
joint_shot_chart = sns.jointplot(information=df, x=’y_half’, y=’x_half’,
variety=’hex’, area=0, colour=cmap(.2), cmap=cmap)
joint_shot_chart.determine.set_size_inches(12,11)
# A joint plot has 3 Axes, the primary one known as ax_joint
# is the one we need to draw our courtroom onto
ax = joint_shot_chart.ax_joint
create_ncaa_half_court(ax=ax,
three_line=”mens”,
court_color=”white”,
lines_color=”black”,
paint_alpha=0,
inner_arc=True)

One final thing, let’s take away the entry labels and add a title.
cmap = plt.cm.gist_heat_r
joint_shot_chart = sns.jointplot(information=df, x=’y_half’, y=’x_half’,
variety=’hex’, area=0, colour=cmap(.2), cmap=cmap)
joint_shot_chart.determine.set_size_inches(12,11)
# A joint plot has 3 Axes, the primary one known as ax_joint
# is the one we need to draw our courtroom onto
ax = joint_shot_chart.ax_joint
create_ncaa_half_court(ax=ax,
three_line=”mens”,
court_color=”white”,
lines_color=”black”,
paint_alpha=0,
inner_arc=True)
# Do away with axis labels and tick marks
ax.set_xlabel(”)
ax.set_ylabel(”)
ax.tick_params(labelbottom=’off’, labelleft=”off”)
ax.set_title(f”Dayton Shot Attemptsn(2024-2025)”, y=1.22, fontsize=18)

There are different kinds of joint plots you can also make by altering the sort parameter on line 3 above. For instance, altering the sort from hex to scatter outcomes on this.

Here’s what occurs after we change it to kde.

It does not look so nice, does it? We are able to fiddle a bit with the styling to make that look a bit higher. I will change the colormap to inferno, add fill and thresh parameters, and alter the half courtroom styling a bit bit.
cmap = plt.cm.inferno
joint_shot_chart = sns.jointplot(information=df, x=’y_half’, y=’x_half’,
variety=’kde’, area=0, fill=True, thresh=0, colour=cmap(.2), cmap=cmap)
joint_shot_chart.determine.set_size_inches(12,11)
# A joint plot has 3 Axes, the primary one known as ax_joint
# is the one we need to draw our courtroom onto
ax = joint_shot_chart.ax_joint
create_ncaa_half_court(ax=ax,
three_line=”mens”,
court_color=”black”,
lines_color=”white”,
paint_alpha=0,
inner_arc=True)
# Do away with axis labels and tick marks
ax.set_xlabel(”)
ax.set_ylabel(”)
ax.tick_params(labelbottom=’off’, labelleft=”off”)
ax.set_title(f”Dayton Shot Attemptsn(2024-2025)”, y=1.22, fontsize=18)

That is higher. See the jointplot docs for extra kinds, examples, and customizations.
It’s best to now be capable of create shot location charts towards an precise courtroom utilizing matplotlib and seaborn with the CBBD Python library. There are lots of methods to take this additional:
Plot a number of groups utilizing subplotsPlot made pictures and missed pictures side-by-side for a similar group utilizing subplotsApply the identical code to plotting shot charts for particular playersFind new styling and customizations
Lastly, I already cited Rob Mulla and his wonderful Kaggle article and helper features for plotting NCAA basketball courts. I would be remiss if I additionally did not shout Savvas Tjortjoglou as a drew a variety of inspiration from his article on plotting NBA shot charts.
As all the time, let me know what you assume and pleased coding!