Analyze NBA Shooting with Python

Fetch real NBA shot data and build a court-overlay shot chart.

Introduction

30 Second Summary

Every basketball fan has watched a player heat up from three-point range and wondered exactly where on the court they're most dangerous. The data behind those moments is publicly available, and Python can turn it into a visual story.

In this project, you will fetch real NBA shot data, clean it with pandas, and produce two professional visualizations: a court-overlay shot chart and a zone efficiency bar chart.

What You'll Build

A Python script that pulls real shooting data from NBA.com's official stats API and produces publication-quality shot chart visualizations for any player.

By the end of this project, you'll have:

  • A working Python script that pulls real NBA shot data from NBA.com's official stats API.
  • Two publication-quality visualizations: a scatter-plot shot chart overlaid on an accurately drawn NBA half-court, and a horizontal bar chart showing field goal percentage by court zone.
  • A repeatable analysis workflow (fetch, clean, analyze, visualize) you can walk through in a sports analytics interview.
  • Secret Mission: Compare two players' shooting patterns side-by-side in a single figure.

Are there any prerequisites?

You should be comfortable with basic Python (variables, functions, loops). Familiarity with pandas is helpful but not required.

Set Up Your Environment

Before you can analyze any NBA data, you need a workspace where your Python script can live and the right packages installed to talk to NBA.com's stats API.

In this step, you will create a project folder, install five Python packages, and confirm everything connects properly.

In this step, get ready to:
  • Create a project folder and open it in VS Code.
  • Install the required Python packages.
  • Verify the installation with a test import.
Create your project folder and open it in VS Code
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).

What is the integrated terminal?

The integrated terminal is a command line built into VS Code. It lets you run terminal commands without leaving your editor.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir nba-shot-chart
cd nba-shot-chart
  • Confirm the folder exists by running:
ls

You should see an empty output since the folder is brand new.

  • Open this folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Select the nba-shot-chart folder on your Desktop, then click Open.

VS Code reloads with your project folder as the workspace. If a trust dialog appears, click Yes, I trust the authors.

  • Reopen the integrated terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Confirm you are in the right folder by running:
pwd

You should see a path ending in nba-shot-chart.

Don't see nba-shot-chart in the path?

Make sure you selected the correct folder when you clicked Open Folder. The terminal should automatically point to the folder you opened.

Help me fix my terminal path

Install the required Python packages

Your shot chart analyzer depends on five packages. The nba_api package fetches real data from NBA.com. pandas handles data manipulation. matplotlib and seaborn produce visualizations. NumPy provides numerical operations.

  • Install all five packages by running this command:
pip install nba_api==1.11.4 pandas matplotlib seaborn numpy

What does this command do?

pip is Python's package manager. It downloads and installs libraries from the Python Package Index (PyPI).

The ==1.11.4 part pins nba_api to a specific version so your code works exactly as expected. The other packages install their latest compatible versions.

The install may take a minute. You should see progress bars as each package downloads.

When it finishes, you should see a line that includes Successfully installed followed by all the package names.

Seeing a permission error or command not found?

If pip is not recognized, try pip3 instead. Some systems use pip3 for Python 3 packages.

If you see a permissions error, add --user to the end of the command to install packages in your user directory.

Help me troubleshoot my pip install

Verify the installation

A quick import test confirms all packages are installed correctly and the player database is accessible.

  • Run this command to test your setup:
python3 -c "from nba_api.stats.static import players; import pandas, matplotlib, seaborn, numpy; print('All packages imported successfully!'); print(f'{len(players.get_players())} NBA players in database')"

What does this command do?

python3 -c runs a snippet of Python directly from the terminal without creating a file.

This snippet imports all five packages and queries the built-in NBA player database. If anything is missing or broken, Python raises an error immediately.

You should see output showing all packages imported successfully, along with a count of NBA players in the database.

Seeing an ImportError?

If you see ModuleNotFoundError, the package did not install correctly. Rerun the pip install command from the previous substep.

On Windows, try python instead of python3.

Help me fix my import error

Your environment is locked and loaded. Next up, you will use these packages to fetch real shot chart data from NBA.com for Stephen Curry.

Fetch NBA Shot Data

In the previous step, you set up your environment with all the packages you need. Now it's time to actually pull real NBA shooting data.

NBA.com's stats API provides detailed shot-by-shot records for every player. Each record includes the exact court coordinates, whether the shot went in, and which zone it came from. You'll write helper functions that wrap this API, making it easy to fetch data for any player and season.

In this step, get ready to:
  • Create the script file and write helper functions that fetch shot data from NBA.com.
  • Run the script to pull Stephen Curry's 2024-25 Regular Season shot data.
  • Examine the raw DataFrame to understand its structure before cleaning.
Create the script and write helper functions

The nba_api package provides a Python wrapper around NBA.com's stats endpoints. You'll write two functions: one to look up a player's unique ID by name, and one to fetch their full shot chart data for a given season.

  • In the VS Code Explorer sidebar, right-click inside your project folder and select New File.
  • Name the file nba_shot_chart.py and press Enter.
  • Paste the following code into the new file:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
import seaborn as sns
from nba_api.stats.endpoints import shotchartdetail
from nba_api.stats.static import players


# --- Data Fetching ---

def get_player_id(player_name):
    """Look up a player's ID by full name."""
    player_list = players.find_players_by_full_name(player_name)
    if not player_list:
        raise ValueError(f"Player '{player_name}' not found.")
    return player_list[0]['id']


def fetch_shot_data(player_name, season='2024-25'):
    """Fetch shot chart data for a player and season."""
    player_id = get_player_id(player_name)

    shot_chart = shotchartdetail.ShotChartDetail(
        team_id=0,
        player_id=player_id,
        context_measure_simple='FGA',
        season_nullable=season,
        season_type_all_star='Regular Season'
    )

    shot_df = shot_chart.get_data_frames()[0]
    league_avg = shot_chart.get_data_frames()[1]

    return shot_df, league_avg

What does this code do?

The imports bring in everything you'll need across the project: pandas for DataFrames, matplotlib for drawing, and nba_api for fetching data from NBA.com.

The get_player_id() function translates a player's full name (like 'Stephen Curry') into the numeric ID that NBA.com's API requires. The fetch_shot_data() function uses that ID to call the ShotChartDetail endpoint, returning two DataFrames: one with individual shot records and one with league-average shooting by zone.

  • Save nba_shot_chart.py with Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm nba_shot_chart.py appears in the Explorer sidebar on the left.

Don't see the file in your Explorer?

Make sure you created the file inside your project folder, not in a parent directory. The file should be at the top level of the folder you opened in VS Code.

If the Explorer sidebar is hidden, press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle it.

Still stuck? Help me find my file in VS Code

Fetch Stephen Curry's shot data

Your helper functions are ready. Now you need a main execution block that actually calls them for a specific player and season.

  • In nba_shot_chart.py, add the following code at the bottom of the file, below the fetch_shot_data() function:

# --- Main Execution ---

if __name__ == '__main__':
    PLAYER_NAME = 'Stephen Curry'
    SEASON = '2024-25'

    print(f"Fetching shot data for {PLAYER_NAME} ({SEASON})...")
    shot_df, league_avg = fetch_shot_data(PLAYER_NAME, SEASON)

    print(f"Retrieved {len(shot_df)} shot attempts.")
    print(shot_df.head())
    print(shot_df.info())
    print(shot_df.describe())

What does this code do?

  • The if __name__ == '__main__': guard ensures this code only runs when you execute the script directly, not when you import it later.
  • It fetches Stephen Curry's 2024-25 Regular Season shot data and stores it in shot_df.
  • The three print calls (.head(), .info(), .describe()) give you a quick look at the data's structure, types, and statistics.
  • Save nba_shot_chart.py.
  • Run the script in your integrated terminal by running this command:
python nba_shot_chart.py

You should see output starting with "Fetching shot data for Stephen Curry (2024-25)..." followed by the number of shot attempts retrieved, a table of the first 5 rows, column info, and descriptive statistics.

Seeing a connection error or timeout?

NBA.com's API can occasionally be slow or throttle requests. Wait 30 seconds and try running the script again.

If you see a ValueError about the player not being found, double check that the player name is spelled exactly as it appears on NBA.com (full first and last name).

Still getting errors? Help me debug my NBA API connection

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full nba_shot_chart.py at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
import seaborn as sns
from nba_api.stats.endpoints import shotchartdetail
from nba_api.stats.static import players


# --- Data Fetching ---

def get_player_id(player_name):
    """Look up a player's ID by full name."""
    player_list = players.find_players_by_full_name(player_name)
    if not player_list:
        raise ValueError(f"Player '{player_name}' not found.")
    return player_list[0]['id']


def fetch_shot_data(player_name, season='2024-25'):
    """Fetch shot chart data for a player and season."""
    player_id = get_player_id(player_name)

    shot_chart = shotchartdetail.ShotChartDetail(
        team_id=0,
        player_id=player_id,
        context_measure_simple='FGA',
        season_nullable=season,
        season_type_all_star='Regular Season'
    )

    shot_df = shot_chart.get_data_frames()[0]
    league_avg = shot_chart.get_data_frames()[1]

    return shot_df, league_avg


# --- Main Execution ---

if __name__ == '__main__':
    PLAYER_NAME = 'Stephen Curry'
    SEASON = '2024-25'

    print(f"Fetching shot data for {PLAYER_NAME} ({SEASON})...")
    shot_df, league_avg = fetch_shot_data(PLAYER_NAME, SEASON)

    print(f"Retrieved {len(shot_df)} shot attempts.")
    print(shot_df.head())
    print(shot_df.info())
    print(shot_df.describe())
Examine the raw data output

Your terminal now shows three different views of the shot data. Each reveals something important about the structure you'll work with in the next step.

What do these outputs tell you?

  • .head() shows the first 5 rows. Look for columns like LOC_X and LOC_Y (court coordinates), SHOT_MADE_FLAG (1 for made, 0 for missed), and SHOT_ZONE_BASIC (which area of the court).
  • .info() lists every column with its data type and non-null count. This helps you spot columns that need type conversion or have missing values.
  • .describe() shows summary statistics (mean, min, max) for numeric columns. Check SHOT_DISTANCE to see the range of distances Curry shoots from.

What is SHOT_ZONE_BASIC?

NBA.com divides the court into zones like "Restricted Area", "In The Paint (Non-RA)", "Mid-Range", "Above the Break 3", "Left Corner 3", "Right Corner 3", and "Backcourt". The SHOT_ZONE_BASIC column labels each shot with one of these zones. You'll use this column later to calculate shooting accuracy by area.

  • Scroll through your terminal output and confirm you can see the LOC_X, LOC_Y, SHOT_MADE_FLAG, SHOT_TYPE, and SHOT_ZONE_BASIC columns in the output.

You now have real NBA shot data sitting in a DataFrame. Next up, you'll clean this data and compute summary statistics to understand Curry's shooting profile before building the visualizations.

Clean and Explore the Data

In the previous step, you pulled real shot data from NBA.com and explored its raw structure.

Raw API data is rarely ready for analysis straight away. Columns might have the wrong type, or the values might be numeric codes instead of human-readable labels. In this step, you will transform the raw DataFrame into something you can actually analyze and visualize.

In this step, get ready to:
  • Add derived columns that make the data easier to work with.
  • Write a summary function that calculates key shooting statistics.
  • Run the summary to understand the player's shooting profile.
Add derived columns to the shot data

The raw DataFrame has numeric codes for shot outcomes (1 for made, 0 for missed) and text strings for shot type ('2PT Field Goal', '3PT Field Goal'). These are fine for a computer but awkward to read or filter quickly.

A cleaning function will add human-readable labels and numeric conversions that make downstream analysis much simpler.

  • In nba_shot_chart.py, find the comment that says # --- Main Execution ---.
  • Add the following # --- Data Cleaning --- section directly above that comment:
# --- Data Cleaning ---

def clean_shot_data(df):
    """Clean and add useful columns to the shot data."""
    df = df.copy()
    df['SHOT_RESULT'] = df['SHOT_MADE_FLAG'].map({1: 'Made', 0: 'Missed'})
    df['SHOT_DISTANCE'] = pd.to_numeric(df['SHOT_DISTANCE'])
    df['SHOT_VALUE'] = df['SHOT_TYPE'].apply(
        lambda x: 3 if '3PT' in x else 2
    )
    return df

What does this code do?

  • df.copy() creates a fresh copy so the original DataFrame stays unchanged.
  • SHOT_RESULT maps the numeric 1/0 flag to readable 'Made' or 'Missed' strings.
  • pd.to_numeric(df['SHOT_DISTANCE']) ensures the distance column is a proper number type for math operations later.
  • SHOT_VALUE checks if '3PT' appears in the shot type string. If so it assigns 3, otherwise 2. This lets you filter three-pointers and two-pointers easily.
  • Save nba_shot_chart.py.

Seeing a NameError or IndentationError?

Make sure the entire function is indented consistently (4 spaces per level). The def clean_shot_data(df): line should be at the same indentation level as def fetch_shot_data() above it.

Also confirm you placed the new section above # --- Main Execution ---, not inside another function.

Help me fix my clean_shot_data function

Write a summary statistics function

Before jumping into visualizations, you want a quick numeric overview of the player's shooting profile. A summary function gives you context for interpreting the charts later.

  • In nba_shot_chart.py, add the following function directly below clean_shot_data() (still above # --- Main Execution ---):
def print_summary(df, player_name):
    """Print a summary of the player's shooting stats."""
    total_shots = len(df)
    made_shots = df['SHOT_MADE_FLAG'].sum()
    fg_pct = made_shots / total_shots * 100

    three_pt = df[df['SHOT_VALUE'] == 3]
    three_made = three_pt['SHOT_MADE_FLAG'].sum()
    three_pct = three_made / len(three_pt) * 100 if len(three_pt) > 0 else 0

    two_pt = df[df['SHOT_VALUE'] == 2]
    two_made = two_pt['SHOT_MADE_FLAG'].sum()
    two_pct = two_made / len(two_pt) * 100 if len(two_pt) > 0 else 0

    print(f"\n{'='*50}")
    print(f"  Shot Chart Summary: {player_name}")
    print(f"{'='*50}")
    print(f"  Total Shots: {total_shots}")
    print(f"  Field Goals Made: {made_shots}")
    print(f"  FG%: {fg_pct:.1f}%")
    print(f"  3PT: {three_made}/{len(three_pt)} ({three_pct:.1f}%)")
    print(f"  2PT: {two_made}/{len(two_pt)} ({two_pct:.1f}%)")
    print(f"  Avg Shot Distance: {df['SHOT_DISTANCE'].mean():.1f} ft")
    print(f"{'='*50}\n")

What does this code do?

  • The function calculates overall FG% by dividing made shots by total attempts.
  • It filters the DataFrame by SHOT_VALUE to split three-pointers from two-pointers and compute separate percentages.
  • The if len(three_pt) > 0 else 0 guard prevents a division-by-zero error if the player has no three-point attempts.
  • The formatted output uses f-strings with :.1f to round percentages to one decimal place.
  • Save nba_shot_chart.py.

Seeing a KeyError on SHOT_VALUE?

The print_summary() function relies on the SHOT_VALUE column that clean_shot_data() creates. You must call clean_shot_data() on your DataFrame before passing it to print_summary(). You will wire this up in the next substep.

Help me fix a KeyError in print_summary

Run the summary and review the output

Now you need to update the main execution block to use your new cleaning and summary functions instead of the raw exploration prints from the previous step.

  • In nba_shot_chart.py, scroll down to the # --- Main Execution --- section.
  • Find these three lines at the bottom:
    print(shot_df.head())
    print(shot_df.info())
    print(shot_df.describe())
  • Delete those three lines and replace them with the following:
    # Clean the data
    shot_df = clean_shot_data(shot_df)

    # Print summary statistics
    print_summary(shot_df, PLAYER_NAME)

What does this change do?

Instead of dumping raw rows and column info, the main block now cleans the DataFrame first and then prints a formatted shooting summary. This is the typical data pipeline flow: fetch, clean, then analyze.

  • Save nba_shot_chart.py.
  • Run the script in your terminal by running:
python nba_shot_chart.py

You should see a formatted summary table in your terminal showing Stephen Curry's total shots, FG%, 3PT%, 2PT%, and average shot distance.

Script hangs or prints nothing?

The NBA.com API call can take 5-10 seconds. If it hangs much longer, check your internet connection.

If you see a ConnectionError, NBA.com may be throttling requests. Wait 30 seconds and try again.

Help me debug my NBA shot chart script

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete nba_shot_chart.py at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
import seaborn as sns
from nba_api.stats.endpoints import shotchartdetail
from nba_api.stats.static import players


# --- Data Fetching ---

def get_player_id(player_name):
    """Look up a player's ID by full name."""
    player_list = players.find_players_by_full_name(player_name)
    if not player_list:
        raise ValueError(f"Player '{player_name}' not found.")
    return player_list[0]['id']


def fetch_shot_data(player_name, season='2024-25'):
    """Fetch shot chart data for a player and season."""
    player_id = get_player_id(player_name)

    shot_chart = shotchartdetail.ShotChartDetail(
        team_id=0,
        player_id=player_id,
        context_measure_simple='FGA',
        season_nullable=season,
        season_type_all_star='Regular Season'
    )

    shot_df = shot_chart.get_data_frames()[0]
    league_avg = shot_chart.get_data_frames()[1]

    return shot_df, league_avg


# --- Data Cleaning ---

def clean_shot_data(df):
    """Clean and add useful columns to the shot data."""
    df = df.copy()
    df['SHOT_RESULT'] = df['SHOT_MADE_FLAG'].map({1: 'Made', 0: 'Missed'})
    df['SHOT_DISTANCE'] = pd.to_numeric(df['SHOT_DISTANCE'])
    df['SHOT_VALUE'] = df['SHOT_TYPE'].apply(
        lambda x: 3 if '3PT' in x else 2
    )
    return df


def print_summary(df, player_name):
    """Print a summary of the player's shooting stats."""
    total_shots = len(df)
    made_shots = df['SHOT_MADE_FLAG'].sum()
    fg_pct = made_shots / total_shots * 100

    three_pt = df[df['SHOT_VALUE'] == 3]
    three_made = three_pt['SHOT_MADE_FLAG'].sum()
    three_pct = three_made / len(three_pt) * 100 if len(three_pt) > 0 else 0

    two_pt = df[df['SHOT_VALUE'] == 2]
    two_made = two_pt['SHOT_MADE_FLAG'].sum()
    two_pct = two_made / len(two_pt) * 100 if len(two_pt) > 0 else 0

    print(f"\n{'='*50}")
    print(f"  Shot Chart Summary: {player_name}")
    print(f"{'='*50}")
    print(f"  Total Shots: {total_shots}")
    print(f"  Field Goals Made: {made_shots}")
    print(f"  FG%: {fg_pct:.1f}%")
    print(f"  3PT: {three_made}/{len(three_pt)} ({three_pct:.1f}%)")
    print(f"  2PT: {two_made}/{len(two_pt)} ({two_pct:.1f}%)")
    print(f"  Avg Shot Distance: {df['SHOT_DISTANCE'].mean():.1f} ft")
    print(f"{'='*50}\n")


# --- Main Execution ---

if __name__ == '__main__':
    PLAYER_NAME = 'Stephen Curry'
    SEASON = '2024-25'

    print(f"Fetching shot data for {PLAYER_NAME} ({SEASON})...")
    shot_df, league_avg = fetch_shot_data(PLAYER_NAME, SEASON)

    print(f"Retrieved {len(shot_df)} shot attempts.")

    # Clean the data
    shot_df = clean_shot_data(shot_df)

    # Print summary statistics
    print_summary(shot_df, PLAYER_NAME)

Your data is now clean and summarized. Next up, you will draw an accurate NBA half-court using matplotlib patches so you can overlay these shots on a real court diagram.

Draw the NBA Court

Your shot data is cleaned, summarized, and ready for visualization. But before you can plot shots on a court, you need the court itself.

In this step, you'll build a reusable draw_court() function using matplotlib patches. Each court element (hoop, paint, three-point line) becomes a geometric shape that lines up perfectly with the NBA's coordinate system.

In this step, get ready to:
  • Write a court-drawing function using Circle, Rectangle, and Arc patches.
  • Test the function by rendering an empty court and verifying the geometry.
Write the draw_court() function

The NBA stats API returns shot coordinates where the hoop sits at (0, 0) and 1 foot equals 10 units. Your court-drawing function needs to use those same coordinates so shots land in the correct positions.

Why draw it yourself instead of using a library?

Libraries like mplbasketball or sportypy can draw courts automatically. But writing the function yourself teaches you how matplotlib patches work and how the NBA coordinate system maps to real court dimensions.

A custom court function is also a stronger portfolio artifact. It shows you understand the geometry, not just that you can call a library.

  • In nba_shot_chart.py, find the print_summary() function.
  • Add the following section comment and draw_court() function below it, above the # --- Main Execution --- comment:
# --- Court Drawing ---

def draw_court(ax=None, color='black', lw=2, outer_lines=False):
    """Draw an NBA basketball court on a matplotlib axes.

    Court coordinates match the NBA stats API:
    - Hoop is at (0, 0)
    - 1 foot = 10 units
    """
    if ax is None:
        ax = plt.gca()

    # Basketball hoop (diameter 18", radius 9" = 7.5 units)
    hoop = Circle((0, 0), radius=7.5, linewidth=lw, color=color, fill=False)

    # Backboard
    backboard = Rectangle((-30, -7.5), 60, -1, linewidth=lw, color=color)

    # The paint (outer box: 16ft wide, 19ft tall)
    outer_box = Rectangle((-80, -47.5), 160, 190, linewidth=lw,
                          color=color, fill=False)

    # Inner box (12ft wide, 19ft tall)
    inner_box = Rectangle((-60, -47.5), 120, 190, linewidth=lw,
                          color=color, fill=False)

    # Free throw top arc
    top_free_throw = Arc((0, 142.5), 120, 120, theta1=0, theta2=180,
                         linewidth=lw, color=color, fill=False)

    # Free throw bottom arc (dashed)
    bottom_free_throw = Arc((0, 142.5), 120, 120, theta1=180, theta2=0,
                            linewidth=lw, color=color, linestyle='dashed')

    # Restricted area (4ft radius from hoop)
    restricted = Arc((0, 0), 80, 80, theta1=0, theta2=180,
                     linewidth=lw, color=color)

    # Three point line - corner threes (14ft long)
    corner_three_a = Rectangle((-220, -47.5), 0, 140, linewidth=lw,
                               color=color)
    corner_three_b = Rectangle((220, -47.5), 0, 140, linewidth=lw,
                               color=color)

    # Three point arc (23'9" from hoop = 237.5 units radius)
    three_arc = Arc((0, 0), 475, 475, theta1=22, theta2=158,
                    linewidth=lw, color=color)

    # Center court arcs
    center_outer_arc = Arc((0, 422.5), 120, 120, theta1=180, theta2=0,
                           linewidth=lw, color=color)
    center_inner_arc = Arc((0, 422.5), 40, 40, theta1=180, theta2=0,
                           linewidth=lw, color=color)

    court_elements = [hoop, backboard, outer_box, inner_box,
                      top_free_throw, bottom_free_throw, restricted,
                      corner_three_a, corner_three_b, three_arc,
                      center_outer_arc, center_inner_arc]

    if outer_lines:
        outer_lines_rect = Rectangle((-250, -47.5), 500, 470,
                                     linewidth=lw, color=color, fill=False)
        court_elements.append(outer_lines_rect)

    for element in court_elements:
        ax.add_patch(element)

    return ax

What does this code do?

Each court element is a matplotlib patch object positioned using the NBA's coordinate system (hoop at origin, 1 foot = 10 units):

  • Circle draws the hoop (radius 7.5 units = 9 inches in real life).
  • Rectangle draws the backboard, paint boxes, and corner three-point lines.
  • Arc draws curved elements. The theta1 and theta2 parameters control where each arc starts and ends in degrees.
  • The outer_lines parameter optionally adds the full half-court boundary rectangle.

The function collects all patches into a list, loops through them with ax.add_patch(), and returns the axes so you can layer shot data on top.

  • Save nba_shot_chart.py.

You won't see a change yet. The next substep tests the function by rendering an empty court.

Seeing a syntax error?

  • Check that the function is indented consistently (4 spaces per level). Mixing tabs and spaces causes IndentationError.
  • Make sure the escaped quotes in comments (like 18") remain inside the comment, not inside a string.
  • Verify the function sits between print_summary() and # --- Main Execution --- with a blank line separating each section.

Help me fix a syntax error in my draw_court() function

Test the function by drawing an empty court

Before layering shot data on top, you want to confirm the court geometry renders correctly on its own.

  • In nba_shot_chart.py, find the last line of the if __name__ == '__main__': block (the print_summary(shot_df, PLAYER_NAME) call).
  • Add these temporary test lines below it to draw an empty court:
    # Test court drawing (temporary)
    fig, ax = plt.subplots(figsize=(12, 11))
    draw_court(ax, outer_lines=True)
    ax.set_xlim(-250, 250)
    ax.set_ylim(422.5, -47.5)
    ax.set_aspect('equal')
    plt.title('Court Drawing Test')
    plt.show()

What does this test code do?

  • Creates a figure and axes, then calls draw_court() with outer_lines=True to include the half-court boundary.
  • Sets the axis limits to match the NBA coordinate system (-250 to 250 horizontally, 422.5 to -47.5 vertically).
  • The y-axis is intentionally inverted (422.5 on top, -47.5 on bottom) so the baseline appears at the bottom like a real court.
  • Save nba_shot_chart.py and run the script by executing this command in your terminal:
python nba_shot_chart.py

A matplotlib window opens showing a clean NBA half-court diagram. You should see the hoop circle at the bottom center, the rectangular paint area, the curved three-point arc connecting to straight corner lines, and the half-court arcs at the top.

Don't see a court window?

  • If the script finishes without opening a window, your matplotlib backend may not support interactive display. Try adding plt.savefig('court_test.png') before plt.show() and open the saved PNG file.
  • If the court looks distorted or stretched, check that ax.set_aspect('equal') is present. Without it, matplotlib distorts the proportions.
  • If the court appears upside down, check that set_ylim(422.5, -47.5) has the larger number first. This inverts the y-axis so the baseline is at the bottom.

Help me debug my court drawing output

  • Once you've confirmed the court looks correct, remove the temporary test lines you just added (everything from # Test court drawing (temporary) through plt.show()).
  • Save nba_shot_chart.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file after removing the test lines.

ⓧ I'd like to double check the full code

Here is the complete nba_shot_chart.py at this point in the project:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
import seaborn as sns
from nba_api.stats.endpoints import shotchartdetail
from nba_api.stats.static import players


# --- Data Fetching ---

def get_player_id(player_name):
    """Look up a player's ID by full name."""
    player_list = players.find_players_by_full_name(player_name)
    if not player_list:
        raise ValueError(f"Player '{player_name}' not found.")
    return player_list[0]['id']


def fetch_shot_data(player_name, season='2024-25'):
    """Fetch shot chart data for a player and season."""
    player_id = get_player_id(player_name)

    shot_chart = shotchartdetail.ShotChartDetail(
        team_id=0,
        player_id=player_id,
        context_measure_simple='FGA',
        season_nullable=season,
        season_type_all_star='Regular Season'
    )

    shot_df = shot_chart.get_data_frames()[0]
    league_avg = shot_chart.get_data_frames()[1]

    return shot_df, league_avg


# --- Data Cleaning ---

def clean_shot_data(df):
    """Clean and add useful columns to the shot data."""
    df = df.copy()
    df['SHOT_RESULT'] = df['SHOT_MADE_FLAG'].map({1: 'Made', 0: 'Missed'})
    df['SHOT_DISTANCE'] = pd.to_numeric(df['SHOT_DISTANCE'])
    df['SHOT_VALUE'] = df['SHOT_TYPE'].apply(
        lambda x: 3 if '3PT' in x else 2
    )
    return df


def print_summary(df, player_name):
    """Print a summary of the player's shooting stats."""
    total_shots = len(df)
    made_shots = df['SHOT_MADE_FLAG'].sum()
    fg_pct = made_shots / total_shots * 100

    three_pt = df[df['SHOT_VALUE'] == 3]
    three_made = three_pt['SHOT_MADE_FLAG'].sum()
    three_pct = three_made / len(three_pt) * 100 if len(three_pt) > 0 else 0

    two_pt = df[df['SHOT_VALUE'] == 2]
    two_made = two_pt['SHOT_MADE_FLAG'].sum()
    two_pct = two_made / len(two_pt) * 100 if len(two_pt) > 0 else 0

    print(f"\n{'='*50}")
    print(f"  Shot Chart Summary: {player_name}")
    print(f"{'='*50}")
    print(f"  Total Shots: {total_shots}")
    print(f"  Field Goals Made: {made_shots}")
    print(f"  FG%: {fg_pct:.1f}%")
    print(f"  3PT: {three_made}/{len(three_pt)} ({three_pct:.1f}%)")
    print(f"  2PT: {two_made}/{len(two_pt)} ({two_pct:.1f}%)")
    print(f"  Avg Shot Distance: {df['SHOT_DISTANCE'].mean():.1f} ft")
    print(f"{'='*50}\n")


# --- Court Drawing ---

def draw_court(ax=None, color='black', lw=2, outer_lines=False):
    """Draw an NBA basketball court on a matplotlib axes.

    Court coordinates match the NBA stats API:
    - Hoop is at (0, 0)
    - 1 foot = 10 units
    """
    if ax is None:
        ax = plt.gca()

    # Basketball hoop (diameter 18", radius 9" = 7.5 units)
    hoop = Circle((0, 0), radius=7.5, linewidth=lw, color=color, fill=False)

    # Backboard
    backboard = Rectangle((-30, -7.5), 60, -1, linewidth=lw, color=color)

    # The paint (outer box: 16ft wide, 19ft tall)
    outer_box = Rectangle((-80, -47.5), 160, 190, linewidth=lw,
                          color=color, fill=False)

    # Inner box (12ft wide, 19ft tall)
    inner_box = Rectangle((-60, -47.5), 120, 190, linewidth=lw,
                          color=color, fill=False)

    # Free throw top arc
    top_free_throw = Arc((0, 142.5), 120, 120, theta1=0, theta2=180,
                         linewidth=lw, color=color, fill=False)

    # Free throw bottom arc (dashed)
    bottom_free_throw = Arc((0, 142.5), 120, 120, theta1=180, theta2=0,
                            linewidth=lw, color=color, linestyle='dashed')

    # Restricted area (4ft radius from hoop)
    restricted = Arc((0, 0), 80, 80, theta1=0, theta2=180,
                     linewidth=lw, color=color)

    # Three point line - corner threes (14ft long)
    corner_three_a = Rectangle((-220, -47.5), 0, 140, linewidth=lw,
                               color=color)
    corner_three_b = Rectangle((220, -47.5), 0, 140, linewidth=lw,
                               color=color)

    # Three point arc (23'9" from hoop = 237.5 units radius)
    three_arc = Arc((0, 0), 475, 475, theta1=22, theta2=158,
                    linewidth=lw, color=color)

    # Center court arcs
    center_outer_arc = Arc((0, 422.5), 120, 120, theta1=180, theta2=0,
                           linewidth=lw, color=color)
    center_inner_arc = Arc((0, 422.5), 40, 40, theta1=180, theta2=0,
                           linewidth=lw, color=color)

    court_elements = [hoop, backboard, outer_box, inner_box,
                      top_free_throw, bottom_free_throw, restricted,
                      corner_three_a, corner_three_b, three_arc,
                      center_outer_arc, center_inner_arc]

    if outer_lines:
        outer_lines_rect = Rectangle((-250, -47.5), 500, 470,
                                     linewidth=lw, color=color, fill=False)
        court_elements.append(outer_lines_rect)

    for element in court_elements:
        ax.add_patch(element)

    return ax


# --- Main Execution ---

if __name__ == '__main__':
    PLAYER_NAME = 'Stephen Curry'
    SEASON = '2024-25'

    print(f"Fetching shot data for {PLAYER_NAME} ({SEASON})...")
    shot_df, league_avg = fetch_shot_data(PLAYER_NAME, SEASON)

    print(f"Retrieved {len(shot_df)} shot attempts.")

    # Clean the data
    shot_df = clean_shot_data(shot_df)

    # Print summary statistics
    print_summary(shot_df, PLAYER_NAME)

Your court canvas is ready. Next up, you'll layer actual shot data on top of this court to create the final visualizations.

Create the Shot Chart Visualizations

Your court is drawn and verified. Now it's time to layer the actual shot data on top of it.

In this step, you'll write two visualization functions that turn raw coordinate data into portfolio-quality charts. One plots every shot attempt directly on the court. The other breaks down shooting accuracy by zone.

In this step, get ready to:
  • Write a shot chart function that overlays made and missed shots on the court.
  • Write a zone analysis function that shows FG% by court area.
  • Run both visualizations and save them as PNG files.
Write the shot chart function

The shot chart is the signature visualization in basketball analytics. It plots every field goal attempt as a colored marker on the court, so you can instantly see where a player shoots from and how often they convert.

  • In nba_shot_chart.py, find the # --- Main Execution --- comment near the bottom of your file.
  • Add the following code above that comment to create a new Visualizations section:
# --- Visualizations ---

def plot_shot_chart(df, player_name, season='2024-25'):
    """Create a shot chart visualization for a player."""
    fig, ax = plt.subplots(figsize=(12, 11))

    # Separate made and missed shots
    made = df[df['SHOT_MADE_FLAG'] == 1]
    missed = df[df['SHOT_MADE_FLAG'] == 0]

    # Plot missed shots (red x markers)
    ax.scatter(missed['LOC_X'], missed['LOC_Y'],
               c='#e74c3c', marker='x', s=20, alpha=0.4,
               label=f'Missed ({len(missed)})')

    # Plot made shots (green circle markers)
    ax.scatter(made['LOC_X'], made['LOC_Y'],
               c='#2ecc71', marker='o', s=30, alpha=0.6,
               label=f'Made ({len(made)})')

    # Draw the court
    draw_court(ax, outer_lines=True)

    # Set axis limits and appearance
    ax.set_xlim(-250, 250)
    ax.set_ylim(422.5, -47.5)
    ax.set_aspect('equal')
    ax.set_title(f'{player_name} Shot Chart - {season} Regular Season',
                 fontsize=16, fontweight='bold', pad=15)
    ax.legend(loc='upper right', fontsize=12)
    ax.axis('off')

    plt.tight_layout()
    plt.savefig('shot_chart.png', dpi=150, bbox_inches='tight',
                facecolor='white')
    plt.show()
    print("Shot chart saved as 'shot_chart.png'")

What does this code do?

  • The function splits the DataFrame into made and missed shots using SHOT_MADE_FLAG.
  • Missed shots appear as small red x markers. Made shots appear as slightly larger green circles. The alpha values make overlapping shots visible.
  • It calls your existing draw_court() function to render the half-court underneath the scatter points.
  • The y-axis is inverted (422.5 to -47.5) so the basket sits at the bottom, matching how courts are shown on TV broadcasts.
  • Finally, it saves the figure as a 150 DPI PNG file.
  • Save nba_shot_chart.py.

You won't see the chart yet. The next substep adds the zone analysis, and then you'll update the main block to run both.

Write the zone analysis function

A shot chart shows where shots land, but it's hard to read percentages from scattered dots. The zone analysis groups shots by court area and shows a clear FG% bar for each zone.

  • In nba_shot_chart.py, add the following function directly below plot_shot_chart() (still above the # --- Main Execution --- comment):
def plot_zone_analysis(df, player_name):
    """Create a bar chart of shooting percentage by court zone."""
    zone_stats = df.groupby('SHOT_ZONE_BASIC').agg(
        FGA=('SHOT_ATTEMPTED_FLAG', 'sum'),
        FGM=('SHOT_MADE_FLAG', 'sum')
    ).reset_index()

    zone_stats['FG_PCT'] = (zone_stats['FGM'] / zone_stats['FGA'] * 100).round(1)
    zone_stats = zone_stats.sort_values('FG_PCT', ascending=True)

    fig, ax = plt.subplots(figsize=(10, 6))

    bars = ax.barh(zone_stats['SHOT_ZONE_BASIC'], zone_stats['FG_PCT'],
                   color=sns.color_palette('viridis', len(zone_stats)))

    for bar, fga in zip(bars, zone_stats['FGA']):
        width = bar.get_width()
        ax.text(width + 1, bar.get_y() + bar.get_height()/2,
                f'{width:.1f}% ({fga} attempts)',
                va='center', fontsize=10)

    ax.set_xlabel('Field Goal Percentage (%)', fontsize=12)
    ax.set_title(f'{player_name} - FG% by Court Zone', fontsize=14,
                 fontweight='bold')
    ax.set_xlim(0, max(zone_stats['FG_PCT']) + 15)
    sns.despine()

    plt.tight_layout()
    plt.savefig('zone_analysis.png', dpi=150, bbox_inches='tight',
                facecolor='white')
    plt.show()
    print("Zone analysis saved as 'zone_analysis.png'")

What does this code do?

  • It groups shots by the SHOT_ZONE_BASIC column (zones like 'Restricted Area', 'Mid-Range', 'Above the Break 3') and counts makes vs attempts per zone.
  • It calculates FG% for each zone, then sorts ascending so the most accurate zone appears at the top of the chart.
  • Each bar is annotated with the exact percentage and total attempt count, giving context for small-sample zones.
  • The sns.despine() call removes the top and right borders for a cleaner look.
  • Save nba_shot_chart.py.

You won't see output yet. The next substep wires both functions into your main block and runs everything.

Run both visualizations and save the PNGs

Both visualization functions are written. Now you need to call them from the main execution block so the script produces charts when you run it.

  • In nba_shot_chart.py, scroll down to the if __name__ == '__main__': block at the bottom of your file.
  • Find this line:
    # Print summary statistics
    print_summary(shot_df, PLAYER_NAME)
  • Add the following lines directly below it:
    # Create visualizations
    plot_shot_chart(shot_df, PLAYER_NAME, SEASON)
    plot_zone_analysis(shot_df, PLAYER_NAME)

What does this code do?

These two lines call your new functions with the cleaned DataFrame, the player name, and (for the shot chart) the season string. Each function creates its chart, displays it, and saves a PNG to your project folder.

  • Save nba_shot_chart.py.
  • Run the script by entering this command in your terminal:
python nba_shot_chart.py

Two chart windows will pop up one after the other. Close each one to continue the script. After both close, you should see the summary stats printed and two confirmation messages.

What should I see?

  • A shot chart showing hundreds of colored dots scattered across a half-court. Green circles cluster around the three-point line and near the rim. Red x markers fill in everywhere else.
  • A horizontal bar chart with zones like 'Restricted Area' and 'Above the Break 3' on the y-axis, and FG% on the x-axis. Each bar is annotated with the percentage and attempt count.
  • Two new files in your project folder: shot_chart.png and zone_analysis.png.

Charts not displaying or script errors?

  • If you see NameError: name 'plot_shot_chart' is not defined, make sure both functions are defined above the if __name__ == '__main__': block. Python reads top to bottom.
  • If the chart window freezes, wait a few seconds. Large scatter plots can take a moment to render. Close the window to advance to the next chart.
  • If you see an indentation error, make sure the two new lines inside if __name__ == '__main__': are indented with exactly 4 spaces to match the other lines in that block.

Still stuck? Help me debug why my shot chart script is failing.

  • Confirm both PNG files were saved by running:
ls *.png

You should see shot_chart.png and zone_analysis.png in the output.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and both PNGs are in your project folder.

ⓧ I'd like to double check the full code

Here is the complete nba_shot_chart.py at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle, Arc
import seaborn as sns
from nba_api.stats.endpoints import shotchartdetail
from nba_api.stats.static import players


# --- Data Fetching ---

def get_player_id(player_name):
    """Look up a player's ID by full name."""
    player_list = players.find_players_by_full_name(player_name)
    if not player_list:
        raise ValueError(f"Player '{player_name}' not found.")
    return player_list[0]['id']


def fetch_shot_data(player_name, season='2024-25'):
    """Fetch shot chart data for a player and season."""
    player_id = get_player_id(player_name)

    shot_chart = shotchartdetail.ShotChartDetail(
        team_id=0,
        player_id=player_id,
        context_measure_simple='FGA',
        season_nullable=season,
        season_type_all_star='Regular Season'
    )

    shot_df = shot_chart.get_data_frames()[0]
    league_avg = shot_chart.get_data_frames()[1]

    return shot_df, league_avg


# --- Data Cleaning ---

def clean_shot_data(df):
    """Clean and add useful columns to the shot data."""
    df = df.copy()
    df['SHOT_RESULT'] = df['SHOT_MADE_FLAG'].map({1: 'Made', 0: 'Missed'})
    df['SHOT_DISTANCE'] = pd.to_numeric(df['SHOT_DISTANCE'])
    df['SHOT_VALUE'] = df['SHOT_TYPE'].apply(
        lambda x: 3 if '3PT' in x else 2
    )
    return df


def print_summary(df, player_name):
    """Print a summary of the player's shooting stats."""
    total_shots = len(df)
    made_shots = df['SHOT_MADE_FLAG'].sum()
    fg_pct = made_shots / total_shots * 100

    three_pt = df[df['SHOT_VALUE'] == 3]
    three_made = three_pt['SHOT_MADE_FLAG'].sum()
    three_pct = three_made / len(three_pt) * 100 if len(three_pt) > 0 else 0

    two_pt = df[df['SHOT_VALUE'] == 2]
    two_made = two_pt['SHOT_MADE_FLAG'].sum()
    two_pct = two_made / len(two_pt) * 100 if len(two_pt) > 0 else 0

    print(f"\n{'='*50}")
    print(f"  Shot Chart Summary: {player_name}")
    print(f"{'='*50}")
    print(f"  Total Shots: {total_shots}")
    print(f"  Field Goals Made: {made_shots}")
    print(f"  FG%: {fg_pct:.1f}%")
    print(f"  3PT: {three_made}/{len(three_pt)} ({three_pct:.1f}%)")
    print(f"  2PT: {two_made}/{len(two_pt)} ({two_pct:.1f}%)")
    print(f"  Avg Shot Distance: {df['SHOT_DISTANCE'].mean():.1f} ft")
    print(f"{'='*50}\n")


# --- Court Drawing ---

def draw_court(ax=None, color='black', lw=2, outer_lines=False):
    """Draw an NBA basketball court on a matplotlib axes.

    Court coordinates match the NBA stats API:
    - Hoop is at (0, 0)
    - 1 foot = 10 units
    """
    if ax is None:
        ax = plt.gca()

    hoop = Circle((0, 0), radius=7.5, linewidth=lw, color=color, fill=False)
    backboard = Rectangle((-30, -7.5), 60, -1, linewidth=lw, color=color)
    outer_box = Rectangle((-80, -47.5), 160, 190, linewidth=lw,
                          color=color, fill=False)
    inner_box = Rectangle((-60, -47.5), 120, 190, linewidth=lw,
                          color=color, fill=False)
    top_free_throw = Arc((0, 142.5), 120, 120, theta1=0, theta2=180,
                         linewidth=lw, color=color, fill=False)
    bottom_free_throw = Arc((0, 142.5), 120, 120, theta1=180, theta2=0,
                            linewidth=lw, color=color, linestyle='dashed')
    restricted = Arc((0, 0), 80, 80, theta1=0, theta2=180,
                     linewidth=lw, color=color)
    corner_three_a = Rectangle((-220, -47.5), 0, 140, linewidth=lw,
                               color=color)
    corner_three_b = Rectangle((220, -47.5), 0, 140, linewidth=lw,
                               color=color)
    three_arc = Arc((0, 0), 475, 475, theta1=22, theta2=158,
                    linewidth=lw, color=color)
    center_outer_arc = Arc((0, 422.5), 120, 120, theta1=180, theta2=0,
                           linewidth=lw, color=color)
    center_inner_arc = Arc((0, 422.5), 40, 40, theta1=180, theta2=0,
                           linewidth=lw, color=color)

    court_elements = [hoop, backboard, outer_box, inner_box,
                      top_free_throw, bottom_free_throw, restricted,
                      corner_three_a, corner_three_b, three_arc,
                      center_outer_arc, center_inner_arc]

    if outer_lines:
        outer_lines_rect = Rectangle((-250, -47.5), 500, 470,
                                     linewidth=lw, color=color, fill=False)
        court_elements.append(outer_lines_rect)

    for element in court_elements:
        ax.add_patch(element)

    return ax


# --- Visualizations ---

def plot_shot_chart(df, player_name, season='2024-25'):
    """Create a shot chart visualization for a player."""
    fig, ax = plt.subplots(figsize=(12, 11))

    # Separate made and missed shots
    made = df[df['SHOT_MADE_FLAG'] == 1]
    missed = df[df['SHOT_MADE_FLAG'] == 0]

    # Plot missed shots (red x markers)
    ax.scatter(missed['LOC_X'], missed['LOC_Y'],
               c='#e74c3c', marker='x', s=20, alpha=0.4,
               label=f'Missed ({len(missed)})')

    # Plot made shots (green circle markers)
    ax.scatter(made['LOC_X'], made['LOC_Y'],
               c='#2ecc71', marker='o', s=30, alpha=0.6,
               label=f'Made ({len(made)})')

    # Draw the court
    draw_court(ax, outer_lines=True)

    # Set axis limits and appearance
    ax.set_xlim(-250, 250)
    ax.set_ylim(422.5, -47.5)
    ax.set_aspect('equal')
    ax.set_title(f'{player_name} Shot Chart - {season} Regular Season',
                 fontsize=16, fontweight='bold', pad=15)
    ax.legend(loc='upper right', fontsize=12)
    ax.axis('off')

    plt.tight_layout()
    plt.savefig('shot_chart.png', dpi=150, bbox_inches='tight',
                facecolor='white')
    plt.show()
    print("Shot chart saved as 'shot_chart.png'")


def plot_zone_analysis(df, player_name):
    """Create a bar chart of shooting percentage by court zone."""
    zone_stats = df.groupby('SHOT_ZONE_BASIC').agg(
        FGA=('SHOT_ATTEMPTED_FLAG', 'sum'),
        FGM=('SHOT_MADE_FLAG', 'sum')
    ).reset_index()

    zone_stats['FG_PCT'] = (zone_stats['FGM'] / zone_stats['FGA'] * 100).round(1)
    zone_stats = zone_stats.sort_values('FG_PCT', ascending=True)

    fig, ax = plt.subplots(figsize=(10, 6))

    bars = ax.barh(zone_stats['SHOT_ZONE_BASIC'], zone_stats['FG_PCT'],
                   color=sns.color_palette('viridis', len(zone_stats)))

    for bar, fga in zip(bars, zone_stats['FGA']):
        width = bar.get_width()
        ax.text(width + 1, bar.get_y() + bar.get_height()/2,
                f'{width:.1f}% ({fga} attempts)',
                va='center', fontsize=10)

    ax.set_xlabel('Field Goal Percentage (%)', fontsize=12)
    ax.set_title(f'{player_name} - FG% by Court Zone', fontsize=14,
                 fontweight='bold')
    ax.set_xlim(0, max(zone_stats['FG_PCT']) + 15)
    sns.despine()

    plt.tight_layout()
    plt.savefig('zone_analysis.png', dpi=150, bbox_inches='tight',
                facecolor='white')
    plt.show()
    print("Zone analysis saved as 'zone_analysis.png'")


# --- Main Execution ---

if __name__ == '__main__':
    PLAYER_NAME = 'Stephen Curry'
    SEASON = '2024-25'

    print(f"Fetching shot data for {PLAYER_NAME} ({SEASON})...")
    shot_df, league_avg = fetch_shot_data(PLAYER_NAME, SEASON)

    print(f"Retrieved {len(shot_df)} shot attempts.")

    # Clean the data
    shot_df = clean_shot_data(shot_df)

    # Print summary statistics
    print_summary(shot_df, PLAYER_NAME)

    # Create visualizations
    plot_shot_chart(shot_df, PLAYER_NAME, SEASON)
    plot_zone_analysis(shot_df, PLAYER_NAME)

Your analysis is complete. You now have two portfolio-quality visualizations that demonstrate the full data analytics workflow. Next up, you'll tackle the Secret Mission and compare two players side-by-side.

Secret mission

Compare Two Players Side-by-Side

Your shot chart shows one player's tendencies. But what if you could compare two players on the same figure? In this secret mission, you'll build a comparison function that puts any two NBA players' shot charts side-by-side, making it easy to spot differences in their shooting patterns at a glance.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and calls NBA.com's free public API with no account or key required. There are no ongoing costs.

Decide whether to keep your resources on your machine or delete them entirely.

Resources you used:

  • Project folder on your Desktop containing nba_shot_chart.py, shot_chart.png, zone_analysis.png, and shot_chart_comparison.png.
  • Python packages installed via pip: nba_api, pandas, matplotlib, seaborn, and numpy.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with different players or seasons.

  • Your project folder stays on your Desktop with all scripts and saved PNGs.
  • The installed Python packages remain available for other projects that use pandas or matplotlib.

Pause - I'll come back to this later

There are no running processes or services to pause. Your files sit on disk until you return.

  • Your script and PNG files will be right where you left them whenever you come back.
  • No memory or CPU is used while the script is not running.

Delete - I don't want to use this again

Remove all project files and uninstall the Python packages you added.

  • Delete your project folder by running this command (replace the folder name with whatever you named it):

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~\Desktop
Remove-Item -Recurse -Force [[PROJECT_FOLDER="your project folder name"]]
  • Uninstall the Python packages by running:
pip uninstall nba_api pandas matplotlib seaborn numpy -y

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete NBA shot chart analyzer that pulls real data from NBA.com, processes it with pandas, and produces portfolio-quality visualizations.

You've learned how to:

  • Fetched real NBA shot data from NBA.com's official stats API using nba_api and cleaned it into an analysis-ready pandas DataFrame.
  • Built a reusable court-drawing function using matplotlib patches (Circle, Rectangle, Arc) that accurately renders an NBA half-court aligned to the API's coordinate system.
  • Created publication-quality visualizations: a scatter-plot shot chart overlaid on the drawn court and a horizontal bar chart showing FG% by court zone.
  • Secret Mission: Generated a side-by-side player comparison chart to analyze shooting patterns across two players in a single figure.

Ready to quiz yourself?