Skip to main content

Overview

The defense.py script collects defensive statistics including opponent field goal percentage (DFG%), rim protection metrics, and defensive frequency data from NBA.com stats and PBP Stats API.

Data Sources

  • NBA.com Stats API: Defensive dashboard data (DFG%, rim defense)
  • PBP Stats API: Rim accuracy and frequency metrics
  • Endpoints:
    • stats.nba.com/stats/leaguedashptdefend
    • api.pbpstats.com/get-on-off/nba/stat

Core Functions

pull_data()

Fetches data from NBA.com stats endpoints.
url
string
required
Full NBA.com stats API URL
Returns: pd.DataFrame with defensive statistics
# From defense.py:22
def pull_data(url):
    headers = {
        "Host": "stats.nba.com",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0",
        "Accept": "application/json, text/plain, */*",
        "Referer": "https://stats.nba.com/"
    }
    json = requests.get(url, headers=headers).json()
    data = json["resultSets"][0]["rowSet"]
    columns = json["resultSets"][0]["headers"]
    df = pd.DataFrame.from_records(data, columns=columns)
    return df

prep_dfg()

Cleans and standardizes DFG (Defender Field Goal) data.
dfg
pd.DataFrame
required
Raw defensive data from NBA.com
Returns: Cleaned DataFrame with percentage values scaled to 0-100 range
# From defense.py:86
def prep_dfg(dfg):
    dfg = dfg.rename(columns={'DIFF%':'Diff%'})
    dfg.columns = ['PLAYER_ID','PLAYER', 'TEAM_ID','TEAM', 'POSITION', 'AGE',
                   'GP', 'G', 'FREQ%', 'DFGM', 'DFGA', 'DFG%', 'FG%', 'DIFF%']
    for col in dfg:
        if '%' in col:
            dfg[col] *= 100
    return dfg

wowy_statlog()

Collects rim protection stats from PBP Stats API.
stat
string
required
Stat type: "AtRimAccuracyOpponent" or "AtRimFrequencyOpponent"
start_year
integer
required
Starting year for data collection
ps
boolean
default:"False"
Playoffs mode toggle
Returns: DataFrame with on/off defensive metrics per team
# From defense.py:96
def wowy_statlog(stat, start_year, ps=False):
    s_type = 'Playoffs' if ps else 'Regular Season'
    player_dict, team_dict = get_index()
    
    for season in range(start_year, 2026):
        season_s = str(season-1) + '-' + str(season % 100)
        url = "https://api.pbpstats.com/get-on-off/nba/stat"
        
        for team in team_dict.keys():
            params = {
                "Season": season_s,
                "SeasonType": s_type,
                "TeamId": team_dict[team],
                "Stat": stat
            }
            response = requests.get(url, params=params)

update_dash()

Updates defensive dashboard data from NBA.com.
ps
boolean
default:"False"
If True, fetches playoff data. If False, regular season.
Returns: Saves two CSV files - overall DFG and rim DFG
# From defense.py:166
def update_dash(ps=False):
    stype = 'Playoffs' if ps else 'Regular%20Season'
    
    # Overall defense
    url = f"https://stats.nba.com/stats/leaguedashptdefend?DefenseCategory=Overall&SeasonType={stype}"
    df = pull_data(url)
    df = prep_dfg(df)
    df['year'] = 2025
    
    # Rim defense (< 6 feet)
    url_rim = f"https://stats.nba.com/stats/leaguedashptdefend?DefenseCategory=Less%20Than%206Ft&SeasonType={stype}"

Statistics Collected

DFG Stats (Defender Field Goal)

PLAYER_ID
integer
NBA player ID
DFGA
integer
Defensive Field Goal Attempts - shots defended
DFGM
integer
Defensive Field Goal Made - shots made when defended
DFG%
float
Opponent field goal percentage when defended
FG%
float
Normal field goal percentage (baseline)
DIFF%
float
Difference between DFG% and FG% (negative is better defense)
FREQ%
float
Frequency of defensive matchups

Rim Protection Stats

AtRimAccuracyOpponent
float
Opponent field goal percentage at the rim (< 6 feet)
AtRimFrequencyOpponent
float
Frequency of opponent attempts at the rim

Output Files

dfg.csv / dfg_p.csv
CSV
Overall defensive field goal dataPath: {year}/defense/dfg.csv or {year}/playoffs/defense/dfg_p.csv
rimdfg.csv / rimdfg_p.csv
CSV
Rim defense field goal data (shots < 6 feet)Path: {year}/defense/rimdfg.csv
rim_acc.csv
CSV
Rim accuracy opponent stats from PBP StatsPath: {year}/defense/rim_acc.csv
rimfreq.csv
CSV
Rim frequency opponent stats from PBP StatsPath: {year}/defense/rimfreq.csv

Usage Example

# Update defensive dashboard for 2024-25 season
update_dash(ps=False)  # Regular season

# Collect rim protection stats
stat = "AtRimAccuracyOpponent"
filename = '2025/defense/rim_acc.csv'
update_log(filename, stat, ps=False)

# Collect rim frequency stats
stat2 = "AtRimFrequencyOpponent"
filename2 = '2025/defense/rimfreq.csv'
update_log(filename2, stat2, ps=False)
Output:
Fetching data from: https://stats.nba.com/stats/leaguedashptdefend...
Processing 612 players for year 2025...
Saved to 2025/defense/dfg.csv
Saved to 2025/defense/rimdfg.csv

Master File Updates

# From defense.py:238-273
def update_masters(masters, ps=False):
    trail = '_p' if ps else ''
    end_year = 2026
    frames = [[] for _ in range(len(masters))]
    
    for year in range(2014, end_year):
        path = f"{year}/{'playoffs/' if ps else ''}defense/"
        for i, file in enumerate(masters):
            df = pd.read_csv(path + file + '.csv')
            frames[i].append(df)
    
    for i, master in enumerate(masters):
        masterframe = pd.concat(frames[i])
        masterframe.to_csv(masters[i] + trail + '.csv', index=False)
Merges individual year files into master files:
  • rimfreq.csv / rimfreq_p.csv
  • rim_acc.csv / rim_acc_p.csv
  • dfg.csv / dfg_p.csv
  • rimdfg.csv / rimdfg_p.csv

Build docs developers (and LLMs) love