Skip to main content

Overview

The team_shooting.py script collects team-level shooting statistics categorized by defender distance. This provides insight into team offensive shot quality, spacing effectiveness, and how defenses contest opposing teams’ shots.

Data Source

API: NBA.com Stats API
Endpoint: leaguedashteamptshot with CloseDefDistRange parameter

Functions

get_teamshots()

Retrieves team shooting statistics grouped by defender distance.
years
list
required
List of season years to collect (e.g., [2023, 2024])
ps
boolean
default:"False"
Set to True for playoff data, False for regular season
Defender Distance Categories:
  • Very Tight (0-2 feet) - Heavily contested shots
  • Tight (2-4 feet) - Contested shots
  • Open (4-6 feet) - Lightly contested shots
  • Wide Open (6+ feet) - Uncontested shots
Returns: DataFrame with team shooting stats for each coverage type
# From team_shooting.py:77-135
def get_teamshots(years,ps=False):
    shots = ["0-2%20Feet%20-%20Very%20Tight","2-4%20Feet%20-%20Tight",
             "4-6%20Feet%20-%20Open","6%2B%20Feet%20-%20Wide%20Open"]
    terms = ['very_tight.csv','tight.csv','open.csv','wide_open.csv']
    folder = '/team_shooting/'
    sfolder=''
    stype = "Regular%20Season"
    if ps == True:
        stype="Playoffs"
        sfolder = "/playoffs"
    dataframe=[]
    for year in years:
        i = 0
        for shot in shots:
            season = str(year)+'-'+str(year+1 - 2000)
            part1 = "https://stats.nba.com/stats/leaguedashteamptshot?CloseDefDistRange="
            part2 = "&College=&Conference=&Country=&DateFrom=&DateTo=&Division=&DraftPick=&DraftYear=&DribbleRange=&GameScope=&GameSegment=&GeneralRange=&Height=&LastNGames=0&LeagueID=00&Location=&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PerMode=Totals&Period=0&PlayerExperience=&PlayerPosition=&Season="
            part3 = "&SeasonSegment=&SeasonType="+stype+"&ShotClockRange=&ShotDistRange=&StarterBench=&VsConference=&VsDivision=&Weight="
            url = part1+shot+part2+season+part3
            # ... API call and processing

master_team_shooting()

Updates master team shooting files with new season data.
year
integer
required
Season year to update
ps
boolean
default:"False"
Set to True for playoff data
Returns: Updated master DataFrame

Output Files

team_shooting.csv / team_shooting_ps.csv

Team offensive shooting by defender distance.
TEAM_ID
integer
required
NBA team ID
TEAM
string
required
Team abbreviation (e.g., “LAL”, “GSW”)
TEAMNAME
string
Full team name
GP
integer
Games played
G
integer
Games (same as GP for teams)
FREQ%
float
Frequency of shots in this coverage type (0-100%)
FGM
integer
Field goals made
FGA
integer
Field goal attempts
FG%
float
Field goal percentage (0-100%)
EFG%
float
Effective field goal percentage (0-100%)
2FG FREQ%
float
Two-point frequency within category
2FGM
integer
Two-point field goals made
2FGA
integer
Two-point attempts
2FG%
float
Two-point percentage
3FG FREQ%
float
Three-point frequency within category
3PM
integer
Three-pointers made
3PA
integer
Three-point attempts
3P%
float
Three-point percentage
shot_coverage
string
required
Coverage type: “very_tight”, “tight”, “open”, “wide_open”
year
integer
required
Season year

opp_team_shooting.csv / opp_team_shooting_ps.csv

Opponent shooting allowed by the defense (team defensive stats). Same schema as team_shooting.csv but represents shots allowed by the defense.

Usage Examples

Collect Team Data

from team_shooting import get_teamshots

# Get 2024 regular season team shooting
df = get_teamshots([2024], ps=False)

# View a specific team's distribution
lakers = df[df['TEAM'] == 'LAL']
print(lakers[['shot_coverage', 'FGA', 'FG%', 'FREQ%']])

Update Master File

from team_shooting import master_team_shooting

# Update master file with 2024 data
master_df = master_team_shooting(2024, ps=False)

# Check coverage
print(f"Teams: {master_df['TEAM'].nunique()}")
print(f"Years: {master_df['year'].min()} - {master_df['year'].max()}")

Analyze Shot Quality

import pandas as pd

df = pd.read_csv('team_shooting.csv')
df_2024 = df[df['year'] == 2024]

# Calculate weighted shot quality (% of wide open shots)
wide_open_freq = df_2024[
    df_2024['shot_coverage'] == 'wide_open'
][['TEAM', 'FREQ%']].sort_values('FREQ%', ascending=False)

print("Teams generating most wide open shots:")
print(wide_open_freq.head(10))

Compare Offense vs Defense

import pandas as pd

offense = pd.read_csv('team_shooting.csv')
defense = pd.read_csv('opp_team_shooting.csv')

# 2024 season only
off_2024 = offense[offense['year'] == 2024]
def_2024 = defense[defense['year'] == 2024]

# Calculate net wide open advantage
off_wo = off_2024[off_2024['shot_coverage'] == 'wide_open'][['TEAM', 'FREQ%']].rename(columns={'FREQ%': 'OFF_WO%'})
def_wo = def_2024[def_2024['shot_coverage'] == 'wide_open'][['TEAM', 'FREQ%']].rename(columns={'FREQ%': 'DEF_WO%'})

net = off_wo.merge(def_wo, on='TEAM')
net['NET_WO%'] = net['OFF_WO%'] - net['DEF_WO%']

print("Best net wide open advantage (offense creates, defense prevents):")
print(net.sort_values('NET_WO%', ascending=False).head(10))

Shot Quality Efficiency

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv('team_shooting.csv')
df_2024 = df[df['year'] == 2024]

# Average efficiency by coverage type
efficiency = df_2024.groupby('shot_coverage').agg({
    'FG%': 'mean',
    'EFG%': 'mean',
    'FREQ%': 'mean'
}).reset_index()

print(efficiency)
# Expected: wide_open > open > tight > very_tight

Analysis Use Cases

Offensive Spacing

Measure team’s ability to generate open looks through ball movement and off-ball actions

Defensive Intensity

Evaluate how well a defense contests shots and limits open opportunities

Pace and Space

Identify teams that play faster and generate more wide open threes

Shot Quality vs Quantity

Balance between volume and quality of shot attempts

Configuration

Rate Limiting: Includes delays between API requests Season Format: Uses YYYY-YY format (e.g., “2023-24”) API Headers: Requires standard NBA.com headers (User-Agent, Host, Referer)

Build docs developers (and LLMs) love