complete app
test / test (push) Successful in 9s

This commit is contained in:
2026-09-17 17:39:22 -05:00
parent 4d633a39be
commit 3d5de1dc3e
84 changed files with 24229 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# ==============================================================================
# NBA Analytics Environment Configuration
# ==============================================================================
# ------------------------------------------------------------------------------
# 1. PostgreSQL Source Connection (Required for pipeline extraction)
# ------------------------------------------------------------------------------
POSTGRES_URL=postgresql://username:password@host:5432/nba_source_db
# ------------------------------------------------------------------------------
# 2. Cloudflare R2 / S3-Compatible Object Storage (Required for DB sync & CI/CD)
# ------------------------------------------------------------------------------
# Cloudflare R2 endpoint format: https://<ACCOUNT_ID>.r2.cloudflarestorage.com
S3_ENDPOINT_URL=https://<ACCOUNT_ID>.r2.cloudflarestorage.com
S3_BUCKET_NAME=nba-analytics
S3_ACCESS_KEY_ID=your_r2_access_key_id
S3_SECRET_ACCESS_KEY=your_r2_secret_access_key
S3_REGION=auto
S3_DB_KEY=dbt_nba.duckdb
# ------------------------------------------------------------------------------
# 3. dbt CLI Configuration (Allows running `uv run dbt ...` directly from root)
# ------------------------------------------------------------------------------
DBT_PROJECT_DIR=dbt_nba
DBT_PROFILES_DIR=dbt_nba
+42
View File
@@ -0,0 +1,42 @@
name: NBA Data Pipeline
on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
jobs:
build-and-sync:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install DuckDB CLI
run: |
curl -fsSL https://github.com/duckdb/duckdb/releases/latest/download/duckdb_cli-linux-amd64.zip -o duckdb.zip
unzip -o duckdb.zip
sudo mv duckdb /usr/local/bin/
rm -f duckdb.zip
- name: Install gettext (envsubst)
run: |
sudo apt-get update && sudo apt-get install -y gettext-base
- name: Run ETL Pipeline & Upload to Cloudflare R2
env:
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }}
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.S3_REGION || 'auto' }}
S3_DB_KEY: ${{ secrets.S3_DB_KEY || 'dbt_nba.duckdb' }}
run: |
chmod +x ./scripts/pipeline.sh ./scripts/db_storage.py
./scripts/pipeline.sh
+19
View File
@@ -176,3 +176,22 @@ cython_debug/
.docs
docs
# Database files (stored in S3 / Cloudflare R2, not in Git)
*.duckdb
*.duckdb.wal
*.duckdb.tmp
# dbt artifacts
dbt_nba/target/
dbt_nba/dbt_packages/
dbt_nba/logs/
# Environments & secrets
.env
.env.*
!.env.example
AGENTS.md
.agents
.claude
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

+4
View File
@@ -0,0 +1,4 @@
target/
dbt_packages/
logs/
+1
View File
@@ -0,0 +1 @@
id: 428675f2-8539-4353-aae8-d37a4d91a7f7
+15
View File
@@ -0,0 +1,15 @@
Welcome to your new dbt project!
### Using the starter project
Try running the following commands:
- dbt run
- dbt test
### Resources:
- Learn more about dbt [in the docs](https://docs.getdbt.com/docs/introduction)
- Check out [Discourse](https://discourse.getdbt.com/) for commonly asked questions and answers
- Join the [chat](https://community.getdbt.com/) on Slack for live discussions and support
- Find [dbt events](https://events.getdbt.com) near you
- Check out [the blog](https://blog.getdbt.com/) for the latest news on dbt's development and best practices
View File
+65
View File
@@ -0,0 +1,65 @@
name: 'dbt_nba'
version: '1.0.0'
config-version: 2
profile: 'dbt_nba'
model-paths: ["models"]
analysis-paths: ["analyses"]
test-paths: ["tests"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
snapshot-paths: ["snapshots"]
clean-targets:
- "target"
- "dbt_packages"
models:
dbt_nba:
+materialized: view
staging:
+materialized: view
+schema: staging
+tags: ['staging']
intermediate:
+materialized: table
+schema: intermediate
+tags: ['intermediate']
marts:
+materialized: table
+schema: marts
+tags: ['marts']
dimensions:
+materialized: table
+tags: ['dimension']
facts:
+materialized: incremental
+tags: ['fact']
tests:
dbt_nba:
+severity: warn
+store_failures: true
+schema: test_results
seeds:
dbt_nba:
+schema: seeds
vars:
start_date: '2002-10-01'
end_date: '2025-11-10'
min_games_played: 10
min_minutes_per_game: 15
clutch_time_minutes: 5
close_game_margin: 5
regular_season_start_month: 10
regular_season_end_month: 4
playoff_start_month: 4
playoff_end_month: 6
View File
@@ -0,0 +1,58 @@
---
version: 2
models:
- name: int_player_performance
description: "Unified model containing basic and advanced stats for each player in every game. Grain: one row per player per game."
tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- game_id
- player_id
columns:
- name: game_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: int_team_performance
description: "Unified model containing basic and advanced stats for each team in every game. Grain: one row per team per game."
tests:
- dbt_utils.unique_combination_of_columns:
combination_of_columns:
- game_id
- team
columns:
- name: game_id
tests:
- not_null
- name: team
tests:
- not_null
- name: int_games_enriched
description: "Game-level model enriched with detailed performance stats for both the home and visitor teams. Grain: one row per game."
columns:
- name: game_id
tests:
- unique
- not_null
- name: int_player_shots_enriched
description: >
Unified shot-level dataset combining field goal attempts (from shot charts)
with free throw attempts (derived from stg_player_game_basic_stats).
Grain: one row per shot attempt (FG or FT).
columns:
- name: shot_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: shot_source
tests:
- not_null
- accepted_values:
values: ['shot_chart', 'box_score_ft']
@@ -0,0 +1,81 @@
{{
config(
materialized='table',
schema='intermediate',
tags=["intermediate"]
)
}}
WITH games AS (
SELECT
g.*,
home_map.team_abbr AS home_team_abbr,
visitor_map.team_abbr AS visitor_team_abbr,
winning_map.team_abbr AS winning_team_abbr
FROM {{ ref('stg_games') }} AS g
LEFT JOIN {{ ref('team_maps') }} AS home_map ON g.home_team = home_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS visitor_map ON g.visitor_team = visitor_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS winning_map ON g.winning_team = winning_map.full_name
WHERE (g.season_start_year >= home_map.start_year AND g.season_start_year < home_map.end_year)
AND (g.season_start_year >= visitor_map.start_year AND g.season_start_year < visitor_map.end_year)
AND (g.season_start_year >= winning_map.start_year AND g.season_start_year < winning_map.end_year)
),
arena_locations AS (
SELECT arena_name, arena_city
FROM (
SELECT
arena_name,
city AS arena_city,
ROW_NUMBER() OVER (PARTITION BY arena_name ORDER BY city) as rn
FROM {{ ref('arena_maps') }}
) AS sub
WHERE rn = 1
),
team_performance AS (
SELECT * FROM {{ ref('int_team_performance') }}
),
final AS (
SELECT
g.game_id,
g.game_date,
g.season_start_year,
g.is_playoff,
g.arena,
al.arena_city,
g.home_team_abbr AS home_team,
g.visitor_team_abbr AS visitor_team,
g.winning_team_abbr AS winning_team,
g.home_points,
g.visitor_points,
g.point_differential,
g.total_points,
g.is_overtime,
home_stats.offensive_rating AS home_offensive_rating,
home_stats.defensive_rating AS home_defensive_rating,
home_stats.net_rating AS home_net_rating,
home_stats.pace AS home_pace,
home_stats.effective_fg_pct AS home_effective_fg_pct,
home_stats.turnover_rate AS home_turnover_rate,
home_stats.offensive_tier AS home_offensive_tier,
home_stats.defensive_tier AS home_defensive_tier,
visitor_stats.offensive_rating AS visitor_offensive_rating,
visitor_stats.defensive_rating AS visitor_defensive_rating,
visitor_stats.net_rating AS visitor_net_rating,
visitor_stats.pace AS visitor_pace,
visitor_stats.effective_fg_pct AS visitor_effective_fg_pct,
visitor_stats.turnover_rate AS visitor_turnover_rate,
visitor_stats.offensive_tier AS visitor_offensive_tier,
visitor_stats.defensive_tier AS visitor_defensive_tier,
(home_stats.pace + visitor_stats.pace) / 2 AS matchup_pace
FROM games g
LEFT JOIN arena_locations al ON g.arena = al.arena_name
LEFT JOIN team_performance AS home_stats
ON g.game_id = home_stats.game_id AND g.home_team_abbr = home_stats.team
LEFT JOIN team_performance AS visitor_stats
ON g.game_id = visitor_stats.game_id AND g.visitor_team_abbr = visitor_stats.team
)
SELECT * FROM final
@@ -0,0 +1,97 @@
{{
config(
materialized='table',
schema='intermediate',
tags=["intermediate"]
)
}}
WITH basic_stats AS (
SELECT
s.*,
map.team_abbr AS team_conformed
FROM {{ ref('stg_player_game_basic_stats') }} AS s
LEFT JOIN {{ ref('team_maps') }} AS map
ON s.team = map.team_abbr
LEFT JOIN {{ ref('stg_games') }} AS g
ON s.game_id = g.game_id
WHERE did_play = TRUE
AND (g.season_start_year >= map.start_year AND g.season_start_year < map.end_year)
),
adv_stats AS (
SELECT
s.*,
map.team_abbr AS team_conformed
FROM {{ ref('stg_player_game_adv_stats_extended') }} AS s
LEFT JOIN {{ ref('team_maps') }} AS map
ON s.team = map.team_abbr
LEFT JOIN {{ ref('stg_games') }} AS g
ON s.game_id = g.game_id
WHERE (g.season_start_year >= map.start_year AND g.season_start_year < map.end_year)
),
games AS (
SELECT
g.game_id,
g.game_date,
g.season_start_year,
g.is_playoff,
home_map.team_abbr AS home_team_abbr,
winning_map.team_abbr AS winning_team_abbr
FROM {{ ref('stg_games') }} g
LEFT JOIN {{ ref('team_maps') }} AS home_map ON g.home_team = home_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS winning_map ON g.winning_team = winning_map.full_name
WHERE (g.season_start_year >= home_map.start_year AND g.season_start_year < home_map.end_year)
AND (g.season_start_year >= winning_map.start_year AND g.season_start_year < winning_map.end_year)
),
final AS (
SELECT
b.game_id,
b.player_id,
b.player_name,
b.team_conformed AS team,
g.game_date,
g.season_start_year,
g.is_playoff,
CASE WHEN b.team_conformed = g.winning_team_abbr THEN 'W' ELSE 'L' END AS game_result,
CASE WHEN b.team_conformed = g.home_team_abbr THEN 'HOME' ELSE 'AWAY' END AS team_location,
b.minutes_played,
b.points,
b.assists,
b.total_rebounds,
b.steals,
b.blocks,
b.turnovers,
b.plus_minus,
a.net_rating,
a.box_plus_minus,
b.field_goals_made,
b.field_goals_attempted,
b.field_goal_pct,
b.three_pointers_made,
b.three_pointers_attempted,
b.three_point_pct,
a.true_shooting_pct,
a.effective_fg_pct,
a.usage_pct,
a.offensive_rating,
a.defensive_rating,
a.usage_tier,
a.impact_tier,
a.shooting_efficiency_tier,
a.minutes_based_role,
b.is_double_double,
b.is_triple_double,
a.is_versatile,
a.is_defensive_specialist,
a.is_three_and_d
FROM basic_stats AS b
LEFT JOIN adv_stats AS a
ON b.game_id = a.game_id AND b.player_id = a.player_id
LEFT JOIN games AS g
ON b.game_id = g.game_id
)
SELECT * FROM final
@@ -0,0 +1,214 @@
{{
config(
materialized='table',
schema='intermediate',
tags=["intermediate"]
)
}}
WITH shot_charts AS (
SELECT * FROM {{ ref('stg_player_shot_charts') }}
),
games AS (
SELECT
g.game_id,
g.game_date,
g.season_start_year,
g.is_playoff,
g.arena,
g.home_points,
g.visitor_points,
g.point_differential,
g.is_overtime,
home_map.team_abbr AS home_team_abbr,
visitor_map.team_abbr AS visitor_team_abbr,
winning_map.team_abbr AS winning_team_abbr
FROM {{ ref('stg_games') }} AS g
LEFT JOIN {{ ref('team_maps') }} AS home_map ON g.home_team = home_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS visitor_map ON g.visitor_team = visitor_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS winning_map ON g.winning_team = winning_map.full_name
WHERE (g.season_start_year >= home_map.start_year AND g.season_start_year < home_map.end_year)
AND (g.season_start_year >= visitor_map.start_year AND g.season_start_year < visitor_map.end_year)
AND (g.season_start_year >= winning_map.start_year AND g.season_start_year < winning_map.end_year)
),
shots_conformed AS (
SELECT
sc.*,
COALESCE(tm.team_abbr, sc.team_abbr_raw) AS team_conformed,
COALESCE(opp.team_abbr, sc.opponent_abbr_raw) AS opponent_conformed
FROM shot_charts AS sc
LEFT JOIN {{ ref('team_maps') }} AS tm
ON sc.team_abbr_raw = tm.team_abbr
AND sc.season_start_year >= tm.start_year
AND sc.season_start_year < tm.end_year
LEFT JOIN {{ ref('team_maps') }} AS opp
ON sc.opponent_abbr_raw = opp.team_abbr
AND sc.season_start_year >= opp.start_year
AND sc.season_start_year < opp.end_year
),
shots_with_game AS (
SELECT
sc.*,
g.game_id,
g.is_playoff,
g.arena,
g.is_overtime AS game_had_overtime,
CASE
WHEN sc.team_conformed = g.home_team_abbr THEN 'HOME'
WHEN sc.team_conformed = g.visitor_team_abbr THEN 'AWAY'
ELSE NULL
END AS team_location,
CASE
WHEN sc.team_conformed = g.winning_team_abbr THEN 'W'
ELSE 'L'
END AS game_result
FROM shots_conformed AS sc
LEFT JOIN games AS g
ON CAST(sc.game_date AS DATE) = CAST(g.game_date AS DATE)
AND (
(sc.team_conformed = g.home_team_abbr AND sc.opponent_conformed = g.visitor_team_abbr)
OR
(sc.team_conformed = g.visitor_team_abbr AND sc.opponent_conformed = g.home_team_abbr)
)
),
fg_shots AS (
SELECT
shot_id,
game_id,
player_id,
team_conformed AS team,
opponent_conformed AS opponent,
game_date,
game_date_raw,
season_start_year,
is_playoff,
team_location,
game_result,
game_had_overtime,
quarter_raw,
quarter_number,
is_overtime_shot,
time_remaining_raw,
seconds_remaining_in_quarter,
shot_x_coordinate,
shot_y_coordinate,
is_made,
shot_made_flag,
shot_missed_flag,
shot_type_raw,
shot_point_value,
is_three_pointer,
is_free_throw,
distance_ft,
shot_distance_zone,
points_generated,
team_had_lead,
team_score_at_shot,
opponent_score_at_shot,
score_margin_at_shot,
is_clutch_shot,
CASE WHEN game_id IS NOT NULL THEN TRUE ELSE FALSE END AS has_game_match,
'shot_chart' AS shot_source,
created_at,
updated_at,
dbt_loaded_at
FROM shots_with_game
),
-- Free throw expansion using DuckDB's generate_series + UNNEST
matched_player_games AS (
SELECT DISTINCT
game_id, player_id, team, opponent, game_date, game_date_raw,
season_start_year, is_playoff, team_location, game_result, game_had_overtime
FROM fg_shots
WHERE has_game_match = TRUE
),
ft_source AS (
SELECT
mpg.*,
bs.free_throws_made,
bs.free_throws_attempted
FROM matched_player_games AS mpg
INNER JOIN {{ ref('stg_player_game_basic_stats') }} AS bs
ON mpg.game_id = bs.game_id AND mpg.player_id = bs.player_id
WHERE bs.did_play = TRUE AND bs.free_throws_attempted > 0
),
ft_made_expanded AS (
SELECT ft.*, n.n AS ft_seq, TRUE AS ft_is_made
FROM ft_source AS ft
CROSS JOIN LATERAL (SELECT UNNEST(generate_series(1, GREATEST(ft.free_throws_made, 0))) AS n) AS n
WHERE ft.free_throws_made > 0
),
ft_missed_expanded AS (
SELECT ft.*, n.n AS ft_seq, FALSE AS ft_is_made
FROM ft_source AS ft
CROSS JOIN LATERAL (SELECT UNNEST(generate_series(1, GREATEST(ft.free_throws_attempted - ft.free_throws_made, 0))) AS n) AS n
WHERE ft.free_throws_attempted > ft.free_throws_made
),
ft_shots AS (
SELECT
-(ABS(hash(
ft.game_id || '|' || ft.player_id || '|FT_'
|| CASE WHEN ft.ft_is_made THEN 'MADE' ELSE 'MISS' END
|| '|' || CAST(ft.ft_seq AS TEXT)
)) % 9223372036854775807 + 1) AS shot_id,
ft.game_id,
ft.player_id,
ft.team,
ft.opponent,
ft.game_date,
ft.game_date_raw,
ft.season_start_year,
ft.is_playoff,
ft.team_location,
ft.game_result,
ft.game_had_overtime,
'Free Throw' AS quarter_raw,
0 AS quarter_number,
FALSE AS is_overtime_shot,
NULL AS time_remaining_raw,
NULL::INT AS seconds_remaining_in_quarter,
NULL::BIGINT AS shot_x_coordinate,
NULL::BIGINT AS shot_y_coordinate,
ft.ft_is_made AS is_made,
CASE WHEN ft.ft_is_made THEN 1 ELSE 0 END AS shot_made_flag,
CASE WHEN ft.ft_is_made THEN 0 ELSE 1 END AS shot_missed_flag,
'free-throw' AS shot_type_raw,
1 AS shot_point_value,
FALSE AS is_three_pointer,
TRUE AS is_free_throw,
15 AS distance_ft,
'Free Throw (15 ft)' AS shot_distance_zone,
CASE WHEN ft.ft_is_made THEN 1 ELSE 0 END AS points_generated,
NULL::BOOLEAN AS team_had_lead,
NULL::BIGINT AS team_score_at_shot,
NULL::BIGINT AS opponent_score_at_shot,
NULL::INT AS score_margin_at_shot,
FALSE AS is_clutch_shot,
TRUE AS has_game_match,
'box_score_ft' AS shot_source,
CURRENT_TIMESTAMP AS created_at,
CURRENT_TIMESTAMP AS updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM (
SELECT * FROM ft_made_expanded
UNION ALL
SELECT * FROM ft_missed_expanded
) AS ft
),
final AS (
SELECT * FROM fg_shots
UNION ALL
SELECT * FROM ft_shots
)
SELECT * FROM final
@@ -0,0 +1,85 @@
{{
config(
materialized='table',
schema='intermediate',
tags=["intermediate"]
)
}}
WITH basic_stats AS (
SELECT
s.*,
map.team_abbr AS team_conformed
FROM {{ ref('stg_team_game_basic_stats') }} AS s
LEFT JOIN {{ ref('team_maps') }} AS map
ON s.team = map.team_abbr
LEFT JOIN {{ ref('stg_games') }} AS g
ON s.game_id = g.game_id
WHERE (g.season_start_year >= map.start_year AND g.season_start_year < map.end_year)
),
adv_stats AS (
SELECT
s.*,
map.team_abbr AS team_conformed
FROM {{ ref('stg_team_game_adv_stats') }} AS s
LEFT JOIN {{ ref('team_maps') }} AS map
ON s.team = map.team_abbr
LEFT JOIN {{ ref('stg_games') }} AS g
ON s.game_id = g.game_id
WHERE (g.season_start_year >= map.start_year AND g.season_start_year < map.end_year)
),
games AS (
SELECT
g.game_id,
g.game_date,
g.season_start_year,
g.is_playoff,
home_map.team_abbr AS home_team_abbr,
visitor_map.team_abbr AS visitor_team_abbr,
winning_map.team_abbr AS winning_team_abbr
FROM {{ ref('stg_games') }} g
LEFT JOIN {{ ref('team_maps') }} AS home_map ON g.home_team = home_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS visitor_map ON g.visitor_team = visitor_map.full_name
LEFT JOIN {{ ref('team_maps') }} AS winning_map ON g.winning_team = winning_map.full_name
WHERE (g.season_start_year >= home_map.start_year AND g.season_start_year < home_map.end_year)
AND (g.season_start_year >= visitor_map.start_year AND g.season_start_year < visitor_map.end_year)
AND (g.season_start_year >= winning_map.start_year AND g.season_start_year < winning_map.end_year)
),
final AS (
SELECT
b.game_id,
b.team_conformed AS team,
g.game_date,
g.season_start_year,
g.is_playoff,
CASE WHEN b.team_conformed = g.winning_team_abbr THEN 'W' ELSE 'L' END AS game_result,
CASE
WHEN b.team_conformed = g.home_team_abbr THEN g.visitor_team_abbr
ELSE g.home_team_abbr
END AS opponent_team,
b.points,
a.offensive_rating,
a.defensive_rating,
a.net_rating,
b.pace,
b.effective_fg_pct,
b.turnover_rate,
b.offensive_rebound_rate,
b.free_throw_rate,
a.offensive_tier,
a.defensive_tier,
a.shot_selection_style,
a.ball_movement_style,
a.ball_security_tier,
a.defensive_activity
FROM basic_stats AS b
LEFT JOIN adv_stats AS a
ON b.game_id = a.game_id AND b.team_conformed = a.team_conformed
LEFT JOIN games AS g
ON b.game_id = g.game_id
)
SELECT * FROM final
@@ -0,0 +1,21 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
WITH source AS (
SELECT arena_name, city, state_or_country, location
FROM {{ ref('arena_mappings') }}
)
SELECT
{{ dbt_utils.generate_surrogate_key(['arena_name', 'city']) }} AS arena_key,
arena_name,
city AS arena_city,
state_or_country AS arena_state_or_country,
location AS arena_location
FROM source
ORDER BY arena_name, arena_city
@@ -0,0 +1,44 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
{% set start_date_query %}
select min(game_date)::date from {{ ref('stg_games') }}
{% endset %}
{% set start_date = dbt_utils.get_single_value(start_date_query) %}
{% set end_date_query %}
select max(game_date)::date from {{ ref('stg_games') }}
{% endset %}
{% set end_date = dbt_utils.get_single_value(end_date_query) %}
WITH date_spine AS (
SELECT UNNEST(generate_series(
'{{ start_date }}'::date,
'{{ end_date }}'::date,
INTERVAL '1 day'
))::date AS date_day
)
SELECT
CAST(strftime(date_day, '%Y%m%d') AS INTEGER) AS date_key,
date_day AS full_date,
EXTRACT(YEAR FROM date_day)::int AS year,
EXTRACT(QUARTER FROM date_day)::int AS quarter_of_year,
EXTRACT(MONTH FROM date_day)::int AS month_of_year,
strftime(date_day, '%B') AS month_name,
EXTRACT(DAY FROM date_day)::int AS day_of_month,
EXTRACT(ISODOW FROM date_day)::int AS day_of_week,
strftime(date_day, '%A') AS day_of_week_name,
EXTRACT(DOY FROM date_day)::int AS day_of_year,
EXTRACT(WEEK FROM date_day)::int AS week_of_year,
CASE
WHEN EXTRACT(ISODOW FROM date_day) IN (6, 7) THEN true
ELSE false
END AS is_weekend
FROM date_spine
ORDER BY full_date
@@ -0,0 +1,36 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
WITH distinct_archetypes AS (
SELECT DISTINCT
usage_tier,
impact_tier,
shooting_efficiency_tier,
minutes_based_role,
is_double_double,
is_triple_double,
is_versatile,
is_defensive_specialist,
is_three_and_d
FROM {{ ref('int_player_performance') }}
)
SELECT
{{ dbt_utils.generate_surrogate_key([
'usage_tier',
'impact_tier',
'shooting_efficiency_tier',
'minutes_based_role',
'is_double_double',
'is_triple_double',
'is_versatile',
'is_defensive_specialist',
'is_three_and_d'
]) }} AS archetype_key,
*
FROM distinct_archetypes
@@ -0,0 +1,26 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
WITH player_game_stats AS (
SELECT
stats.player_id,
stats.player_name,
games.game_date,
ROW_NUMBER() OVER (PARTITION BY stats.player_id ORDER BY games.game_date DESC) as rn
FROM {{ ref('stg_player_game_basic_stats') }} AS stats
LEFT JOIN {{ ref('stg_games') }} AS games
ON stats.game_id = games.game_id
)
SELECT
{{ dbt_utils.generate_surrogate_key(['player_id']) }} AS player_key,
player_id,
player_name
FROM player_game_stats
WHERE rn = 1
ORDER BY player_name
@@ -0,0 +1,19 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
WITH all_seasons AS (
SELECT DISTINCT season_start_year
FROM {{ ref('int_games_enriched') }}
)
SELECT
{{ dbt_utils.generate_surrogate_key(['season_start_year']) }} AS season_key,
season_start_year,
season_start_year || '-' || SUBSTR(CAST(season_start_year + 1 AS VARCHAR), 3, 2) AS season_display
FROM all_seasons
ORDER BY season_start_year
@@ -0,0 +1,32 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
WITH zones AS (
SELECT * FROM (
VALUES
('At Rim (0-3 ft)', 0, 3, 1, 'Paint', 'Interior', 'Field Goal'),
('Short Range (4-10 ft)', 4, 10, 2, 'Paint', 'Interior', 'Field Goal'),
('Mid Range (11-16 ft)', 11, 16, 3, 'Mid Range', 'Mid Range', 'Field Goal'),
('Long Mid Range (17-23 ft)', 17, 23, 4, 'Mid Range', 'Mid Range', 'Field Goal'),
('Three Point (24-27 ft)', 24, 27, 5, 'Perimeter', 'Three Point', 'Field Goal'),
('Deep Three (28+ ft)', 28, 50, 6, 'Perimeter', 'Three Point', 'Field Goal'),
('Free Throw (15 ft)', 15, 15, 7, 'Free Throw', 'Free Throw', 'Free Throw')
) AS t(shot_distance_zone, min_distance_ft, max_distance_ft, zone_order, zone_group, zone_category, shot_class)
)
SELECT
{{ dbt_utils.generate_surrogate_key(['shot_distance_zone']) }} AS shot_zone_key,
shot_distance_zone,
min_distance_ft,
max_distance_ft,
zone_order,
zone_group,
zone_category,
shot_class
FROM zones
ORDER BY zone_order
@@ -0,0 +1,19 @@
{{
config(
materialized='table',
schema='marts',
tags=["dimension"]
)
}}
WITH team_mappings AS (
SELECT team_abbr, full_name
FROM {{ ref('team_maps') }}
)
SELECT
{{ dbt_utils.generate_surrogate_key(['team_abbr']) }} AS team_key,
team_abbr,
full_name AS team_full_name
FROM team_mappings
ORDER BY team_abbr
@@ -0,0 +1,50 @@
{{
config(
materialized='incremental',
schema='marts',
unique_key='game_key',
tags=["fact"]
)
}}
WITH games_enriched AS (
SELECT *
FROM {{ ref('int_games_enriched') }}
{% if is_incremental() %}
WHERE game_date >= (SELECT MAX(game_date) FROM {{ this }}) - INTERVAL '30 days'
{% endif %}
)
SELECT
{{ dbt_utils.generate_surrogate_key(['ge.game_id']) }} AS game_key,
d.date_key,
s.season_key,
a.arena_key,
home_team.team_key AS home_team_key,
visitor_team.team_key AS visitor_team_key,
winning_team.team_key AS winning_team_key,
ge.game_id,
ge.home_points,
ge.visitor_points,
ge.point_differential,
ge.total_points,
ge.is_playoff,
ge.is_overtime,
CASE WHEN ge.home_points > ge.visitor_points THEN TRUE ELSE FALSE END AS is_home_team_winner,
ge.home_net_rating,
ge.visitor_net_rating,
ge.matchup_pace,
CASE
WHEN ge.point_differential <= 5 THEN 'Clutch Game'
WHEN ge.point_differential <= 10 THEN 'Competitive'
WHEN ge.point_differential <= 20 THEN 'Decisive'
ELSE 'Blowout'
END AS game_competitiveness_tier,
CAST(ge.game_date AS DATE) AS game_date
FROM games_enriched AS ge
LEFT JOIN {{ ref('dim_dates') }} AS d ON CAST(ge.game_date AS DATE) = d.full_date
LEFT JOIN {{ ref('dim_seasons') }} AS s ON ge.season_start_year = s.season_start_year
LEFT JOIN {{ ref('dim_arenas') }} AS a ON ge.arena = a.arena_name AND ge.arena_city = a.arena_city
LEFT JOIN {{ ref('dim_teams') }} AS home_team ON ge.home_team = home_team.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS visitor_team ON ge.visitor_team = visitor_team.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS winning_team ON ge.winning_team = winning_team.team_abbr
@@ -0,0 +1,175 @@
{{
config(
materialized='incremental',
schema='marts',
unique_key='player_game_shooting_key',
tags=["fact"]
)
}}
WITH shots AS (
SELECT *
FROM {{ ref('int_player_shots_enriched') }}
WHERE has_game_match = TRUE
{% if is_incremental() %}
AND game_date >= (SELECT MAX(game_date) FROM {{ this }}) - INTERVAL '30 days'
{% endif %}
),
game_agg AS (
SELECT
game_id, player_id, team, opponent,
MIN(game_date) AS game_date,
MIN(season_start_year) AS season_start_year,
BOOL_OR(is_playoff) AS is_playoff,
MIN(team_location) AS team_location,
MIN(game_result) AS game_result,
SUM(points_generated) AS total_points,
COUNT(*) AS total_shot_attempts,
SUM(shot_made_flag) AS total_shots_made,
COUNT(*) FILTER (WHERE shot_source = 'shot_chart') AS fg_attempts,
SUM(shot_made_flag) FILTER (WHERE shot_source = 'shot_chart') AS fg_makes,
CASE WHEN COUNT(*) FILTER (WHERE shot_source = 'shot_chart') > 0
THEN SUM(shot_made_flag) FILTER (WHERE shot_source = 'shot_chart')::DECIMAL
/ COUNT(*) FILTER (WHERE shot_source = 'shot_chart')::DECIMAL
ELSE 0
END AS fg_pct,
SUM(points_generated) FILTER (WHERE shot_source = 'shot_chart') AS fg_points,
COUNT(*) FILTER (WHERE shot_source = 'box_score_ft') AS ft_attempts,
SUM(shot_made_flag) FILTER (WHERE shot_source = 'box_score_ft') AS ft_makes,
CASE WHEN COUNT(*) FILTER (WHERE shot_source = 'box_score_ft') > 0
THEN SUM(shot_made_flag) FILTER (WHERE shot_source = 'box_score_ft')::DECIMAL
/ COUNT(*) FILTER (WHERE shot_source = 'box_score_ft')::DECIMAL
ELSE 0
END AS ft_pct,
SUM(points_generated) FILTER (WHERE shot_source = 'box_score_ft') AS ft_points,
COUNT(*) FILTER (WHERE is_three_pointer = FALSE AND shot_source = 'shot_chart') AS two_point_attempts,
SUM(shot_made_flag) FILTER (WHERE is_three_pointer = FALSE AND shot_source = 'shot_chart') AS two_point_makes,
CASE WHEN COUNT(*) FILTER (WHERE is_three_pointer = FALSE AND shot_source = 'shot_chart') > 0
THEN SUM(shot_made_flag) FILTER (WHERE is_three_pointer = FALSE AND shot_source = 'shot_chart')::DECIMAL
/ COUNT(*) FILTER (WHERE is_three_pointer = FALSE AND shot_source = 'shot_chart')::DECIMAL
ELSE 0
END AS two_point_fg_pct,
COUNT(*) FILTER (WHERE is_three_pointer = TRUE) AS three_point_attempts,
SUM(shot_made_flag) FILTER (WHERE is_three_pointer = TRUE) AS three_point_makes,
CASE WHEN COUNT(*) FILTER (WHERE is_three_pointer = TRUE) > 0
THEN SUM(shot_made_flag) FILTER (WHERE is_three_pointer = TRUE)::DECIMAL
/ COUNT(*) FILTER (WHERE is_three_pointer = TRUE)::DECIMAL
ELSE 0
END AS three_point_fg_pct,
COUNT(*) FILTER (WHERE shot_distance_zone = 'At Rim (0-3 ft)') AS at_rim_attempts,
SUM(shot_made_flag) FILTER (WHERE shot_distance_zone = 'At Rim (0-3 ft)') AS at_rim_makes,
CASE WHEN COUNT(*) FILTER (WHERE shot_distance_zone = 'At Rim (0-3 ft)') > 0
THEN SUM(shot_made_flag) FILTER (WHERE shot_distance_zone = 'At Rim (0-3 ft)')::DECIMAL
/ COUNT(*) FILTER (WHERE shot_distance_zone = 'At Rim (0-3 ft)')::DECIMAL
ELSE 0
END AS at_rim_fg_pct,
COUNT(*) FILTER (WHERE is_clutch_shot = TRUE) AS clutch_fg_attempts,
SUM(shot_made_flag) FILTER (WHERE is_clutch_shot = TRUE) AS clutch_fg_makes,
CASE WHEN COUNT(*) FILTER (WHERE is_clutch_shot = TRUE) > 0
THEN SUM(shot_made_flag) FILTER (WHERE is_clutch_shot = TRUE)::DECIMAL
/ COUNT(*) FILTER (WHERE is_clutch_shot = TRUE)::DECIMAL
ELSE 0
END AS clutch_fg_pct,
CASE WHEN COUNT(*) FILTER (WHERE shot_source = 'shot_chart') > 0
THEN COUNT(*) FILTER (WHERE is_three_pointer = TRUE)::DECIMAL
/ COUNT(*) FILTER (WHERE shot_source = 'shot_chart')::DECIMAL
ELSE 0
END AS three_point_rate,
CASE WHEN COUNT(*) FILTER (WHERE shot_source = 'shot_chart') > 0
THEN COUNT(*) FILTER (WHERE shot_distance_zone = 'At Rim (0-3 ft)')::DECIMAL
/ COUNT(*) FILTER (WHERE shot_source = 'shot_chart')::DECIMAL
ELSE 0
END AS at_rim_rate,
CASE WHEN COUNT(*) FILTER (WHERE shot_source = 'shot_chart') > 0
THEN COUNT(*) FILTER (WHERE shot_distance_zone IN ('Mid Range (11-16 ft)', 'Long Mid Range (17-23 ft)'))::DECIMAL
/ COUNT(*) FILTER (WHERE shot_source = 'shot_chart')::DECIMAL
ELSE 0
END AS mid_range_rate,
AVG(distance_ft) FILTER (WHERE shot_source = 'shot_chart') AS avg_fg_distance_ft,
CASE WHEN (
COUNT(*) FILTER (WHERE shot_source = 'shot_chart')
+ 0.44 * COUNT(*) FILTER (WHERE shot_source = 'box_score_ft')
) > 0
THEN SUM(points_generated)::DECIMAL / (
2.0 * (
COUNT(*) FILTER (WHERE shot_source = 'shot_chart')
+ 0.44 * COUNT(*) FILTER (WHERE shot_source = 'box_score_ft')
)
)
ELSE 0
END AS true_shooting_pct
FROM shots
GROUP BY game_id, player_id, team, opponent
),
final AS (
SELECT
{{ dbt_utils.generate_surrogate_key(['ga.game_id', 'ga.player_id']) }} AS player_game_shooting_key,
p.player_key,
t.team_key,
opp_t.team_key AS opponent_key,
d.date_key,
s.season_key,
ga.game_id,
ga.is_playoff,
ga.team_location,
ga.game_result,
ga.total_points,
ga.total_shot_attempts,
ga.total_shots_made,
ga.fg_attempts,
ga.fg_makes,
ga.fg_pct,
ga.fg_points,
ga.ft_attempts,
ga.ft_makes,
ga.ft_pct,
ga.ft_points,
ga.true_shooting_pct,
ga.two_point_attempts,
ga.two_point_makes,
ga.two_point_fg_pct,
ga.three_point_attempts,
ga.three_point_makes,
ga.three_point_fg_pct,
ga.at_rim_attempts,
ga.at_rim_makes,
ga.at_rim_fg_pct,
ga.clutch_fg_attempts,
ga.clutch_fg_makes,
ga.clutch_fg_pct,
ga.three_point_rate,
ga.at_rim_rate,
ga.mid_range_rate,
ga.avg_fg_distance_ft,
CASE
WHEN ga.three_point_rate >= 0.50 THEN 'Perimeter Heavy'
WHEN ga.at_rim_rate >= 0.50 THEN 'Rim Attacker'
WHEN ga.mid_range_rate >= 0.40 THEN 'Mid Range Heavy'
WHEN ga.three_point_rate >= 0.35 AND ga.at_rim_rate >= 0.30 THEN 'Modern (Rim & Three)'
ELSE 'Balanced'
END AS shot_profile_type,
CAST(ga.game_date AS DATE) AS game_date
FROM game_agg AS ga
LEFT JOIN {{ ref('dim_players') }} AS p ON ga.player_id = p.player_id
LEFT JOIN {{ ref('dim_teams') }} AS t ON ga.team = t.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS opp_t ON ga.opponent = opp_t.team_abbr
LEFT JOIN {{ ref('dim_dates') }} AS d ON CAST(ga.game_date AS DATE) = d.full_date
LEFT JOIN {{ ref('dim_seasons') }} AS s ON ga.season_start_year = s.season_start_year
)
SELECT * FROM final
@@ -0,0 +1,71 @@
{{
config(
materialized='incremental',
schema='marts',
unique_key='player_game_key',
tags=["fact"]
)
}}
WITH player_performance AS (
SELECT * FROM {{ ref('int_player_performance') }}
),
game_details AS (
SELECT game_id, arena AS arena_name, arena_city
FROM {{ ref('int_games_enriched') }}
),
final AS (
SELECT
{{ dbt_utils.generate_surrogate_key(['pp.game_id', 'pp.player_id']) }} AS player_game_key,
p.player_key,
t.team_key,
d.date_key,
s.season_key,
a.arena_key,
arch.archetype_key,
pp.game_id,
pp.minutes_played,
pp.points,
pp.assists,
pp.total_rebounds,
pp.steals,
pp.blocks,
pp.turnovers,
pp.plus_minus,
pp.net_rating,
pp.box_plus_minus,
pp.field_goals_made,
pp.field_goals_attempted,
pp.three_pointers_made,
pp.three_pointers_attempted,
pp.true_shooting_pct,
pp.effective_fg_pct,
pp.usage_pct,
pp.offensive_rating,
pp.defensive_rating,
CAST(pp.game_date AS DATE) AS game_date
FROM player_performance AS pp
LEFT JOIN game_details AS gd ON pp.game_id = gd.game_id
LEFT JOIN {{ ref('dim_player_game_archetypes') }} AS arch
ON pp.usage_tier = arch.usage_tier
AND pp.impact_tier = arch.impact_tier
AND pp.shooting_efficiency_tier = arch.shooting_efficiency_tier
AND pp.minutes_based_role = arch.minutes_based_role
AND pp.is_double_double = arch.is_double_double
AND pp.is_triple_double = arch.is_triple_double
AND pp.is_versatile = arch.is_versatile
AND pp.is_defensive_specialist = arch.is_defensive_specialist
AND pp.is_three_and_d = arch.is_three_and_d
LEFT JOIN {{ ref('dim_players') }} AS p ON pp.player_id = p.player_id
LEFT JOIN {{ ref('dim_teams') }} AS t ON pp.team = t.team_abbr
LEFT JOIN {{ ref('dim_dates') }} AS d ON CAST(pp.game_date AS DATE) = d.full_date
LEFT JOIN {{ ref('dim_seasons') }} AS s ON pp.season_start_year = s.season_start_year
LEFT JOIN {{ ref('dim_arenas') }} AS a ON gd.arena_name = a.arena_name AND gd.arena_city = a.arena_city
{% if is_incremental() %}
WHERE pp.game_date >= (SELECT MAX(game_date) FROM {{ this }}) - INTERVAL '30 days'
{% endif %}
)
SELECT * FROM final
@@ -0,0 +1,62 @@
{{
config(
materialized='incremental',
schema='marts',
unique_key='shot_key',
tags=["fact"]
)
}}
WITH shots_enriched AS (
SELECT *
FROM {{ ref('int_player_shots_enriched') }}
WHERE has_game_match = TRUE
{% if is_incremental() %}
AND game_date >= (SELECT MAX(game_date) FROM {{ this }}) - INTERVAL '30 days'
{% endif %}
),
final AS (
SELECT
{{ dbt_utils.generate_surrogate_key(['se.shot_id', 'se.shot_source']) }} AS shot_key,
p.player_key,
t.team_key,
opp_t.team_key AS opponent_key,
d.date_key,
s.season_key,
se.game_id,
se.shot_id,
se.shot_source,
se.is_playoff,
se.team_location,
se.game_result,
se.quarter_number,
se.is_overtime_shot,
se.seconds_remaining_in_quarter,
se.shot_x_coordinate,
se.shot_y_coordinate,
se.is_made,
se.shot_made_flag,
se.shot_missed_flag,
se.shot_type_raw AS shot_type,
se.shot_point_value,
se.is_three_pointer,
se.is_free_throw,
se.distance_ft,
se.shot_distance_zone,
se.points_generated,
se.team_had_lead,
se.team_score_at_shot,
se.opponent_score_at_shot,
se.score_margin_at_shot,
se.is_clutch_shot,
CAST(se.game_date AS DATE) AS game_date
FROM shots_enriched AS se
LEFT JOIN {{ ref('dim_players') }} AS p ON se.player_id = p.player_id
LEFT JOIN {{ ref('dim_teams') }} AS t ON se.team = t.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS opp_t ON se.opponent = opp_t.team_abbr
LEFT JOIN {{ ref('dim_dates') }} AS d ON CAST(se.game_date AS DATE) = d.full_date
LEFT JOIN {{ ref('dim_seasons') }} AS s ON se.season_start_year = s.season_start_year
)
SELECT * FROM final
@@ -0,0 +1,57 @@
{{
config(
materialized='incremental',
schema='marts',
unique_key='quarter_scoring_key',
tags=["fact"]
)
}}
WITH games AS (
SELECT game_id, game_date, season_start_year
FROM {{ ref('int_games_enriched') }}
{% if is_incremental() %}
WHERE game_date >= (SELECT MAX(game_date) FROM {{ this }}) - INTERVAL '30 days'
{% endif %}
),
unpivoted_scores AS (
{{ dbt_utils.unpivot(
relation=ref('stg_line_scores'),
cast_to='INTEGER',
exclude=['game_id', 'team', 'total_points', 'first_half_points', 'second_half_points', 'regulation_points', 'overtime_points', 'had_ot1', 'had_ot2', 'had_ot3', 'q2_momentum', 'q3_momentum', 'q4_momentum', 'max_quarter_score', 'min_quarter_score', 'best_quarter', 'created_at', 'updated_at', 'dbt_loaded_at'],
field_name='period_name',
value_name='points_scored'
) }}
WHERE game_id IN (SELECT game_id FROM games)
),
final AS (
SELECT
{{ dbt_utils.generate_surrogate_key(['team_scores.game_id', 'team_map.team_abbr', 'team_scores.period_name']) }} AS quarter_scoring_key,
d.date_key,
s.season_key,
t.team_key,
opp.team_key AS opponent_key,
team_scores.game_id,
REPLACE(UPPER(team_scores.period_name), '_POINTS', '') AS period,
team_scores.points_scored,
opponent_scores.points_scored AS opponent_points_scored,
(team_scores.points_scored - opponent_scores.points_scored) AS period_point_differential,
CAST(g.game_date AS DATE) AS game_date
FROM unpivoted_scores AS team_scores
INNER JOIN unpivoted_scores AS opponent_scores
ON team_scores.game_id = opponent_scores.game_id
AND team_scores.period_name = opponent_scores.period_name
AND team_scores.team != opponent_scores.team
INNER JOIN games AS g ON team_scores.game_id = g.game_id
LEFT JOIN {{ ref('team_maps') }} AS team_map ON team_scores.team = team_map.team_abbr
LEFT JOIN {{ ref('team_maps') }} AS opponent_map ON opponent_scores.team = opponent_map.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS t ON team_map.team_abbr = t.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS opp ON opponent_map.team_abbr = opp.team_abbr
LEFT JOIN {{ ref('dim_dates') }} AS d ON CAST(g.game_date AS DATE) = d.full_date
LEFT JOIN {{ ref('dim_seasons') }} AS s ON g.season_start_year = s.season_start_year
WHERE team_scores.points_scored IS NOT NULL
)
SELECT * FROM final
@@ -0,0 +1,54 @@
{{
config(
materialized='incremental',
schema='marts',
tags=["fact"],
unique_key='team_game_key'
)
}}
WITH team_performance AS (
SELECT * FROM {{ ref('int_team_performance') }}
),
game_details AS (
SELECT
game_id, game_date, season_start_year,
home_team, visitor_team, arena AS arena_name, arena_city
FROM {{ ref('int_games_enriched') }}
),
final AS (
SELECT
{{ dbt_utils.generate_surrogate_key(['tp.game_id', 'tp.team']) }} AS team_game_key,
t.team_key,
d.date_key,
s.season_key,
a.arena_key,
opp.team_key AS opponent_key,
tp.game_id,
(tp.team = gd.home_team) AS is_home_team,
tp.points,
tp.offensive_rating,
tp.defensive_rating,
tp.net_rating,
tp.pace,
tp.effective_fg_pct,
tp.turnover_rate,
tp.offensive_tier,
tp.defensive_tier,
CAST(gd.game_date AS DATE) AS game_date
FROM team_performance AS tp
LEFT JOIN game_details AS gd ON tp.game_id = gd.game_id
LEFT JOIN {{ ref('dim_teams') }} AS t ON tp.team = t.team_abbr
LEFT JOIN {{ ref('dim_teams') }} AS opp
ON CASE WHEN tp.team = gd.home_team THEN gd.visitor_team ELSE gd.home_team END = opp.team_abbr
LEFT JOIN {{ ref('dim_dates') }} AS d ON CAST(gd.game_date AS DATE) = d.full_date
LEFT JOIN {{ ref('dim_seasons') }} AS s ON gd.season_start_year = s.season_start_year
LEFT JOIN {{ ref('dim_arenas') }} AS a ON gd.arena_name = a.arena_name AND gd.arena_city = a.arena_city
{% if is_incremental() %}
WHERE gd.game_date >= (SELECT MAX(game_date) FROM {{ this }}) - INTERVAL '30 days'
{% endif %}
)
SELECT * FROM final
+129
View File
@@ -0,0 +1,129 @@
version: 2
sources:
- name: raw_nba
description: Raw NBA data in DuckDB
schema: main
tables:
- name: games
description: Core game information including scores, teams, and venues
columns:
- name: game_id
tests:
- unique
- not_null
- name: line_scores
description: Quarter-by-quarter scoring breakdown
columns:
- name: game_id
tests:
- not_null
- name: player_game_basic_stats
description: Basic player statistics for each game
columns:
- name: game_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: player_game_adv_stats
description: Advanced player statistics for each game
columns:
- name: game_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: team_game_basic_stats
description: Basic team statistics for each game
columns:
- name: game_id
tests:
- not_null
- name: team
tests:
- not_null
- name: team_game_adv_stats
description: Advanced team statistics for each game
columns:
- name: game_id
tests:
- not_null
- name: team
tests:
- not_null
- name: player_shot_charts
description: Individual shot-level data from NBA player shot charts
columns:
- name: id
tests:
- unique
- not_null
models:
- name: stg_games
description: Staged games data with cleaned fields and derived metrics
columns:
- name: game_id
tests:
- unique
- not_null
- name: stg_line_scores
description: Staged quarter-by-quarter scoring with momentum metrics
columns:
- name: game_id
tests:
- not_null
- name: stg_player_game_basic_stats
description: Staged player basic stats with calculated metrics
columns:
- name: game_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: stg_player_game_adv_stats
description: Staged player advanced stats with performance tiers
columns:
- name: game_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: stg_player_game_adv_stats_extended
description: Extended player advanced stats with dynamic per-season tiering
columns:
- name: game_id
tests:
- not_null
- name: player_id
tests:
- not_null
- name: stg_team_game_basic_stats
description: Staged team basic stats with four factors
columns:
- name: game_id
tests:
- not_null
- name: team
tests:
- not_null
- name: stg_team_game_adv_stats
description: Staged team advanced stats with play style indicators
columns:
- name: game_id
tests:
- not_null
- name: team
tests:
- not_null
+82
View File
@@ -0,0 +1,82 @@
{{
config(
materialized='view',
schema='staging'
)
}}
WITH overtime_games AS (
SELECT
game_id,
MAX(CASE WHEN overtime_points > 0 THEN 1 ELSE 0 END) AS had_overtime
FROM {{ ref('stg_line_scores') }}
GROUP BY 1
),
source_data AS (
SELECT * FROM {{ source('raw_nba', 'games') }}
WHERE deleted_at IS NULL
),
cleaned AS (
SELECT
g.game_id,
g.home_team,
g.visitor_team,
g.date AS game_date,
EXTRACT(YEAR FROM g.date) AS season_year,
CASE
WHEN EXTRACT(MONTH FROM g.date) >= 10 THEN EXTRACT(YEAR FROM g.date)
ELSE EXTRACT(YEAR FROM g.date) - 1
END AS season_start_year,
EXTRACT(MONTH FROM g.date) AS game_month,
EXTRACT(DAY FROM g.date) AS game_day,
strftime(g.date, '%A') AS game_day_of_week,
g.is_playoff,
g.start_time_et,
CASE
WHEN g.start_time_et LIKE '%7:%p%' OR g.start_time_et LIKE '%8:%p%' THEN 'Prime Time'
WHEN g.start_time_et LIKE '%12:%p%' OR g.start_time_et LIKE '%1:%p%'
OR g.start_time_et LIKE '%2:%p%' OR g.start_time_et LIKE '%3:%p%' THEN 'Afternoon'
ELSE 'Late Night'
END AS game_time_slot,
g.arena,
g.home_pts AS home_points,
g.visitor_pts AS visitor_points,
CASE
WHEN g.home_pts > g.visitor_pts THEN g.home_team
WHEN g.visitor_pts > g.home_pts THEN g.visitor_team
ELSE NULL
END AS winning_team,
CASE
WHEN g.home_pts < g.visitor_pts THEN g.home_team
WHEN g.visitor_pts < g.home_pts THEN g.visitor_team
ELSE NULL
END AS losing_team,
CASE
WHEN g.home_pts > g.visitor_pts THEN 'HOME'
WHEN g.visitor_pts > g.home_pts THEN 'AWAY'
ELSE NULL
END AS winner_location,
ABS(g.home_pts - g.visitor_pts) AS point_differential,
g.home_pts + g.visitor_pts AS total_points,
g.game_duration,
CASE
WHEN ot.had_overtime = 1 THEN TRUE
ELSE FALSE
END AS is_overtime,
g.box_score_url,
g.created_at,
g.updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM source_data AS g
LEFT JOIN overtime_games AS ot
ON g.game_id = ot.game_id
WHERE
g.game_id IS NOT NULL
AND g.date IS NOT NULL
AND g.home_team IS NOT NULL
AND g.visitor_team IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,51 @@
{{
config(
materialized='view',
schema='staging'
)
}}
WITH source_data AS (
SELECT *
FROM {{ source('raw_nba', 'line_scores') }}
WHERE deleted_at IS NULL
),
cleaned_and_transformed AS (
SELECT
game_id,
team,
COALESCE(q1, 0) AS q1_points,
COALESCE(q2, 0) AS q2_points,
COALESCE(q3, 0) AS q3_points,
COALESCE(q4, 0) AS q4_points,
COALESCE(ot1, 0) AS ot1_points,
COALESCE(ot2, 0) AS ot2_points,
COALESCE(ot3, 0) AS ot3_points,
total AS total_points,
COALESCE(q1, 0) + COALESCE(q2, 0) AS first_half_points,
COALESCE(q3, 0) + COALESCE(q4, 0) AS second_half_points,
COALESCE(q1, 0) + COALESCE(q2, 0) + COALESCE(q3, 0) + COALESCE(q4, 0) AS regulation_points,
COALESCE(ot1, 0) + COALESCE(ot2, 0) + COALESCE(ot3, 0) AS overtime_points,
COALESCE(ot1, 0) > 0 AS had_ot1,
COALESCE(ot2, 0) > 0 AS had_ot2,
COALESCE(ot3, 0) > 0 AS had_ot3,
COALESCE(q2, 0) - COALESCE(q1, 0) AS q2_momentum,
COALESCE(q3, 0) - COALESCE(q2, 0) AS q3_momentum,
COALESCE(q4, 0) - COALESCE(q3, 0) AS q4_momentum,
GREATEST(COALESCE(q1, 0), COALESCE(q2, 0), COALESCE(q3, 0), COALESCE(q4, 0)) AS max_quarter_score,
LEAST(COALESCE(q1, 0), COALESCE(q2, 0), COALESCE(q3, 0), COALESCE(q4, 0)) AS min_quarter_score,
CASE
WHEN GREATEST(COALESCE(q1, 0), COALESCE(q2, 0), COALESCE(q3, 0), COALESCE(q4, 0)) = COALESCE(q1, 0) THEN 'Q1'
WHEN GREATEST(COALESCE(q1, 0), COALESCE(q2, 0), COALESCE(q3, 0), COALESCE(q4, 0)) = COALESCE(q2, 0) THEN 'Q2'
WHEN GREATEST(COALESCE(q1, 0), COALESCE(q2, 0), COALESCE(q3, 0), COALESCE(q4, 0)) = COALESCE(q3, 0) THEN 'Q3'
ELSE 'Q4'
END AS best_quarter,
created_at,
updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM source_data
WHERE game_id IS NOT NULL AND team IS NOT NULL
)
SELECT * FROM cleaned_and_transformed
@@ -0,0 +1,106 @@
{{
config(
materialized='view',
schema='staging'
)
}}
WITH source_data AS (
SELECT * FROM {{ source('raw_nba', 'player_game_adv_stats') }}
WHERE deleted_at IS NULL
),
with_minutes AS (
SELECT
*,
CASE
WHEN mp IS NOT NULL AND mp != '' AND mp LIKE '%:%' THEN
CAST(SPLIT_PART(mp, ':', 1) AS DECIMAL) +
(CAST(SPLIT_PART(mp, ':', 2) AS DECIMAL) / 60.0)
WHEN mp IS NOT NULL AND mp != '' THEN CAST(mp AS DECIMAL)
ELSE 0
END AS minutes_played_calc
FROM source_data
),
cleaned AS (
SELECT
game_id,
player_id,
team,
player_name,
mp AS minutes_played_str,
minutes_played_calc AS minutes_played,
COALESCE(ts_percent, 0) AS true_shooting_pct,
COALESCE(efg_percent, 0) AS effective_fg_pct,
COALESCE(three_p_ar, 0) AS three_point_attempt_rate,
COALESCE(f_tr, 0) AS free_throw_rate,
COALESCE(orb_percent, 0) AS offensive_rebound_pct,
COALESCE(drb_percent, 0) AS defensive_rebound_pct,
COALESCE(trb_percent, 0) AS total_rebound_pct,
COALESCE(ast_percent, 0) AS assist_pct,
COALESCE(stl_percent, 0) AS steal_pct,
COALESCE(blk_percent, 0) AS block_pct,
COALESCE(tov_percent, 0) AS turnover_pct,
COALESCE(usg_percent, 0) AS usage_pct,
COALESCE(o_rtg, 0) AS offensive_rating,
COALESCE(d_rtg, 0) AS defensive_rating,
COALESCE(bpm, 0) AS box_plus_minus,
COALESCE(o_rtg, 0) - COALESCE(d_rtg, 0) AS net_rating,
CASE
WHEN minutes_played_calc < 5 THEN 'Insufficient Minutes'
WHEN COALESCE(ts_percent, 0) >= 0.60 THEN 'Elite'
WHEN COALESCE(ts_percent, 0) >= 0.55 THEN 'Good'
WHEN COALESCE(ts_percent, 0) >= 0.50 THEN 'Average'
ELSE 'Below Average'
END AS shooting_efficiency_tier,
CASE
WHEN minutes_played_calc < 5 THEN 'Garbage Time'
WHEN minutes_played_calc < 15 THEN 'Limited Minutes'
WHEN (minutes_played_calc >= 30 AND COALESCE(usg_percent, 0) >= 22) OR
(minutes_played_calc >= 25 AND COALESCE(usg_percent, 0) >= 25) THEN 'Primary Option'
WHEN minutes_played_calc >= 20 AND COALESCE(usg_percent, 0) >= 20 THEN 'Secondary Option'
WHEN minutes_played_calc >= 15 THEN 'Role Player'
ELSE 'Limited Minutes'
END AS usage_tier,
CASE
WHEN minutes_played_calc < 5 THEN 'Insufficient Minutes'
WHEN COALESCE(bpm, 0) >= 10 THEN 'Elite Impact'
WHEN COALESCE(bpm, 0) >= 5 THEN 'High Impact'
WHEN COALESCE(bpm, 0) >= 0 THEN 'Positive Impact'
WHEN COALESCE(bpm, 0) >= -5 THEN 'Negative Impact'
ELSE 'Very Negative Impact'
END AS impact_tier,
CASE
WHEN minutes_played_calc >= 32 THEN 'Starter/Key Player'
WHEN minutes_played_calc >= 20 THEN 'Rotation Player'
WHEN minutes_played_calc >= 10 THEN 'Bench Player'
WHEN minutes_played_calc >= 5 THEN 'Deep Bench'
ELSE 'Garbage Time'
END AS minutes_based_role,
CASE
WHEN minutes_played_calc >= 15
AND COALESCE(ast_percent, 0) >= 20
AND COALESCE(trb_percent, 0) >= 15 THEN TRUE
ELSE FALSE
END AS is_versatile,
CASE
WHEN minutes_played_calc >= 15
AND (COALESCE(stl_percent, 0) >= 2.5 OR COALESCE(blk_percent, 0) >= 4)
AND COALESCE(d_rtg, 0) < 105 THEN TRUE
ELSE FALSE
END AS is_defensive_specialist,
CASE
WHEN minutes_played_calc >= 15
AND COALESCE(three_p_ar, 0) >= 0.4
AND COALESCE(d_rtg, 0) < 110 THEN TRUE
ELSE FALSE
END AS is_three_and_d,
created_at,
updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM with_minutes
WHERE game_id IS NOT NULL AND player_id IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,169 @@
{{
config(
materialized='table',
schema='staging',
tags=["staging"]
)
}}
{% set minutes_insufficient = 5 %}
WITH source_data AS (
SELECT * FROM {{ source('raw_nba', 'player_game_adv_stats') }}
WHERE deleted_at IS NULL
),
games AS (
SELECT game_id, season_start_year
FROM {{ ref('stg_games') }}
),
with_minutes AS (
SELECT
sd.*,
g.season_start_year,
CASE
WHEN sd.mp IS NOT NULL AND sd.mp != '' AND sd.mp LIKE '%:%' THEN
CAST(SPLIT_PART(sd.mp, ':', 1) AS DECIMAL)
+ (CAST(SPLIT_PART(sd.mp, ':', 2) AS DECIMAL) / 60.0)
WHEN sd.mp IS NOT NULL AND sd.mp != '' AND regexp_matches(sd.mp, '^[0-9\.]+$') THEN
CAST(sd.mp AS DECIMAL)
ELSE 0
END AS minutes_played
FROM source_data AS sd
LEFT JOIN games AS g ON sd.game_id = g.game_id
),
season_thresholds AS (
SELECT * FROM {{ ref('stg_season_thresholds') }}
),
cleaned AS (
SELECT
wm.game_id,
wm.player_id,
wm.team,
wm.player_name,
wm.mp AS minutes_played_str,
wm.minutes_played,
COALESCE(wm.ts_percent, 0) AS true_shooting_pct,
COALESCE(wm.efg_percent, 0) AS effective_fg_pct,
COALESCE(wm.three_p_ar, 0) AS three_point_attempt_rate,
COALESCE(wm.f_tr, 0) AS free_throw_rate,
COALESCE(wm.orb_percent, 0) AS offensive_rebound_pct,
COALESCE(wm.drb_percent, 0) AS defensive_rebound_pct,
COALESCE(wm.trb_percent, 0) AS total_rebound_pct,
COALESCE(wm.ast_percent, 0) AS assist_pct,
COALESCE(wm.stl_percent, 0) AS steal_pct,
COALESCE(wm.blk_percent, 0) AS block_pct,
COALESCE(wm.tov_percent, 0) AS turnover_pct,
COALESCE(wm.usg_percent, 0) AS usage_pct,
COALESCE(wm.o_rtg, 0) AS offensive_rating,
COALESCE(wm.d_rtg, 0) AS defensive_rating,
COALESCE(wm.bpm, 0) AS box_plus_minus,
COALESCE(wm.o_rtg, 0) - COALESCE(wm.d_rtg, 0) AS net_rating,
-- Minutes-based role
CASE
WHEN wm.minutes_played >= t.min_p95 THEN 'Elite Minutes (Top 5%)'
WHEN wm.minutes_played >= t.min_p90 THEN 'Elite Minutes (Top 10%)'
WHEN wm.minutes_played >= t.min_q3 THEN 'Starter'
WHEN wm.minutes_played >= t.min_median THEN 'Key Rotation'
WHEN wm.minutes_played >= t.min_q1 THEN 'Regular Rotation'
WHEN wm.minutes_played >= t.min_p5 THEN 'Deep Bench'
WHEN wm.minutes_played >= {{ minutes_insufficient }} THEN 'Garbage Time'
ELSE 'Insufficient Minutes'
END AS minutes_based_role,
CASE
WHEN wm.minutes_played < {{ minutes_insufficient }} THEN 'Insufficient'
WHEN wm.minutes_played < t.min_q1 THEN 'Q1'
WHEN wm.minutes_played < t.min_median THEN 'Q2'
WHEN wm.minutes_played < t.min_q3 THEN 'Q3'
ELSE 'Q4'
END AS minutes_quartile,
-- Usage Tier
CASE
WHEN wm.minutes_played < {{ minutes_insufficient }} THEN 'Insufficient Minutes'
WHEN COALESCE(wm.usg_percent, 0) >= t.usg_p90
AND wm.minutes_played >= t.min_q1 THEN 'Heliocentric Option'
WHEN COALESCE(wm.usg_percent, 0) >= t.usg_q3
AND wm.minutes_played >= t.min_q3 THEN 'Primary Option'
WHEN COALESCE(wm.usg_percent, 0) >= t.usg_median
AND wm.minutes_played >= t.min_median THEN 'Secondary Option'
WHEN COALESCE(wm.usg_percent, 0) >= t.usg_q1
AND wm.minutes_played >= t.min_q1 THEN 'Role Player'
WHEN wm.minutes_played >= t.min_q1
AND COALESCE(wm.usg_percent, 0) >= t.usg_p5 THEN 'Connector/Specialist'
WHEN wm.minutes_played >= t.min_p5 THEN 'Low Usage Player'
ELSE 'Limited Role'
END AS usage_tier,
CASE
WHEN COALESCE(wm.usg_percent, 0) < t.usg_q1 THEN 'Q1'
WHEN COALESCE(wm.usg_percent, 0) < t.usg_median THEN 'Q2'
WHEN COALESCE(wm.usg_percent, 0) < t.usg_q3 THEN 'Q3'
ELSE 'Q4'
END AS usage_quartile,
-- Impact Tier
CASE
WHEN wm.minutes_played < {{ minutes_insufficient }} THEN 'Insufficient Minutes'
WHEN COALESCE(wm.bpm, 0) >= t.bpm_p90 THEN 'Elite Impact'
WHEN COALESCE(wm.bpm, 0) >= t.bpm_q3 THEN 'High Impact'
WHEN COALESCE(wm.bpm, 0) >= t.bpm_median THEN 'Positive Impact'
WHEN COALESCE(wm.bpm, 0) >= t.bpm_q1 THEN 'Neutral Impact'
WHEN COALESCE(wm.bpm, 0) >= t.bpm_p5 THEN 'Negative Impact'
ELSE 'Very Negative Impact'
END AS impact_tier,
CASE
WHEN COALESCE(wm.bpm, 0) < t.bpm_q1 THEN 'Q1'
WHEN COALESCE(wm.bpm, 0) < t.bpm_median THEN 'Q2'
WHEN COALESCE(wm.bpm, 0) < t.bpm_q3 THEN 'Q3'
ELSE 'Q4'
END AS bpm_quartile,
-- Shooting efficiency tier
CASE
WHEN wm.minutes_played < {{ minutes_insufficient }} THEN 'Insufficient Minutes'
WHEN COALESCE(wm.ts_percent, 0) >= t.ts_q3 THEN 'Elite'
WHEN COALESCE(wm.ts_percent, 0) >= t.ts_median THEN 'Good'
WHEN COALESCE(wm.ts_percent, 0) >= t.ts_q1 THEN 'Average'
ELSE 'Below Average'
END AS shooting_efficiency_tier,
-- Boolean flags
wm.minutes_played >= t.min_p90 AS is_extreme_minutes,
wm.minutes_played >= t.min_q3 AS is_starter_minutes,
wm.minutes_played >= t.min_q1 AS is_meaningful_minutes,
wm.minutes_played >= t.min_q1
AND COALESCE(wm.ast_percent, 0) >= 20
AND COALESCE(wm.trb_percent, 0) >= 13 AS is_versatile,
wm.minutes_played >= t.min_q1
AND (COALESCE(wm.stl_percent, 0) >= 2.5 OR COALESCE(wm.blk_percent, 0) >= 3)
AND COALESCE(wm.d_rtg, 0) <= 110 AS is_defensive_specialist,
wm.minutes_played >= t.min_q1
AND COALESCE(wm.three_p_ar, 0) >= 0.4
AND COALESCE(wm.d_rtg, 0) <= 110 AS is_three_and_d,
COALESCE(wm.usg_percent, 0) >= t.usg_p90 AS is_extreme_usage,
COALESCE(wm.usg_percent, 0) >= t.usg_q3 AS is_high_usage,
COALESCE(wm.bpm, 0) >= t.bpm_p90 AS is_elite_impact,
COALESCE(wm.bpm, 0) >= t.bpm_q3 AS is_positive_impact,
wm.created_at,
wm.updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM with_minutes AS wm
LEFT JOIN season_thresholds AS t
ON wm.season_start_year = t.season_start_year
WHERE wm.game_id IS NOT NULL AND wm.player_id IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,93 @@
{{
config(
materialized='view',
schema='staging'
)
}}
WITH source_data AS (
SELECT * FROM {{ source('raw_nba', 'player_game_basic_stats') }}
WHERE deleted_at IS NULL
),
cleaned AS (
SELECT
game_id,
player_id,
team,
player_name,
COALESCE(status, 'Unknown') AS player_status,
CASE WHEN status = 'Played' THEN TRUE ELSE FALSE END AS did_play,
mp AS minutes_played_str,
CAST(
CASE
WHEN mp IS NOT NULL AND mp != '' AND mp LIKE '%:%' THEN
CAST(SPLIT_PART(mp, ':', 1) AS DECIMAL) +
(CAST(SPLIT_PART(mp, ':', 2) AS DECIMAL) / 60.0)
WHEN mp IS NOT NULL AND mp != '' THEN CAST(mp AS DECIMAL)
ELSE 0
END AS DECIMAL(10, 2)) AS minutes_played,
COALESCE(fg, 0) AS field_goals_made,
COALESCE(fga, 0) AS field_goals_attempted,
COALESCE(fg_percent, 0) AS field_goal_pct,
COALESCE(three_p, 0) AS three_pointers_made,
COALESCE(three_pa, 0) AS three_pointers_attempted,
COALESCE(three_p_percent, 0) AS three_point_pct,
COALESCE(ft, 0) AS free_throws_made,
COALESCE(fta, 0) AS free_throws_attempted,
COALESCE(ft_percent, 0) AS free_throw_pct,
COALESCE(orb, 0) AS offensive_rebounds,
COALESCE(drb, 0) AS defensive_rebounds,
COALESCE(trb, 0) AS total_rebounds,
COALESCE(ast, 0) AS assists,
COALESCE(stl, 0) AS steals,
COALESCE(blk, 0) AS blocks,
COALESCE(tov, 0) AS turnovers,
COALESCE(pf, 0) AS personal_fouls,
COALESCE(pts, 0) AS points,
COALESCE(gm_sc, 0) AS game_score,
COALESCE(plus_minus, 0) AS plus_minus,
COALESCE(fg, 0) - COALESCE(three_p, 0) AS two_pointers_made,
COALESCE(fga, 0) - COALESCE(three_pa, 0) AS two_pointers_attempted,
CASE
WHEN (COALESCE(fga, 0) - COALESCE(three_pa, 0)) > 0 THEN
CAST(COALESCE(fg, 0) - COALESCE(three_p, 0) AS DECIMAL) /
CAST(COALESCE(fga, 0) - COALESCE(three_pa, 0) AS DECIMAL)
ELSE 0
END AS two_point_pct,
CASE
WHEN COALESCE(fga, 0) > 0 THEN
CAST(COALESCE(pts, 0) AS DECIMAL) / CAST(COALESCE(fga, 0) AS DECIMAL)
ELSE 0
END AS points_per_shot,
CASE
WHEN (
(CASE WHEN COALESCE(pts, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(trb, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(ast, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(stl, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(blk, 0) >= 10 THEN 1 ELSE 0 END)
) >= 2 THEN TRUE ELSE FALSE
END AS is_double_double,
CASE
WHEN (
(CASE WHEN COALESCE(pts, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(trb, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(ast, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(stl, 0) >= 10 THEN 1 ELSE 0 END) +
(CASE WHEN COALESCE(blk, 0) >= 10 THEN 1 ELSE 0 END)
) >= 3 THEN TRUE ELSE FALSE
END AS is_triple_double,
CASE
WHEN mp IS NOT NULL AND mp LIKE '%:%' AND
CAST(SPLIT_PART(mp, ':', 1) AS DECIMAL) >= 20 THEN TRUE
ELSE FALSE
END AS likely_starter,
created_at,
updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM source_data
WHERE game_id IS NOT NULL AND player_id IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,105 @@
{{
config(
materialized='view',
schema='staging'
)
}}
WITH source_data AS (
SELECT *
FROM {{ source('raw_nba', 'player_shot_charts') }}
WHERE deleted_at IS NULL
),
cleaned AS (
SELECT
id AS shot_id,
player_id,
season AS season_year,
CASE
WHEN date IS NOT NULL AND date != '' THEN
strptime(TRIM(date), '%b %d,%Y')::DATE
ELSE NULL
END AS game_date,
date AS game_date_raw,
CASE
WHEN date IS NOT NULL AND date != '' THEN
CASE
WHEN EXTRACT(MONTH FROM strptime(TRIM(date), '%b %d,%Y')::DATE) >= 10
THEN EXTRACT(YEAR FROM strptime(TRIM(date), '%b %d,%Y')::DATE)::INT
ELSE EXTRACT(YEAR FROM strptime(TRIM(date), '%b %d,%Y')::DATE)::INT - 1
END
ELSE NULL
END AS season_start_year,
qtr AS quarter_raw,
CASE
WHEN qtr LIKE '1st%' THEN 1
WHEN qtr LIKE '2nd%' THEN 2
WHEN qtr LIKE '3rd%' THEN 3
WHEN qtr LIKE '4th%' THEN 4
WHEN qtr ILIKE '%OT%' THEN 5
ELSE NULL
END AS quarter_number,
CASE WHEN qtr ILIKE '%OT%' THEN TRUE ELSE FALSE END AS is_overtime_shot,
time_remaining AS time_remaining_raw,
CASE
WHEN time_remaining IS NOT NULL AND time_remaining LIKE '%:%' THEN
CAST(SPLIT_PART(time_remaining, ':', 1) AS INT) * 60
+ CAST(SPLIT_PART(time_remaining, ':', 2) AS INT)
ELSE NULL
END AS seconds_remaining_in_quarter,
top AS shot_y_coordinate,
"left" AS shot_x_coordinate,
COALESCE(result, FALSE) AS is_made,
CASE WHEN COALESCE(result, FALSE) = TRUE THEN 1 ELSE 0 END AS shot_made_flag,
CASE WHEN COALESCE(result, FALSE) = FALSE THEN 1 ELSE 0 END AS shot_missed_flag,
shot_type AS shot_type_raw,
CASE
WHEN shot_type = '3-pointer' THEN 3
WHEN shot_type = '2-pointer' THEN 2
ELSE NULL
END AS shot_point_value,
CASE WHEN shot_type = '3-pointer' THEN TRUE ELSE FALSE END AS is_three_pointer,
FALSE AS is_free_throw,
COALESCE(distance_ft, 0) AS distance_ft,
CASE
WHEN COALESCE(distance_ft, 0) <= 3 THEN 'At Rim (0-3 ft)'
WHEN COALESCE(distance_ft, 0) <= 10 THEN 'Short Range (4-10 ft)'
WHEN COALESCE(distance_ft, 0) <= 16 THEN 'Mid Range (11-16 ft)'
WHEN COALESCE(distance_ft, 0) <= 23 THEN 'Long Mid Range (17-23 ft)'
WHEN COALESCE(distance_ft, 0) <= 27 THEN 'Three Point (24-27 ft)'
ELSE 'Deep Three (28+ ft)'
END AS shot_distance_zone,
COALESCE(lead, FALSE) AS team_had_lead,
COALESCE(team_score, 0) AS team_score_at_shot,
COALESCE(opponent_team_score, 0) AS opponent_score_at_shot,
COALESCE(team_score, 0) - COALESCE(opponent_team_score, 0) AS score_margin_at_shot,
CASE
WHEN qtr LIKE '4th%'
AND time_remaining IS NOT NULL
AND time_remaining LIKE '%:%'
AND (CAST(SPLIT_PART(time_remaining, ':', 1) AS INT) * 60
+ CAST(SPLIT_PART(time_remaining, ':', 2) AS INT)) <= 300
AND ABS(COALESCE(team_score, 0) - COALESCE(opponent_team_score, 0)) <= 5
THEN TRUE
ELSE FALSE
END AS is_clutch_shot,
team AS team_abbr_raw,
opponent AS opponent_abbr_raw,
CASE
WHEN COALESCE(result, FALSE) = TRUE THEN
CASE
WHEN shot_type = '3-pointer' THEN 3
WHEN shot_type = '2-pointer' THEN 2
ELSE 0
END
ELSE 0
END AS points_generated,
created_at,
updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM source_data
WHERE id IS NOT NULL AND player_id IS NOT NULL AND date IS NOT NULL AND team IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,59 @@
{{
config(
materialized='table',
schema='staging',
tags=["staging"]
)
}}
WITH player_stats AS (
SELECT
g.season_start_year,
CASE
WHEN s.mp IS NOT NULL AND s.mp != '' AND s.mp LIKE '%:%' THEN
CAST(SPLIT_PART(s.mp, ':', 1) AS DECIMAL)
+ (CAST(SPLIT_PART(s.mp, ':', 2) AS DECIMAL) / 60.0)
WHEN s.mp IS NOT NULL AND s.mp != '' AND regexp_matches(s.mp, '^[0-9\.]+$') THEN
CAST(s.mp AS DECIMAL)
ELSE 0
END AS minutes_played,
COALESCE(s.usg_percent, 0) AS usage_pct,
COALESCE(s.bpm, 0) AS bpm,
COALESCE(s.ts_percent, 0) AS ts_pct
FROM {{ source('raw_nba', 'player_game_adv_stats') }} AS s
INNER JOIN {{ ref('stg_games') }} AS g
ON s.game_id = g.game_id
WHERE s.deleted_at IS NULL
AND s.game_id IS NOT NULL
AND s.player_id IS NOT NULL
)
SELECT
season_start_year,
PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY minutes_played) AS min_p5,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY minutes_played) AS min_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY minutes_played) AS min_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY minutes_played) AS min_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY minutes_played) AS min_p90,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY minutes_played) AS min_p95,
PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY usage_pct) AS usg_p5,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY usage_pct) AS usg_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY usage_pct) AS usg_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY usage_pct) AS usg_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY usage_pct) AS usg_p90,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY usage_pct) AS usg_p95,
PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY bpm) AS bpm_p5,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY bpm) AS bpm_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY bpm) AS bpm_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY bpm) AS bpm_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY bpm) AS bpm_p90,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY bpm) AS bpm_p95,
PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY ts_pct) AS ts_p5,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY ts_pct) AS ts_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY ts_pct) AS ts_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY ts_pct) AS ts_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY ts_pct) AS ts_p90,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY ts_pct) AS ts_p95
FROM player_stats
GROUP BY season_start_year
ORDER BY season_start_year
@@ -0,0 +1,102 @@
{{
config(
materialized='table',
schema='staging',
tags=["staging"]
)
}}
WITH source_data AS (
SELECT * FROM {{ source('raw_nba', 'team_game_adv_stats') }}
WHERE deleted_at IS NULL
),
games AS (
SELECT game_id, season_start_year
FROM {{ ref('stg_games') }}
),
team_season_thresholds AS (
SELECT * FROM {{ ref('stg_team_season_thresholds') }}
),
cleaned AS (
SELECT
sd.game_id,
sd.team,
COALESCE(sd.mp, 240) AS minutes_played,
COALESCE(sd.ts_percent, 0) AS true_shooting_pct,
COALESCE(sd.efg_percent, 0) AS effective_fg_pct,
COALESCE(sd.three_p_ar, 0) AS three_point_attempt_rate,
COALESCE(sd.f_tr, 0) AS free_throw_rate,
COALESCE(sd.orb_percent, 0) AS offensive_rebound_pct,
COALESCE(sd.drb_percent, 0) AS defensive_rebound_pct,
COALESCE(sd.trb_percent, 0) AS total_rebound_pct,
COALESCE(sd.ast_percent, 0) AS assist_pct,
COALESCE(sd.stl_percent, 0) AS steal_pct,
COALESCE(sd.blk_percent, 0) AS block_pct,
COALESCE(sd.tov_percent, 0) AS turnover_pct,
COALESCE(sd.usg_percent, 100) AS usage_pct,
COALESCE(sd.o_rtg, 0) AS offensive_rating,
COALESCE(sd.d_rtg, 0) AS defensive_rating,
COALESCE(sd.o_rtg, 0) - COALESCE(sd.d_rtg, 0) AS net_rating,
CASE
WHEN COALESCE(sd.o_rtg, 0) >= t.ortg_p90 THEN 'Elite Offense'
WHEN COALESCE(sd.o_rtg, 0) >= t.ortg_q3 THEN 'Above Average Offense'
WHEN COALESCE(sd.o_rtg, 0) >= t.ortg_q1 THEN 'Average Offense'
WHEN COALESCE(sd.o_rtg, 0) >= t.ortg_p10 THEN 'Below Average Offense'
ELSE 'Poor Offense'
END AS offensive_tier,
CASE
WHEN COALESCE(sd.d_rtg, 0) <= t.drtg_p10 THEN 'Elite Defense'
WHEN COALESCE(sd.d_rtg, 0) <= t.drtg_q1 THEN 'Above Average Defense'
WHEN COALESCE(sd.d_rtg, 0) <= t.drtg_q3 THEN 'Average Defense'
WHEN COALESCE(sd.d_rtg, 0) <= t.drtg_p90 THEN 'Below Average Defense'
ELSE 'Poor Defense'
END AS defensive_tier,
CASE
WHEN COALESCE(sd.three_p_ar, 0) >= t.tpar_p90 THEN 'Three Point Heavy'
WHEN COALESCE(sd.three_p_ar, 0) >= t.tpar_q3 THEN 'Three Point Leaning'
WHEN COALESCE(sd.three_p_ar, 0) >= t.tpar_q1 THEN 'Balanced'
WHEN COALESCE(sd.three_p_ar, 0) >= t.tpar_p10 THEN 'Inside Leaning'
ELSE 'Inside Focused'
END AS shot_selection_style,
CASE
WHEN COALESCE(sd.ast_percent, 0) >= t.ast_p90 THEN 'Elite Ball Movement'
WHEN COALESCE(sd.ast_percent, 0) >= t.ast_q3 THEN 'High Ball Movement'
WHEN COALESCE(sd.ast_percent, 0) >= t.ast_q1 THEN 'Average Ball Movement'
WHEN COALESCE(sd.ast_percent, 0) >= t.ast_p10 THEN 'Low Ball Movement'
ELSE 'Isolation Heavy'
END AS ball_movement_style,
CASE
WHEN COALESCE(sd.tov_percent, 0) <= t.tov_p10 THEN 'Elite Ball Security'
WHEN COALESCE(sd.tov_percent, 0) <= t.tov_q1 THEN 'Above Average Ball Security'
WHEN COALESCE(sd.tov_percent, 0) <= t.tov_q3 THEN 'Average Ball Security'
WHEN COALESCE(sd.tov_percent, 0) <= t.tov_p90 THEN 'Below Average Ball Security'
ELSE 'Poor Ball Security'
END AS ball_security_tier,
CASE
WHEN (COALESCE(sd.stl_percent, 0) + COALESCE(sd.blk_percent, 0)) >= t.stl_blk_p90 THEN 'Elite Defensive Activity'
WHEN (COALESCE(sd.stl_percent, 0) + COALESCE(sd.blk_percent, 0)) >= t.stl_blk_q3 THEN 'High Defensive Activity'
WHEN (COALESCE(sd.stl_percent, 0) + COALESCE(sd.blk_percent, 0)) >= t.stl_blk_q1 THEN 'Average Defensive Activity'
WHEN (COALESCE(sd.stl_percent, 0) + COALESCE(sd.blk_percent, 0)) >= t.stl_blk_p10 THEN 'Low Defensive Activity'
ELSE 'Passive Defense'
END AS defensive_activity,
sd.created_at,
sd.updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM source_data AS sd
LEFT JOIN games AS g ON sd.game_id = g.game_id
LEFT JOIN team_season_thresholds AS t ON g.season_start_year = t.season_start_year
WHERE sd.game_id IS NOT NULL AND sd.team IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,92 @@
{{
config(
materialized='view',
schema='staging'
)
}}
WITH source_data AS (
SELECT * FROM {{ source('raw_nba', 'team_game_basic_stats') }}
WHERE deleted_at IS NULL
),
cleaned AS (
SELECT
game_id,
team,
COALESCE(mp, 240) AS minutes_played,
COALESCE(fg, 0) AS field_goals_made,
COALESCE(fga, 0) AS field_goals_attempted,
COALESCE(fg_percent, 0) AS field_goal_pct,
COALESCE(three_p, 0) AS three_pointers_made,
COALESCE(three_pa, 0) AS three_pointers_attempted,
COALESCE(three_p_percent, 0) AS three_point_pct,
COALESCE(ft, 0) AS free_throws_made,
COALESCE(fta, 0) AS free_throws_attempted,
COALESCE(ft_percent, 0) AS free_throw_pct,
COALESCE(orb, 0) AS offensive_rebounds,
COALESCE(drb, 0) AS defensive_rebounds,
COALESCE(trb, 0) AS total_rebounds,
COALESCE(ast, 0) AS assists,
COALESCE(stl, 0) AS steals,
COALESCE(blk, 0) AS blocks,
COALESCE(tov, 0) AS turnovers,
COALESCE(pf, 0) AS personal_fouls,
COALESCE(pts, 0) AS points,
COALESCE(fg, 0) - COALESCE(three_p, 0) AS two_pointers_made,
COALESCE(fga, 0) - COALESCE(three_pa, 0) AS two_pointers_attempted,
CASE
WHEN (COALESCE(fga, 0) - COALESCE(three_pa, 0)) > 0 THEN
CAST(COALESCE(fg, 0) - COALESCE(three_p, 0) AS DECIMAL) /
CAST(COALESCE(fga, 0) - COALESCE(three_pa, 0) AS DECIMAL)
ELSE 0
END AS two_point_pct,
COALESCE(fga, 0) + 0.44 * COALESCE(fta, 0) - COALESCE(orb, 0) + COALESCE(tov, 0) AS possessions_estimate,
CASE
WHEN COALESCE(mp, 240) > 0 THEN
(COALESCE(fga, 0) + 0.44 * COALESCE(fta, 0) - COALESCE(orb, 0) + COALESCE(tov, 0)) * 48.0 / (COALESCE(mp, 240) / 5.0)
ELSE 0
END AS pace,
CASE
WHEN COALESCE(fga, 0) > 0 THEN
CAST(COALESCE(fg, 0) + 0.5 * COALESCE(three_p, 0) AS DECIMAL) / CAST(COALESCE(fga, 0) AS DECIMAL)
ELSE 0
END AS effective_fg_pct,
CASE
WHEN (COALESCE(fga, 0) + 0.44 * COALESCE(fta, 0) + COALESCE(tov, 0)) > 0 THEN
CAST(COALESCE(tov, 0) AS DECIMAL) / CAST(COALESCE(fga, 0) + 0.44 * COALESCE(fta, 0) + COALESCE(tov, 0) AS DECIMAL)
ELSE 0
END AS turnover_rate,
CASE
WHEN (COALESCE(orb, 0) + COALESCE(drb, 0)) > 0 THEN
CAST(COALESCE(orb, 0) AS DECIMAL) / CAST(COALESCE(orb, 0) + COALESCE(drb, 0) AS DECIMAL)
ELSE 0
END AS offensive_rebound_rate,
CASE
WHEN COALESCE(fga, 0) > 0 THEN
CAST(COALESCE(fta, 0) AS DECIMAL) / CAST(COALESCE(fga, 0) AS DECIMAL)
ELSE 0
END AS free_throw_rate,
CASE
WHEN COALESCE(tov, 0) > 0 THEN
CAST(COALESCE(ast, 0) AS DECIMAL) / CAST(COALESCE(tov, 0) AS DECIMAL)
ELSE 0
END AS ast_to_tov_ratio,
CASE
WHEN (COALESCE(fga, 0) + 0.44 * COALESCE(fta, 0) - COALESCE(orb, 0) + COALESCE(tov, 0)) > 0 THEN
CAST(COALESCE(pts, 0) AS DECIMAL) / CAST(COALESCE(fga, 0) + 0.44 * COALESCE(fta, 0) - COALESCE(orb, 0) + COALESCE(tov, 0) AS DECIMAL)
ELSE 0
END AS points_per_possession,
CASE
WHEN COALESCE(fga, 0) > 0 THEN
CAST(COALESCE(three_pa, 0) AS DECIMAL) / CAST(COALESCE(fga, 0) AS DECIMAL)
ELSE 0
END AS three_point_rate,
created_at,
updated_at,
CURRENT_TIMESTAMP AS dbt_loaded_at
FROM source_data
WHERE game_id IS NOT NULL AND team IS NOT NULL
)
SELECT * FROM cleaned
@@ -0,0 +1,84 @@
{{
config(
materialized='table',
schema='staging',
tags=["staging"]
)
}}
WITH team_stats AS (
SELECT
g.season_start_year,
COALESCE(s.o_rtg, 0) AS offensive_rating,
COALESCE(s.d_rtg, 0) AS defensive_rating,
COALESCE(s.three_p_ar, 0) AS three_point_attempt_rate,
COALESCE(s.ast_percent, 0) AS assist_pct,
COALESCE(s.orb_percent, 0) AS offensive_rebound_pct,
COALESCE(s.drb_percent, 0) AS defensive_rebound_pct,
COALESCE(s.tov_percent, 0) AS turnover_pct,
COALESCE(s.stl_percent, 0) + COALESCE(s.blk_percent, 0) AS stl_blk_combined,
COALESCE(s.ts_percent, 0) AS true_shooting_pct,
COALESCE(s.f_tr, 0) AS free_throw_rate
FROM {{ source('raw_nba', 'team_game_adv_stats') }} AS s
INNER JOIN {{ ref('stg_games') }} AS g
ON s.game_id = g.game_id
WHERE s.deleted_at IS NULL
AND s.game_id IS NOT NULL
AND s.team IS NOT NULL
)
SELECT
season_start_year,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY offensive_rating) AS ortg_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY offensive_rating) AS ortg_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY offensive_rating) AS ortg_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY offensive_rating) AS ortg_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY offensive_rating) AS ortg_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY defensive_rating) AS drtg_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY defensive_rating) AS drtg_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY defensive_rating) AS drtg_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY defensive_rating) AS drtg_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY defensive_rating) AS drtg_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY three_point_attempt_rate) AS tpar_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY three_point_attempt_rate) AS tpar_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY three_point_attempt_rate) AS tpar_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY three_point_attempt_rate) AS tpar_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY three_point_attempt_rate) AS tpar_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY assist_pct) AS ast_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY assist_pct) AS ast_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY assist_pct) AS ast_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY assist_pct) AS ast_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY assist_pct) AS ast_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY offensive_rebound_pct) AS orb_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY offensive_rebound_pct) AS orb_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY offensive_rebound_pct) AS orb_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY offensive_rebound_pct) AS orb_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY offensive_rebound_pct) AS orb_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY defensive_rebound_pct) AS drb_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY defensive_rebound_pct) AS drb_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY defensive_rebound_pct) AS drb_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY defensive_rebound_pct) AS drb_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY defensive_rebound_pct) AS drb_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY turnover_pct) AS tov_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY turnover_pct) AS tov_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY turnover_pct) AS tov_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY turnover_pct) AS tov_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY turnover_pct) AS tov_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY stl_blk_combined) AS stl_blk_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY stl_blk_combined) AS stl_blk_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY stl_blk_combined) AS stl_blk_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY stl_blk_combined) AS stl_blk_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY stl_blk_combined) AS stl_blk_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY true_shooting_pct) AS ts_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY true_shooting_pct) AS ts_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY true_shooting_pct) AS ts_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY true_shooting_pct) AS ts_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY true_shooting_pct) AS ts_p90,
PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY free_throw_rate) AS ftr_p10,
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY free_throw_rate) AS ftr_q1,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY free_throw_rate) AS ftr_median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY free_throw_rate) AS ftr_q3,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY free_throw_rate) AS ftr_p90
FROM team_stats
GROUP BY season_start_year
ORDER BY season_start_year
+5
View File
@@ -0,0 +1,5 @@
packages:
- name: dbt_utils
package: dbt-labs/dbt_utils
version: 1.3.1
sha1_hash: 1b75844a5c14558fe9ae17335b3632c080d6784c
+3
View File
@@ -0,0 +1,3 @@
packages:
- package: dbt-labs/dbt_utils
version: 1.3.1
+6
View File
@@ -0,0 +1,6 @@
dbt_nba:
target: dev
outputs:
dev:
type: duckdb
path: "{{ env_var('DBT_DUCKDB_PATH', 'data/DB/dbt_nba.duckdb') }}"
@@ -0,0 +1 @@
{"anonymousId":"8aa41dce-bd0b-40af-96a3-de949fffe712","traits":{"projectCreated":"2026-05-21T05:40:13.247Z"}}
@@ -0,0 +1,4 @@
{
"version": "1.0",
"customFormats": []
}
+10
View File
@@ -0,0 +1,10 @@
.evidence/template
.svelte-kit
build
node_modules
.DS_Store
static/data
*.options.yaml
.vscode/settings.json
.env
.evidence/meta
+3
View File
@@ -0,0 +1,3 @@
loglevel=error
audit=false
fund=false
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"evidence.evidence-vscode"
]
}
+48
View File
@@ -0,0 +1,48 @@
# Evidence Template Project
## Using Codespaces
If you are using this template in Codespaces, click the `Start Evidence` button in the bottom status bar. This will install dependencies and open a preview of your project in your browser - you should get a popup prompting you to open in browser.
Or you can use the following commands to get started:
```bash
npm install
npm run sources
npm run dev -- --host 0.0.0.0
```
See [the CLI docs](https://docs.evidence.dev/cli/) for more command information.
**Note:** Codespaces is much faster on the Desktop app. After the Codespace has booted, select the hamburger menu → Open in VS Code Desktop.
## Get Started from VS Code
The easiest way to get started is using the [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Evidence.evidence-vscode):
1. Install the extension from the VS Code Marketplace
2. Open the Command Palette (Ctrl/Cmd + Shift + P) and enter `Evidence: New Evidence Project`
3. Click `Start Evidence` in the bottom status bar
## Get Started using the CLI
```bash
npx degit evidence-dev/template my-project
cd my-project
npm install
npm run sources
npm run dev
```
Check out the docs for [alternative install methods](https://docs.evidence.dev/getting-started/install-evidence) including Docker, Github Codespaces, and alongside dbt.
## Learning More
- [Docs](https://docs.evidence.dev/)
- [Github](https://github.com/evidence-dev/evidence)
- [Slack Community](https://slack.evidence.dev/)
- [Evidence Home Page](https://www.evidence.dev)
View File
View File
+81
View File
@@ -0,0 +1,81 @@
appearance:
default: system
switcher: true
theme:
colorPalettes:
default:
light:
- "#236aa4"
- "#45a1bf"
- "#a5cdee"
- "#8dacbf"
- "#85c7c6"
- "#d2c6ac"
- "#f4b548"
- "#8f3d56"
- "#71b9f4"
- "#46a485"
dark:
- "#236aa4"
- "#45a1bf"
- "#a5cdee"
- "#8dacbf"
- "#85c7c6"
- "#d2c6ac"
- "#f4b548"
- "#8f3d56"
- "#71b9f4"
- "#46a485"
colorScales:
default:
light:
- "#ADD8E6"
- "#00008B"
dark:
- "#ADD8E6"
- "#00008B"
colors:
primary:
light: "#2563eb"
dark: "#3b82f6"
accent:
light: "#c2410c"
dark: "#fdba74"
base:
light: "#ffffff"
dark: "#09090b"
info:
light: "#0284c7"
dark: "#38bdf8"
positive:
light: "#16a34a"
dark: "#4ade80"
warning:
light: "#f8c900"
dark: "#fbbf24"
negative:
light: "#dc2626"
dark: "#f87171"
plugins:
components:
# This loads all of evidence's core charts and UI components
# You probably don't want to edit this dependency unless you know what you are doing
"@evidence-dev/core-components": {}
datasources:
# You can add additional datasources here by adding npm packages.
# Make to also add them to `package.json`.
"@evidence-dev/bigquery": { }
"@evidence-dev/csv": { }
"@evidence-dev/databricks": { }
"@evidence-dev/duckdb": { }
"@evidence-dev/mssql": { }
"@evidence-dev/mysql": { }
"@evidence-dev/postgres": { }
"@evidence-dev/source-javascript": { }
"@evidence-dev/snowflake": { }
"@evidence-dev/sqlite": { }
"@evidence-dev/trino": { }
"@evidence-dev/motherduck": { }
+17662
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
{
"name": "my-evidence-project",
"version": "0.0.1",
"scripts": {
"build": "evidence build",
"build:strict": "evidence build:strict",
"dev": "evidence dev --open /",
"test": "evidence build",
"sources": "evidence sources",
"sources:strict": "evidence sources --strict",
"preview": "evidence preview"
},
"engines": {
"npm": ">=7.0.0",
"node": ">=18.0.0"
},
"type": "module",
"dependencies": {
"@evidence-dev/bigquery": "^2.0.12",
"@evidence-dev/core-components": "^5.4.2",
"@evidence-dev/csv": "^1.0.16",
"@evidence-dev/databricks": "^1.0.10",
"@evidence-dev/duckdb": "^2.0.1",
"@evidence-dev/evidence": "^40.1.8",
"@evidence-dev/motherduck": "^1.0.6",
"@evidence-dev/mssql": "^1.1.4",
"@evidence-dev/mysql": "^1.1.6",
"@evidence-dev/postgres": "^1.0.10",
"@evidence-dev/snowflake": "^1.2.4",
"@evidence-dev/source-javascript": "^0.0.3",
"@evidence-dev/sqlite": "^2.0.9",
"@evidence-dev/trino": "^1.0.11"
},
"overrides": {
"jsonwebtoken": "9.0.0",
"trim@<0.0.3": ">0.0.3",
"sqlite3": "5.1.5",
"axios": "^1.7.4",
"vitest": "^3.2.6"
}
}
+182
View File
@@ -0,0 +1,182 @@
---
title: NBA Analytics Dashboard
---
```sql games
select * from nba.games
```
```sql season_summary
select * from nba.season_summary
```
```sql competitiveness
select * from nba.competitiveness
```
```sql home_advantage
select * from nba.home_advantage
```
```sql top_scorers
select * from nba.top_scorers
```
```sql pace_and_scoring
select * from nba.pace_and_scoring
```
```sql team_ratings
select * from nba.team_ratings
```
```sql play_styles
select * from nba.play_styles
```
```sql player_impact
select * from nba.player_impact
```
```sql game_drama
select * from nba.game_drama
```
```sql usage_efficiency
select * from nba.usage_efficiency
```
# 🏀 NBA Analytics
<BigValue data={season_summary} value=total_games title="Total Games" />
<BigValue data={season_summary} value=avg_total_points title="Avg Points/Game" />
<BigValue data={season_summary} value=overtime_games title="OT Games" />
<BigValue data={season_summary} value=avg_margin title="Avg Margin" />
---
## League Evolution: Pace & Scoring
<LineChart
data={pace_and_scoring}
x=season
y={['avg_total_points', 'avg_pace']}
y2=avg_efg_pct
title="How the NBA Has Changed: Points, Pace & Efficiency"
yAxisTitle="Points / Pace"
y2AxisTitle="eFG%"
/>
## Game Drama Over Time
<AreaChart
data={game_drama}
x=season
y={['clutch_pct', 'blowout_pct', 'ot_pct']}
title="% of Games: Clutch (≤5pt margin) vs Blowouts (20+) vs Overtime"
yAxisTitle="% of Games"
/>
---
## Current Season: Team Power Rankings
<BarChart
data={team_ratings}
x=team
y=avg_net_rating
title="Net Rating by Team (Current Season)"
swapXY=true
colorPalette={['#dc2626', '#dc2626', '#dc2626', '#dc2626', '#dc2626', '#f97316', '#f97316', '#f97316', '#f97316', '#f97316', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#6b7280', '#22c55e', '#22c55e', '#22c55e', '#22c55e', '#22c55e', '#16a34a', '#16a34a', '#16a34a', '#16a34a', '#16a34a']}
/>
<DataTable data={team_ratings} rows=30>
<Column id=team title="Team" />
<Column id=wins title="W" />
<Column id=games title="GP" />
<Column id=win_pct title="Win%" />
<Column id=avg_off_rating title="ORtg" />
<Column id=avg_def_rating title="DRtg" />
<Column id=avg_net_rating title="Net" contentType=colorscale colorScale=RdYlGn />
<Column id=avg_pace title="Pace" />
</DataTable>
## Shot Selection Style & Winning
<BarChart
data={play_styles}
x=shot_selection_style
y={['win_pct', 'avg_off_rating']}
y2=avg_off_rating
title="Does Three-Point Shooting Win Games?"
yAxisTitle="Win %"
y2AxisTitle="Off Rating"
/>
---
## Player Impact: Usage vs Efficiency
<ScatterPlot
data={usage_efficiency}
x=usage
y=efficiency
size=ppg
tooltipTitle=player_name
title="Usage Rate vs True Shooting % (bubble size = PPG)"
xAxisTitle="Usage Rate %"
yAxisTitle="True Shooting %"
/>
## Top 30 Players by Box Plus/Minus (Current Season)
<DataTable data={player_impact} rows=30>
<Column id=player_name title="Player" />
<Column id=team title="Team" />
<Column id=games title="GP" />
<Column id=ppg title="PPG" />
<Column id=apg title="APG" />
<Column id=rpg title="RPG" />
<Column id=avg_bpm title="BPM" contentType=colorscale colorScale=RdYlGn />
<Column id=avg_usage title="USG%" />
<Column id=ts_pct title="TS%" />
<Column id=double_doubles title="DD" />
<Column id=triple_doubles title="TD" />
</DataTable>
<BarChart
data={player_impact}
x=player_name
y=avg_bpm
title="Box Plus/Minus Leaders"
swapXY=true
/>
---
## Home Court Advantage Trend
<LineChart
data={home_advantage}
x=season_display
y=home_win_pct
title="Home Team Win % by Season"
yAxisTitle="Win %"
yMin=40
yMax=70
markers=true
/>
## Recent Games
<DataTable data={games} rows=20>
<Column id=game_date title="Date" />
<Column id=home_team title="Home" />
<Column id=visitor_team title="Away" />
<Column id=home_points title="H Pts" />
<Column id=visitor_points title="A Pts" />
<Column id=winning_team title="Winner" />
<Column id=point_differential title="Margin" />
<Column id=game_competitiveness_tier title="Type" />
<Column id=is_overtime title="OT" />
</DataTable>
@@ -0,0 +1,6 @@
select
game_competitiveness_tier,
count(*) as games
from main_marts.fct_game_results
group by game_competitiveness_tier
order by games desc
@@ -0,0 +1,4 @@
name: nba
type: duckdb
options:
filename: dbt_nba.duckdb
@@ -0,0 +1,8 @@
select
season_start_year || '-' || substr(cast(season_start_year + 1 as varchar), 3, 2) as season,
round(100.0 * sum(case when is_overtime then 1 else 0 end) / count(*), 1) as ot_pct,
round(100.0 * sum(case when point_differential <= 5 then 1 else 0 end) / count(*), 1) as clutch_pct,
round(100.0 * sum(case when point_differential >= 20 then 1 else 0 end) / count(*), 1) as blowout_pct
from main_intermediate.int_games_enriched
group by season_start_year
order by season_start_year
+18
View File
@@ -0,0 +1,18 @@
select
g.game_date,
g.home_points,
g.visitor_points,
g.total_points,
g.point_differential,
g.is_overtime,
g.is_playoff,
g.game_competitiveness_tier,
g.is_home_team_winner,
ht.team_abbr as home_team,
vt.team_abbr as visitor_team,
wt.team_abbr as winning_team
from main_marts.fct_game_results g
left join main_marts.dim_teams ht on g.home_team_key = ht.team_key
left join main_marts.dim_teams vt on g.visitor_team_key = vt.team_key
left join main_marts.dim_teams wt on g.winning_team_key = wt.team_key
order by g.game_date desc
@@ -0,0 +1,9 @@
select
s.season_display,
count(*) as total_games,
sum(case when g.is_home_team_winner then 1 else 0 end) as home_wins,
round(100.0 * sum(case when g.is_home_team_winner then 1 else 0 end) / count(*), 1) as home_win_pct
from main_marts.fct_game_results g
left join main_marts.dim_seasons s on g.season_key = s.season_key
group by s.season_display
order by s.season_display
@@ -0,0 +1,10 @@
select
season_start_year || '-' || substr(cast(season_start_year + 1 as varchar), 3, 2) as season,
round(avg(matchup_pace), 1) as avg_pace,
round(avg(total_points), 1) as avg_total_points,
round(avg(home_effective_fg_pct + visitor_effective_fg_pct) / 2 * 100, 1) as avg_efg_pct,
round(avg(point_differential), 1) as avg_margin,
count(*) as games
from main_intermediate.int_games_enriched
group by season_start_year
order by season_start_year
@@ -0,0 +1,9 @@
select
shot_selection_style,
count(*) as games,
round(avg(case when game_result = 'W' then 1.0 else 0.0 end) * 100, 1) as win_pct,
round(avg(offensive_rating), 1) as avg_off_rating
from main_intermediate.int_team_performance
where season_start_year = (select max(season_start_year) from main_intermediate.int_team_performance)
group by shot_selection_style
order by avg_off_rating desc
@@ -0,0 +1,20 @@
select
player_name,
team,
count(*) as games,
round(avg(points), 1) as ppg,
round(avg(assists), 1) as apg,
round(avg(total_rebounds), 1) as rpg,
round(avg(box_plus_minus), 2) as avg_bpm,
round(avg(usage_pct), 1) as avg_usage,
round(avg(true_shooting_pct) * 100, 1) as ts_pct,
round(avg(net_rating), 1) as avg_net_rating,
sum(case when is_double_double then 1 else 0 end) as double_doubles,
sum(case when is_triple_double then 1 else 0 end) as triple_doubles
from main_intermediate.int_player_performance
where season_start_year = (select max(season_start_year) from main_intermediate.int_player_performance)
and minutes_played >= 20
group by player_name, team
having count(*) >= 20
order by avg_bpm desc
limit 30
@@ -0,0 +1,11 @@
select
s.season_display,
count(*) as total_games,
sum(case when g.is_overtime then 1 else 0 end) as overtime_games,
sum(case when g.is_playoff then 1 else 0 end) as playoff_games,
round(avg(g.total_points), 1) as avg_total_points,
round(avg(g.point_differential), 1) as avg_margin
from main_marts.fct_game_results g
left join main_marts.dim_seasons s on g.season_key = s.season_key
group by s.season_display
order by s.season_display desc
@@ -0,0 +1,14 @@
select
team,
count(*) as games,
sum(case when game_result = 'W' then 1 else 0 end) as wins,
round(100.0 * sum(case when game_result = 'W' then 1 else 0 end) / count(*), 1) as win_pct,
round(avg(offensive_rating), 1) as avg_off_rating,
round(avg(defensive_rating), 1) as avg_def_rating,
round(avg(net_rating), 1) as avg_net_rating,
round(avg(pace), 1) as avg_pace,
round(avg(effective_fg_pct) * 100, 1) as avg_efg_pct
from main_intermediate.int_team_performance
where season_start_year = (select max(season_start_year) from main_intermediate.int_team_performance)
group by team
order by avg_net_rating desc
@@ -0,0 +1,13 @@
select
p.player_name,
count(*) as games_played,
round(avg(ps.points), 1) as ppg,
round(avg(ps.total_rebounds), 1) as rpg,
round(avg(ps.assists), 1) as apg,
round(avg(ps.true_shooting_pct) * 100, 1) as ts_pct
from main_marts.fct_player_game_stats ps
left join main_marts.dim_players p on ps.player_key = p.player_key
group by p.player_name
having count(*) >= 50
order by ppg desc
limit 20
@@ -0,0 +1,13 @@
select
player_name,
team,
count(*) as games,
round(avg(usage_pct), 1) as usage,
round(avg(true_shooting_pct) * 100, 1) as efficiency,
round(avg(points), 1) as ppg
from main_intermediate.int_player_performance
where season_start_year = (select max(season_start_year) from main_intermediate.int_player_performance)
and minutes_played >= 20
group by player_name, team
having count(*) >= 20
order by usage desc
+97
View File
@@ -0,0 +1,97 @@
arena_name,city,state_or_country,location
ARCO Arena (II),Sacramento,California,North Natomas
AT&T Center,San Antonio,Texas,East Side
AccorHotels Arena,Paris,France,Bercy
Air Canada Centre,Toronto,Ontario,Harbourfront
Alamodome,San Antonio,Texas,Downtown
Amalie Arena,Tampa,Florida,Channelside
America West Arena,Phoenix,Arizona,Downtown
American Airlines Center,Dallas,Texas,Victory Park
AmericanAirlines Arena,Miami,Florida,Waterfront
Amway Arena,Orlando,Florida,Downtown
Amway Center,Orlando,Florida,Parramore
BMO Harris Bradley Center,Milwaukee,Wisconsin,Downtown
Ball Arena,Denver,Colorado,Central Platte Valley
Bankers Life Fieldhouse,Indianapolis,Indiana,Downtown
Barclays Center,Brooklyn,New York,Atlantic Yards
Bradley Center,Milwaukee,Wisconsin,Downtown
Capital One Arena,Washington,D.C.,Penn Quarter
Charlotte Bobcats Arena,Charlotte,North Carolina,Uptown
Charlotte Coliseum,Charlotte,North Carolina,Southwest Charlotte
Chase Center,San Francisco,California,Mission Bay
Chesapeake Energy Arena,Oklahoma City,Oklahoma,Downtown
Compaq Center,Houston,Texas,Greenway Plaza
Conseco Fieldhouse,Indianapolis,Indiana,Downtown
Continental Airlines Arena,East Rutherford,New Jersey,Meadowlands
Crypto.com Arena,Los Angeles,California,South Park
Delta Center,Salt Lake City,Utah,Downtown
EnergySolutions Arena,Salt Lake City,Utah,Downtown
FTX Arena,Miami,Florida,Waterfront
FedEx Forum,Memphis,Tennessee,Downtown
FedExForum,Memphis,Tennessee,Downtown
First Union Center,Philadelphia,Pennsylvania,South Philadelphia
Fiserv Forum,Milwaukee,Wisconsin,Deer District
FleetCenter,Boston,Massachusetts,West End
Footprint Center,Phoenix,Arizona,Downtown
Ford Center,Oklahoma City,Oklahoma,Downtown
Frost Bank Center,San Antonio,Texas,East Side
Gainbridge Fieldhouse,Indianapolis,Indiana,Downtown
Golden 1 Center,Sacramento,California,Downtown Commons
Gund Arena,Cleveland,Ohio,Gateway District
Intuit Dome,Inglewood,California,Hollywood Park
Izod Center,East Rutherford,New Jersey,Meadowlands
Kaseya Center,Miami,Florida,Waterfront
KeyArena at Seattle Center,Seattle,Washington,Seattle Center
Kia Center,Orlando,Florida,Parramore
LLoyd Noble Center,Norman,Oklahoma,University of Oklahoma
Little Caesars Arena,Detroit,Michigan,Midtown
MCI Center,Washington,D.C.,Penn Quarter
Madison Square Garden (IV),New York,New York,Midtown Manhattan
Mexico City Arena,Mexico City,Mexico,Azcapotzalco
Moda Center,Portland,Oregon,Lloyd District
Moody Center,Austin,Texas,University of Texas
Mortgage Matchup Center,Cleveland,Ohio,Gateway District
New Orleans Arena,New Orleans,Louisiana,Central Business District
Oakland Arena,Oakland,California,Coliseum Complex
Oklahoma City Arena,Oklahoma City,Oklahoma,Downtown
Oracle Arena,Oakland,California,Coliseum Complex
Paycom Center,Oklahoma City,Oklahoma,Downtown
Pepsi Center,Denver,Colorado,Central Platte Valley
Pete Maravich Assembly Center,Baton Rouge,Louisiana,LSU Campus
Philips Arena,Atlanta,Georgia,Downtown
Phoenix Suns Arena,Phoenix,Arizona,Downtown
Power Balance Pavilion,Sacramento,California,North Natomas
Prudential Center,Newark,New Jersey,Downtown
Pyramid Arena,Memphis,Tennessee,Downtown
Quicken Loans Arena,Cleveland,Ohio,Gateway District
Rocket Arena,Houston,Texas,Greenway Plaza
Rocket Mortgage Fieldhouse,Cleveland,Ohio,Gateway District
Rose Garden Arena,Portland,Oregon,Lloyd District
SBC Center,San Antonio,Texas,East Side
STAPLES Center,Los Angeles,California,South Park
Saitama Super Arena,Saitama,Japan,Chūō-ku
Scotiabank Arena,Toronto,Ontario,Harbourfront
Sleep Train Arena,Sacramento,California,North Natomas
Smoothie King Center,New Orleans,Louisiana,Central Business District
Spectrum Center,Charlotte,North Carolina,Uptown
State Farm Arena,Atlanta,Georgia,Downtown
T-Mobile Arena,Las Vegas,Nevada,Paradise
TD Banknorth Garden,Boston,Massachusetts,West End
TD Garden,Boston,Massachusetts,West End
TD Waterhouse Centre,Orlando,Florida,Downtown
Talking Stick Resort Arena,Phoenix,Arizona,Downtown
Target Center,Minneapolis,Minnesota,Warehouse District
The Arena in Oakland,Oakland,California,Coliseum Complex
The O2 Arena,London,England,Greenwich Peninsula
The Palace of Auburn Hills,Auburn Hills,Michigan,Auburn Hills
Time Warner Cable Arena,Charlotte,North Carolina,Uptown
Toyota Center,Houston,Texas,Downtown
US Airways Center,Phoenix,Arizona,Downtown
UWMilwaukee Panther Arena,Milwaukee,Wisconsin,Downtown
United Center,Chicago,Illinois,Near West Side
Verizon Center,Washington,D.C.,Penn Quarter
Vivint Arena,Salt Lake City,Utah,Downtown
Vivint Smart Home Arena,Salt Lake City,Utah,Downtown
Wachovia Center,Philadelphia,Pennsylvania,South Philadelphia
Wells Fargo Center,Philadelphia,Pennsylvania,South Philadelphia
Xfinity Mobile Arena,Manchester,New Hampshire,Downtown
1 arena_name city state_or_country location
2 ARCO Arena (II) Sacramento California North Natomas
3 AT&T Center San Antonio Texas East Side
4 AccorHotels Arena Paris France Bercy
5 Air Canada Centre Toronto Ontario Harbourfront
6 Alamodome San Antonio Texas Downtown
7 Amalie Arena Tampa Florida Channelside
8 America West Arena Phoenix Arizona Downtown
9 American Airlines Center Dallas Texas Victory Park
10 AmericanAirlines Arena Miami Florida Waterfront
11 Amway Arena Orlando Florida Downtown
12 Amway Center Orlando Florida Parramore
13 BMO Harris Bradley Center Milwaukee Wisconsin Downtown
14 Ball Arena Denver Colorado Central Platte Valley
15 Bankers Life Fieldhouse Indianapolis Indiana Downtown
16 Barclays Center Brooklyn New York Atlantic Yards
17 Bradley Center Milwaukee Wisconsin Downtown
18 Capital One Arena Washington D.C. Penn Quarter
19 Charlotte Bobcats Arena Charlotte North Carolina Uptown
20 Charlotte Coliseum Charlotte North Carolina Southwest Charlotte
21 Chase Center San Francisco California Mission Bay
22 Chesapeake Energy Arena Oklahoma City Oklahoma Downtown
23 Compaq Center Houston Texas Greenway Plaza
24 Conseco Fieldhouse Indianapolis Indiana Downtown
25 Continental Airlines Arena East Rutherford New Jersey Meadowlands
26 Crypto.com Arena Los Angeles California South Park
27 Delta Center Salt Lake City Utah Downtown
28 EnergySolutions Arena Salt Lake City Utah Downtown
29 FTX Arena Miami Florida Waterfront
30 FedEx Forum Memphis Tennessee Downtown
31 FedExForum Memphis Tennessee Downtown
32 First Union Center Philadelphia Pennsylvania South Philadelphia
33 Fiserv Forum Milwaukee Wisconsin Deer District
34 FleetCenter Boston Massachusetts West End
35 Footprint Center Phoenix Arizona Downtown
36 Ford Center Oklahoma City Oklahoma Downtown
37 Frost Bank Center San Antonio Texas East Side
38 Gainbridge Fieldhouse Indianapolis Indiana Downtown
39 Golden 1 Center Sacramento California Downtown Commons
40 Gund Arena Cleveland Ohio Gateway District
41 Intuit Dome Inglewood California Hollywood Park
42 Izod Center East Rutherford New Jersey Meadowlands
43 Kaseya Center Miami Florida Waterfront
44 KeyArena at Seattle Center Seattle Washington Seattle Center
45 Kia Center Orlando Florida Parramore
46 LLoyd Noble Center Norman Oklahoma University of Oklahoma
47 Little Caesars Arena Detroit Michigan Midtown
48 MCI Center Washington D.C. Penn Quarter
49 Madison Square Garden (IV) New York New York Midtown Manhattan
50 Mexico City Arena Mexico City Mexico Azcapotzalco
51 Moda Center Portland Oregon Lloyd District
52 Moody Center Austin Texas University of Texas
53 Mortgage Matchup Center Cleveland Ohio Gateway District
54 New Orleans Arena New Orleans Louisiana Central Business District
55 Oakland Arena Oakland California Coliseum Complex
56 Oklahoma City Arena Oklahoma City Oklahoma Downtown
57 Oracle Arena Oakland California Coliseum Complex
58 Paycom Center Oklahoma City Oklahoma Downtown
59 Pepsi Center Denver Colorado Central Platte Valley
60 Pete Maravich Assembly Center Baton Rouge Louisiana LSU Campus
61 Philips Arena Atlanta Georgia Downtown
62 Phoenix Suns Arena Phoenix Arizona Downtown
63 Power Balance Pavilion Sacramento California North Natomas
64 Prudential Center Newark New Jersey Downtown
65 Pyramid Arena Memphis Tennessee Downtown
66 Quicken Loans Arena Cleveland Ohio Gateway District
67 Rocket Arena Houston Texas Greenway Plaza
68 Rocket Mortgage Fieldhouse Cleveland Ohio Gateway District
69 Rose Garden Arena Portland Oregon Lloyd District
70 SBC Center San Antonio Texas East Side
71 STAPLES Center Los Angeles California South Park
72 Saitama Super Arena Saitama Japan Chūō-ku
73 Scotiabank Arena Toronto Ontario Harbourfront
74 Sleep Train Arena Sacramento California North Natomas
75 Smoothie King Center New Orleans Louisiana Central Business District
76 Spectrum Center Charlotte North Carolina Uptown
77 State Farm Arena Atlanta Georgia Downtown
78 T-Mobile Arena Las Vegas Nevada Paradise
79 TD Banknorth Garden Boston Massachusetts West End
80 TD Garden Boston Massachusetts West End
81 TD Waterhouse Centre Orlando Florida Downtown
82 Talking Stick Resort Arena Phoenix Arizona Downtown
83 Target Center Minneapolis Minnesota Warehouse District
84 The Arena in Oakland Oakland California Coliseum Complex
85 The O2 Arena London England Greenwich Peninsula
86 The Palace of Auburn Hills Auburn Hills Michigan Auburn Hills
87 Time Warner Cable Arena Charlotte North Carolina Uptown
88 Toyota Center Houston Texas Downtown
89 US Airways Center Phoenix Arizona Downtown
90 UW–Milwaukee Panther Arena Milwaukee Wisconsin Downtown
91 United Center Chicago Illinois Near West Side
92 Verizon Center Washington D.C. Penn Quarter
93 Vivint Arena Salt Lake City Utah Downtown
94 Vivint Smart Home Arena Salt Lake City Utah Downtown
95 Wachovia Center Philadelphia Pennsylvania South Philadelphia
96 Wells Fargo Center Philadelphia Pennsylvania South Philadelphia
97 Xfinity Mobile Arena Manchester New Hampshire Downtown
+97
View File
@@ -0,0 +1,97 @@
arena_name,city,state_or_country,location,start_date,end_date
ARCO Arena (II),Sacramento,California,North Natomas,1988-11-08,2016-10-30
AT&T Center,San Antonio,Texas,East Side,2006-01-01,2023-09-21
AccorHotels Arena,Paris,France,Bercy (12th arrondissement),2015-10-01,2020-06-01
Air Canada Centre,Toronto,Ontario,Downtown/Harbourfront,1999-02-20,2018-06-30
Alamodome,San Antonio,Texas,Downtown,1993-05-15,2026-06-30
Amalie Arena,Tampa,Florida,Channelside,1996-10-20,2026-06-30
America West Arena,Phoenix,Arizona,Downtown,1992-06-06,2006-09-30
American Airlines Center,Dallas,Texas,Victory Park,2001-07-17,2026-06-30
AmericanAirlines Arena,Miami,Florida,Downtown/Biscayne Bay,1999-12-31,2021-06-01
Amway Arena,Orlando,Florida,Downtown,1989-01-29,2010-10-01
Amway Center,Orlando,Florida,Downtown,2010-10-01,2023-12-19
BMO Harris Bradley Center,Milwaukee,Wisconsin,Downtown,2012-05-21,2018-09-17
Ball Arena,Denver,Colorado,Central Platte Valley/LoDo,2020-10-22,2026-06-30
Bankers Life Fieldhouse,Indianapolis,Indiana,Downtown,2011-12-01,2021-09-26
Barclays Center,Brooklyn,New York,Downtown Brooklyn/Prospect Heights,2012-09-21,2026-06-30
Bradley Center,Milwaukee,Wisconsin,Downtown,1988-10-01,2012-05-20
Capital One Arena,Washington,District of Columbia,Chinatown/Penn Quarter,2017-08-09,2026-06-30
Charlotte Bobcats Arena,Charlotte,North Carolina,Uptown (First Ward),2005-10-21,2008-08-01
Charlotte Coliseum,Charlotte,North Carolina,Tyvola Road,1988-08-11,2005-10-26
Chase Center,San Francisco,California,Mission Bay,2019-09-06,2026-06-30
Chesapeake Energy Arena,Oklahoma City,Oklahoma,Downtown,2011-01-01,2021-04-01
Compaq Center,Houston,Texas,Greenway Plaza,1998-01-01,2003-10-01
Conseco Fieldhouse,Indianapolis,Indiana,Downtown,1999-11-06,2011-11-30
Continental Airlines Arena,East Rutherford,New Jersey,Meadowlands Sports Complex,1996-01-04,2007-06-30
Crypto.com Arena,Los Angeles,California,South Park/Downtown,2021-12-25,2026-06-30
Delta Center,Salt Lake City,Utah,West Downtown,1991-10-04,2026-06-30
EnergySolutions Arena,Salt Lake City,Utah,West Downtown,2006-11-20,2015-10-25
FTX Arena,Miami,Florida,Downtown/Biscayne Bay,2021-06-01,2023-01-01
FedEx Forum,Memphis,Tennessee,Downtown (near Beale Street),2004-09-06,2026-06-30
FedExForum,Memphis,Tennessee,Downtown (near Beale Street),2004-09-06,2026-06-30
First Union Center,Philadelphia,Pennsylvania,South Philadelphia Sports Complex,1998-09-01,2003-06-30
Fiserv Forum,Milwaukee,Wisconsin,Deer District/Downtown,2018-08-26,2026-06-30
FleetCenter,Boston,Massachusetts,West End,1995-09-30,2005-06-30
Footprint Center,Phoenix,Arizona,Downtown,2021-07-16,2025-02-17
Ford Center,Oklahoma City,Oklahoma,Downtown,2002-06-08,2010-12-31
Frost Bank Center,San Antonio,Texas,East Side,2023-09-22,2026-06-30
Gainbridge Fieldhouse,Indianapolis,Indiana,Downtown,2021-09-27,2026-06-30
Golden 1 Center,Sacramento,California,Downtown Commons (DoCo),2016-10-04,2026-06-30
Gund Arena,Cleveland,Ohio,Gateway District/Downtown,1994-10-01,2005-08-01
Intuit Dome,Inglewood,California,Hollywood Park,2024-08-15,2026-06-30
Izod Center,East Rutherford,New Jersey,Meadowlands Sports Complex,2007-07-01,2015-01-15
Kaseya Center,Miami,Florida,Downtown/Biscayne Bay,2023-04-01,2026-06-30
KeyArena at Seattle Center,Seattle,Washington,Seattle Center/Lower Queen Anne,1995-10-26,2008-04-13
Kia Center,Orlando,Florida,Downtown,2023-12-20,2026-06-30
LLoyd Noble Center,Norman,Oklahoma,University of Oklahoma Campus,1975-11-01,2026-06-30
Little Caesars Arena,Detroit,Michigan,Midtown/District Detroit,2017-09-05,2026-06-30
MCI Center,Washington,District of Columbia,Chinatown/Penn Quarter,1997-12-02,2006-02-28
Madison Square Garden (IV),New York,New York,Midtown Manhattan (Penn Station),1968-02-11,2026-06-30
Mexico City Arena,Mexico City,Mexico,Azcapotzalco,2012-02-25,2026-06-30
Moda Center,Portland,Oregon,Rose Quarter/Lloyd District,2013-08-13,2026-06-30
Moody Center,Austin,Texas,UT Campus,2022-04-20,2026-06-30
Mortgage Matchup Center,Phoenix,Arizona,Downtown,2025-10-02,2026-06-30
New Orleans Arena,New Orleans,Louisiana,Central Business District,1999-10-01,2014-02-04
Oakland Arena,Oakland,California,Coliseum Industrial,2019-06-14,2026-06-30
Oklahoma City Arena,Oklahoma City,Oklahoma,Downtown,2010-01-01,2010-12-31
Oracle Arena,Oakland,California,Coliseum Industrial,2006-10-20,2019-06-13
Paycom Center,Oklahoma City,Oklahoma,Downtown,2021-07-27,2026-06-30
Pepsi Center,Denver,Colorado,Central Platte Valley/LoDo,1999-10-01,2020-10-21
Pete Maravich Assembly Center,Baton Rouge,Louisiana,LSU Campus,1972-01-15,2026-06-30
Philips Arena,Atlanta,Georgia,Centennial Olympic Park District,1999-09-01,2018-08-28
Phoenix Suns Arena,Phoenix,Arizona,Downtown,2020-11-01,2021-07-15
Power Balance Pavilion,Sacramento,California,North Natomas,2011-03-01,2012-12-31
Prudential Center,Newark,New Jersey,Downtown Newark,2007-10-25,2026-06-30
Pyramid Arena,Memphis,Tennessee,Downtown (Riverfront),1991-11-09,2004-12-31
Quicken Loans Arena,Cleveland,Ohio,Gateway District/Downtown,2005-08-01,2019-03-31
Rocket Arena,Cleveland,Ohio,Gateway District/Downtown,2025-02-18,2026-06-30
Rocket Mortgage Fieldhouse,Cleveland,Ohio,Gateway District/Downtown,2019-04-01,2025-02-17
Rose Garden Arena,Portland,Oregon,Rose Quarter/Lloyd District,1995-10-12,2013-08-12
SBC Center,San Antonio,Texas,East Side,2002-10-18,2005-12-31
STAPLES Center,Los Angeles,California,South Park/Downtown,1999-10-17,2021-12-24
Saitama Super Arena,Saitama,Japan,Chuo-ku/Shin-Toshin,2000-09-01,2026-06-30
Scotiabank Arena,Toronto,Ontario,Downtown/Harbourfront,2018-07-01,2026-06-30
Sleep Train Arena,Sacramento,California,North Natomas,2013-01-01,2016-10-30
Smoothie King Center,New Orleans,Louisiana,Central Business District,2014-02-05,2026-06-30
Spectrum Center,Charlotte,North Carolina,Uptown (First Ward),2016-08-17,2026-06-30
State Farm Arena,Atlanta,Georgia,Centennial Olympic Park District,2018-08-29,2026-06-30
T-Mobile Arena,Las Vegas,Nevada,Las Vegas Strip,2016-04-06,2026-06-30
TD Banknorth Garden,Boston,Massachusetts,West End,2005-07-01,2009-07-15
TD Garden,Boston,Massachusetts,West End,2009-07-16,2026-06-30
TD Waterhouse Centre,Orlando,Florida,Downtown,1999-10-01,2002-12-31
Talking Stick Resort Arena,Phoenix,Arizona,Downtown,2015-09-01,2020-10-31
Target Center,Minneapolis,Minnesota,Warehouse District/North Loop,1990-10-16,2026-06-30
The Arena in Oakland,Oakland,California,Coliseum Industrial,1997-10-01,2004-12-31
The O2 Arena,London,United Kingdom,Greenwich Peninsula,2007-06-24,2026-06-30
The Palace of Auburn Hills,Auburn Hills,Michigan,Auburn Hills (I-75 corridor),1988-10-01,2017-10-12
Time Warner Cable Arena,Charlotte,North Carolina,Uptown (First Ward),2008-08-01,2016-08-16
Toyota Center,Houston,Texas,Downtown/East Downtown,2003-10-06,2026-06-30
US Airways Center,Phoenix,Arizona,Downtown,2006-10-01,2015-08-31
UWMilwaukee Panther Arena,Milwaukee,Wisconsin,Downtown,1950-08-04,2026-06-30
United Center,Chicago,Illinois,Near West Side,1994-08-18,2026-06-30
Verizon Center,Washington,District of Columbia,Chinatown/Penn Quarter,2006-03-01,2017-08-08
Vivint Arena,Salt Lake City,Utah,West Downtown,2020-01-01,2023-06-30
Vivint Smart Home Arena,Salt Lake City,Utah,West Downtown,2015-10-26,2019-12-31
Wachovia Center,Philadelphia,Pennsylvania,South Philadelphia Sports Complex,2003-07-01,2010-06-30
Wells Fargo Center,Philadelphia,Pennsylvania,South Philadelphia Sports Complex,2010-07-01,2025-08-31
Xfinity Mobile Arena,Philadelphia,Pennsylvania,South Philadelphia Sports Complex,2025-09-01,2026-06-30
1 arena_name city state_or_country location start_date end_date
2 ARCO Arena (II) Sacramento California North Natomas 1988-11-08 2016-10-30
3 AT&T Center San Antonio Texas East Side 2006-01-01 2023-09-21
4 AccorHotels Arena Paris France Bercy (12th arrondissement) 2015-10-01 2020-06-01
5 Air Canada Centre Toronto Ontario Downtown/Harbourfront 1999-02-20 2018-06-30
6 Alamodome San Antonio Texas Downtown 1993-05-15 2026-06-30
7 Amalie Arena Tampa Florida Channelside 1996-10-20 2026-06-30
8 America West Arena Phoenix Arizona Downtown 1992-06-06 2006-09-30
9 American Airlines Center Dallas Texas Victory Park 2001-07-17 2026-06-30
10 AmericanAirlines Arena Miami Florida Downtown/Biscayne Bay 1999-12-31 2021-06-01
11 Amway Arena Orlando Florida Downtown 1989-01-29 2010-10-01
12 Amway Center Orlando Florida Downtown 2010-10-01 2023-12-19
13 BMO Harris Bradley Center Milwaukee Wisconsin Downtown 2012-05-21 2018-09-17
14 Ball Arena Denver Colorado Central Platte Valley/LoDo 2020-10-22 2026-06-30
15 Bankers Life Fieldhouse Indianapolis Indiana Downtown 2011-12-01 2021-09-26
16 Barclays Center Brooklyn New York Downtown Brooklyn/Prospect Heights 2012-09-21 2026-06-30
17 Bradley Center Milwaukee Wisconsin Downtown 1988-10-01 2012-05-20
18 Capital One Arena Washington District of Columbia Chinatown/Penn Quarter 2017-08-09 2026-06-30
19 Charlotte Bobcats Arena Charlotte North Carolina Uptown (First Ward) 2005-10-21 2008-08-01
20 Charlotte Coliseum Charlotte North Carolina Tyvola Road 1988-08-11 2005-10-26
21 Chase Center San Francisco California Mission Bay 2019-09-06 2026-06-30
22 Chesapeake Energy Arena Oklahoma City Oklahoma Downtown 2011-01-01 2021-04-01
23 Compaq Center Houston Texas Greenway Plaza 1998-01-01 2003-10-01
24 Conseco Fieldhouse Indianapolis Indiana Downtown 1999-11-06 2011-11-30
25 Continental Airlines Arena East Rutherford New Jersey Meadowlands Sports Complex 1996-01-04 2007-06-30
26 Crypto.com Arena Los Angeles California South Park/Downtown 2021-12-25 2026-06-30
27 Delta Center Salt Lake City Utah West Downtown 1991-10-04 2026-06-30
28 EnergySolutions Arena Salt Lake City Utah West Downtown 2006-11-20 2015-10-25
29 FTX Arena Miami Florida Downtown/Biscayne Bay 2021-06-01 2023-01-01
30 FedEx Forum Memphis Tennessee Downtown (near Beale Street) 2004-09-06 2026-06-30
31 FedExForum Memphis Tennessee Downtown (near Beale Street) 2004-09-06 2026-06-30
32 First Union Center Philadelphia Pennsylvania South Philadelphia Sports Complex 1998-09-01 2003-06-30
33 Fiserv Forum Milwaukee Wisconsin Deer District/Downtown 2018-08-26 2026-06-30
34 FleetCenter Boston Massachusetts West End 1995-09-30 2005-06-30
35 Footprint Center Phoenix Arizona Downtown 2021-07-16 2025-02-17
36 Ford Center Oklahoma City Oklahoma Downtown 2002-06-08 2010-12-31
37 Frost Bank Center San Antonio Texas East Side 2023-09-22 2026-06-30
38 Gainbridge Fieldhouse Indianapolis Indiana Downtown 2021-09-27 2026-06-30
39 Golden 1 Center Sacramento California Downtown Commons (DoCo) 2016-10-04 2026-06-30
40 Gund Arena Cleveland Ohio Gateway District/Downtown 1994-10-01 2005-08-01
41 Intuit Dome Inglewood California Hollywood Park 2024-08-15 2026-06-30
42 Izod Center East Rutherford New Jersey Meadowlands Sports Complex 2007-07-01 2015-01-15
43 Kaseya Center Miami Florida Downtown/Biscayne Bay 2023-04-01 2026-06-30
44 KeyArena at Seattle Center Seattle Washington Seattle Center/Lower Queen Anne 1995-10-26 2008-04-13
45 Kia Center Orlando Florida Downtown 2023-12-20 2026-06-30
46 LLoyd Noble Center Norman Oklahoma University of Oklahoma Campus 1975-11-01 2026-06-30
47 Little Caesars Arena Detroit Michigan Midtown/District Detroit 2017-09-05 2026-06-30
48 MCI Center Washington District of Columbia Chinatown/Penn Quarter 1997-12-02 2006-02-28
49 Madison Square Garden (IV) New York New York Midtown Manhattan (Penn Station) 1968-02-11 2026-06-30
50 Mexico City Arena Mexico City Mexico Azcapotzalco 2012-02-25 2026-06-30
51 Moda Center Portland Oregon Rose Quarter/Lloyd District 2013-08-13 2026-06-30
52 Moody Center Austin Texas UT Campus 2022-04-20 2026-06-30
53 Mortgage Matchup Center Phoenix Arizona Downtown 2025-10-02 2026-06-30
54 New Orleans Arena New Orleans Louisiana Central Business District 1999-10-01 2014-02-04
55 Oakland Arena Oakland California Coliseum Industrial 2019-06-14 2026-06-30
56 Oklahoma City Arena Oklahoma City Oklahoma Downtown 2010-01-01 2010-12-31
57 Oracle Arena Oakland California Coliseum Industrial 2006-10-20 2019-06-13
58 Paycom Center Oklahoma City Oklahoma Downtown 2021-07-27 2026-06-30
59 Pepsi Center Denver Colorado Central Platte Valley/LoDo 1999-10-01 2020-10-21
60 Pete Maravich Assembly Center Baton Rouge Louisiana LSU Campus 1972-01-15 2026-06-30
61 Philips Arena Atlanta Georgia Centennial Olympic Park District 1999-09-01 2018-08-28
62 Phoenix Suns Arena Phoenix Arizona Downtown 2020-11-01 2021-07-15
63 Power Balance Pavilion Sacramento California North Natomas 2011-03-01 2012-12-31
64 Prudential Center Newark New Jersey Downtown Newark 2007-10-25 2026-06-30
65 Pyramid Arena Memphis Tennessee Downtown (Riverfront) 1991-11-09 2004-12-31
66 Quicken Loans Arena Cleveland Ohio Gateway District/Downtown 2005-08-01 2019-03-31
67 Rocket Arena Cleveland Ohio Gateway District/Downtown 2025-02-18 2026-06-30
68 Rocket Mortgage Fieldhouse Cleveland Ohio Gateway District/Downtown 2019-04-01 2025-02-17
69 Rose Garden Arena Portland Oregon Rose Quarter/Lloyd District 1995-10-12 2013-08-12
70 SBC Center San Antonio Texas East Side 2002-10-18 2005-12-31
71 STAPLES Center Los Angeles California South Park/Downtown 1999-10-17 2021-12-24
72 Saitama Super Arena Saitama Japan Chuo-ku/Shin-Toshin 2000-09-01 2026-06-30
73 Scotiabank Arena Toronto Ontario Downtown/Harbourfront 2018-07-01 2026-06-30
74 Sleep Train Arena Sacramento California North Natomas 2013-01-01 2016-10-30
75 Smoothie King Center New Orleans Louisiana Central Business District 2014-02-05 2026-06-30
76 Spectrum Center Charlotte North Carolina Uptown (First Ward) 2016-08-17 2026-06-30
77 State Farm Arena Atlanta Georgia Centennial Olympic Park District 2018-08-29 2026-06-30
78 T-Mobile Arena Las Vegas Nevada Las Vegas Strip 2016-04-06 2026-06-30
79 TD Banknorth Garden Boston Massachusetts West End 2005-07-01 2009-07-15
80 TD Garden Boston Massachusetts West End 2009-07-16 2026-06-30
81 TD Waterhouse Centre Orlando Florida Downtown 1999-10-01 2002-12-31
82 Talking Stick Resort Arena Phoenix Arizona Downtown 2015-09-01 2020-10-31
83 Target Center Minneapolis Minnesota Warehouse District/North Loop 1990-10-16 2026-06-30
84 The Arena in Oakland Oakland California Coliseum Industrial 1997-10-01 2004-12-31
85 The O2 Arena London United Kingdom Greenwich Peninsula 2007-06-24 2026-06-30
86 The Palace of Auburn Hills Auburn Hills Michigan Auburn Hills (I-75 corridor) 1988-10-01 2017-10-12
87 Time Warner Cable Arena Charlotte North Carolina Uptown (First Ward) 2008-08-01 2016-08-16
88 Toyota Center Houston Texas Downtown/East Downtown 2003-10-06 2026-06-30
89 US Airways Center Phoenix Arizona Downtown 2006-10-01 2015-08-31
90 UW–Milwaukee Panther Arena Milwaukee Wisconsin Downtown 1950-08-04 2026-06-30
91 United Center Chicago Illinois Near West Side 1994-08-18 2026-06-30
92 Verizon Center Washington District of Columbia Chinatown/Penn Quarter 2006-03-01 2017-08-08
93 Vivint Arena Salt Lake City Utah West Downtown 2020-01-01 2023-06-30
94 Vivint Smart Home Arena Salt Lake City Utah West Downtown 2015-10-26 2019-12-31
95 Wachovia Center Philadelphia Pennsylvania South Philadelphia Sports Complex 2003-07-01 2010-06-30
96 Wells Fargo Center Philadelphia Pennsylvania South Philadelphia Sports Complex 2010-07-01 2025-08-31
97 Xfinity Mobile Arena Philadelphia Pennsylvania South Philadelphia Sports Complex 2025-09-01 2026-06-30
@@ -0,0 +1,41 @@
source_abbr,team_abbr
ATL,ATL
BOS,BOS
BKN,BKN
NJN,BKN
BRK,BKN
CHA,CHA
CHH,CHA
CHO,CHA
CHI,CHI
CLE,CLE
DAL,DAL
DEN,DEN
DET,DET
GSW,GSW
HOU,HOU
IND,IND
LAC,LAC
LAL,LAL
MEM,MEM
MIA,MIA
MIL,MIL
MIN,MIN
NOP,NOP
NOH,NOP
NOK,NOP
NYK,NYK
OKC,OKC
SEA,OKC
ORL,ORL
PHI,PHI
PHX,PHX
PHO,PHX
POR,POR
SAC,SAC
SAS,SAS
TOR,TOR
UTA,UTA
WAS,WAS
VAN,MEM
SDC,LAC
1 source_abbr team_abbr
2 ATL ATL
3 BOS BOS
4 BKN BKN
5 NJN BKN
6 BRK BKN
7 CHA CHA
8 CHH CHA
9 CHO CHA
10 CHI CHI
11 CLE CLE
12 DAL DAL
13 DEN DEN
14 DET DET
15 GSW GSW
16 HOU HOU
17 IND IND
18 LAC LAC
19 LAL LAL
20 MEM MEM
21 MIA MIA
22 MIL MIL
23 MIN MIN
24 NOP NOP
25 NOH NOP
26 NOK NOP
27 NYK NYK
28 OKC OKC
29 SEA OKC
30 ORL ORL
31 PHI PHI
32 PHX PHX
33 PHO PHX
34 POR POR
35 SAC SAC
36 SAS SAS
37 TOR TOR
38 UTA UTA
39 WAS WAS
40 VAN MEM
41 SDC LAC
+36
View File
@@ -0,0 +1,36 @@
team_abbr,full_name
ATL,Atlanta Hawks
BOS,Boston Celtics
BKN,Brooklyn Nets
BKN,New Jersey Nets
CHA,Charlotte Hornets
CHA,Charlotte Bobcats
CHI,Chicago Bulls
CLE,Cleveland Cavaliers
DAL,Dallas Mavericks
DEN,Denver Nuggets
DET,Detroit Pistons
GSW,Golden State Warriors
HOU,Houston Rockets
IND,Indiana Pacers
LAC,Los Angeles Clippers
LAL,Los Angeles Lakers
MEM,Memphis Grizzlies
MIA,Miami Heat
MIL,Milwaukee Bucks
MIN,Minnesota Timberwolves
NOP,New Orleans Pelicans
NOP,New Orleans Hornets
NOP,New Orleans/Oklahoma City Hornets
NYK,New York Knicks
OKC,Oklahoma City Thunder
OKC,Seattle SuperSonics
ORL,Orlando Magic
PHI,Philadelphia 76ers
PHX,Phoenix Suns
POR,Portland Trail Blazers
SAC,Sacramento Kings
SAS,San Antonio Spurs
TOR,Toronto Raptors
UTA,Utah Jazz
WAS,Washington Wizards
1 team_abbr full_name
2 ATL Atlanta Hawks
3 BOS Boston Celtics
4 BKN Brooklyn Nets
5 BKN New Jersey Nets
6 CHA Charlotte Hornets
7 CHA Charlotte Bobcats
8 CHI Chicago Bulls
9 CLE Cleveland Cavaliers
10 DAL Dallas Mavericks
11 DEN Denver Nuggets
12 DET Detroit Pistons
13 GSW Golden State Warriors
14 HOU Houston Rockets
15 IND Indiana Pacers
16 LAC Los Angeles Clippers
17 LAL Los Angeles Lakers
18 MEM Memphis Grizzlies
19 MIA Miami Heat
20 MIL Milwaukee Bucks
21 MIN Minnesota Timberwolves
22 NOP New Orleans Pelicans
23 NOP New Orleans Hornets
24 NOP New Orleans/Oklahoma City Hornets
25 NYK New York Knicks
26 OKC Oklahoma City Thunder
27 OKC Seattle SuperSonics
28 ORL Orlando Magic
29 PHI Philadelphia 76ers
30 PHX Phoenix Suns
31 POR Portland Trail Blazers
32 SAC Sacramento Kings
33 SAS San Antonio Spurs
34 TOR Toronto Raptors
35 UTA Utah Jazz
36 WAS Washington Wizards
+39
View File
@@ -0,0 +1,39 @@
team_abbr,full_name,start_year,end_year,is_current,source,notes
ATL,Atlanta Hawks,1968,2026,TRUE,all,No changes since 1968
BOS,Boston Celtics,1946,2026,TRUE,all,No changes since 1946
BRK,Brooklyn Nets,2012,2026,TRUE,bref,Basketball-Reference abbreviation for Brooklyn
CHA,Charlotte Bobcats,2004,2014,FALSE,bref,Renamed to Hornets in 2014; ESPN/NBA.com also use CHA for current Hornets
CHH,Charlotte Hornets,1988,2002,FALSE,bref,Original Hornets; relocated to New Orleans after 2001-02
CHI,Chicago Bulls,1966,2026,TRUE,all,No changes since 1966
CHO,Charlotte Hornets,2014,2026,TRUE,bref,Basketball-Reference abbreviation; reclaimed original Hornets history
CLE,Cleveland Cavaliers,1970,2026,TRUE,all,No changes since 1970
DAL,Dallas Mavericks,1980,2026,TRUE,all,Expansion team 1980-81 season
DEN,Denver Nuggets,1976,2026,TRUE,all,Joined NBA in 1976 from ABA
DET,Detroit Pistons,1957,2026,TRUE,all,In Detroit since 1957
GSW,Golden State Warriors,1971,2026,TRUE,all,Name changed from San Francisco Warriors in 1971
HOU,Houston Rockets,1971,2026,TRUE,all,Moved from San Diego in 1971
IND,Indiana Pacers,1976,2026,TRUE,all,Joined NBA in 1976 from ABA
LAC,Los Angeles Clippers,1984,2026,TRUE,all,Relocated from San Diego
LAL,Los Angeles Lakers,1960,2026,TRUE,all,Moved from Minneapolis in 1960
MEM,Memphis Grizzlies,2001,2026,TRUE,all,Relocated from Vancouver
MIA,Miami Heat,1988,2026,TRUE,all,Expansion team 1988-89 season
MIL,Milwaukee Bucks,1968,2026,TRUE,all,No changes since 1968
MIN,Minnesota Timberwolves,1989,2026,TRUE,all,Expansion team 1989-90 season
NJN,New Jersey Nets,1977,2012,FALSE,all,Moved to Brooklyn after 2011-12 season
NOH,New Orleans Hornets,2002,2005,FALSE,bref,Relocated from Charlotte; excludes NOK years; renamed Pelicans in 2013
NOK,New Orleans/Oklahoma City Hornets,2005,2007,FALSE,bref,Hurricane Katrina temporary relocation; split home games
NOP,New Orleans Pelicans,2013,2026,TRUE,all,Renamed from Hornets
NYK,New York Knicks,1946,2026,TRUE,all,No changes since 1946
OKC,Oklahoma City Thunder,2008,2026,TRUE,all,Relocated from Seattle
ORL,Orlando Magic,1989,2026,TRUE,all,Expansion team 1989-90 season
PHI,Philadelphia 76ers,1963,2026,TRUE,all,Moved from Syracuse in 1963
PHO,Phoenix Suns,1968,2026,TRUE,bref,Basketball-Reference abbreviation
POR,Portland Trail Blazers,1970,2026,TRUE,all,No changes since 1970
SAC,Sacramento Kings,1985,2026,TRUE,all,Relocated from Kansas City
SAS,San Antonio Spurs,1976,2026,TRUE,all,Joined NBA in 1976 from ABA
SDC,San Diego Clippers,1978,1984,FALSE,bref,Moved to Los Angeles after 1983-84 season
SEA,Seattle SuperSonics,1967,2008,FALSE,all,Relocated to Oklahoma City after 2007-08 season
TOR,Toronto Raptors,1995,2026,TRUE,all,Expansion team 1995-96 season
UTA,Utah Jazz,1979,2026,TRUE,all,Moved from New Orleans in 1979
VAN,Vancouver Grizzlies,1995,2001,FALSE,all,Expansion team 1995-96; relocated to Memphis after 2000-01
WAS,Washington Wizards,1997,2026,TRUE,all,Renamed from Bullets
1 team_abbr full_name start_year end_year is_current source notes
2 ATL Atlanta Hawks 1968 2026 TRUE all No changes since 1968
3 BOS Boston Celtics 1946 2026 TRUE all No changes since 1946
4 BRK Brooklyn Nets 2012 2026 TRUE bref Basketball-Reference abbreviation for Brooklyn
5 CHA Charlotte Bobcats 2004 2014 FALSE bref Renamed to Hornets in 2014; ESPN/NBA.com also use CHA for current Hornets
6 CHH Charlotte Hornets 1988 2002 FALSE bref Original Hornets; relocated to New Orleans after 2001-02
7 CHI Chicago Bulls 1966 2026 TRUE all No changes since 1966
8 CHO Charlotte Hornets 2014 2026 TRUE bref Basketball-Reference abbreviation; reclaimed original Hornets history
9 CLE Cleveland Cavaliers 1970 2026 TRUE all No changes since 1970
10 DAL Dallas Mavericks 1980 2026 TRUE all Expansion team 1980-81 season
11 DEN Denver Nuggets 1976 2026 TRUE all Joined NBA in 1976 from ABA
12 DET Detroit Pistons 1957 2026 TRUE all In Detroit since 1957
13 GSW Golden State Warriors 1971 2026 TRUE all Name changed from San Francisco Warriors in 1971
14 HOU Houston Rockets 1971 2026 TRUE all Moved from San Diego in 1971
15 IND Indiana Pacers 1976 2026 TRUE all Joined NBA in 1976 from ABA
16 LAC Los Angeles Clippers 1984 2026 TRUE all Relocated from San Diego
17 LAL Los Angeles Lakers 1960 2026 TRUE all Moved from Minneapolis in 1960
18 MEM Memphis Grizzlies 2001 2026 TRUE all Relocated from Vancouver
19 MIA Miami Heat 1988 2026 TRUE all Expansion team 1988-89 season
20 MIL Milwaukee Bucks 1968 2026 TRUE all No changes since 1968
21 MIN Minnesota Timberwolves 1989 2026 TRUE all Expansion team 1989-90 season
22 NJN New Jersey Nets 1977 2012 FALSE all Moved to Brooklyn after 2011-12 season
23 NOH New Orleans Hornets 2002 2005 FALSE bref Relocated from Charlotte; excludes NOK years; renamed Pelicans in 2013
24 NOK New Orleans/Oklahoma City Hornets 2005 2007 FALSE bref Hurricane Katrina temporary relocation; split home games
25 NOP New Orleans Pelicans 2013 2026 TRUE all Renamed from Hornets
26 NYK New York Knicks 1946 2026 TRUE all No changes since 1946
27 OKC Oklahoma City Thunder 2008 2026 TRUE all Relocated from Seattle
28 ORL Orlando Magic 1989 2026 TRUE all Expansion team 1989-90 season
29 PHI Philadelphia 76ers 1963 2026 TRUE all Moved from Syracuse in 1963
30 PHO Phoenix Suns 1968 2026 TRUE bref Basketball-Reference abbreviation
31 POR Portland Trail Blazers 1970 2026 TRUE all No changes since 1970
32 SAC Sacramento Kings 1985 2026 TRUE all Relocated from Kansas City
33 SAS San Antonio Spurs 1976 2026 TRUE all Joined NBA in 1976 from ABA
34 SDC San Diego Clippers 1978 1984 FALSE bref Moved to Los Angeles after 1983-84 season
35 SEA Seattle SuperSonics 1967 2008 FALSE all Relocated to Oklahoma City after 2007-08 season
36 TOR Toronto Raptors 1995 2026 TRUE all Expansion team 1995-96 season
37 UTA Utah Jazz 1979 2026 TRUE all Moved from New Orleans in 1979
38 VAN Vancouver Grizzlies 1995 2001 FALSE all Expansion team 1995-96; relocated to Memphis after 2000-01
39 WAS Washington Wizards 1997 2026 TRUE all Renamed from Bullets
+16
View File
@@ -0,0 +1,16 @@
selectors:
- name: nba_pipeline
description: "Executes the full pipeline: Staging -> Intermediate -> Dimensions -> Facts"
default: false
definition:
union:
- method: tag
value: staging
- method: tag
value: intermediate
- method: tag
value: dimension
- method: tag
value: fact
- method: tag
value: marts
View File
View File
+13
View File
@@ -0,0 +1,13 @@
[project]
name = "dbt-nba"
version = "1.0.0"
requires-python = ">=3.11,<3.13"
dependencies = [
"dbt-duckdb",
"streamlit>=1.45.1",
"duckdb>=1.3.0",
"plotly>=6.1.2",
"streamlit-echarts>=0.4.0",
"boto3>=1.34.0",
"python-dotenv>=1.0.0",
]
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
S3/Cloudflare R2 Object Storage utility for DuckDB database management.
Supports:
- Cloudflare R2 (recommended: zero egress fees)
- Hetzner S3 Object Storage
- SeaweedFS / MinIO / AWS S3
Commands:
python scripts/db_storage.py status
python scripts/db_storage.py upload [--file PATH] [--key KEY]
python scripts/db_storage.py download [--force] [--key KEY]
"""
import argparse
import hashlib
import os
import shutil
import sys
import time
from pathlib import Path
try:
from dotenv import load_dotenv
# Load .env file from repo root
repo_root = Path(__file__).resolve().parent.parent
load_dotenv(repo_root / ".env")
except ImportError:
repo_root = Path(__file__).resolve().parent.parent
try:
import boto3
from boto3.s3.transfer import TransferConfig
from botocore.client import Config
from botocore.exceptions import ClientError
except ImportError:
print("ERROR: boto3 is not installed. Please run `uv sync` to install dependencies.", file=sys.stderr)
sys.exit(1)
# Default Paths
DEFAULT_LOCAL_DB = repo_root / "data" / "DB" / "dbt_nba.duckdb"
DEFAULT_REPORTS_DB = repo_root / "dbt_nba" / "reports" / "sources" / "nba" / "dbt_nba.duckdb"
# Tuned TransferConfig for resilient large multipart transfers over HTTPS
TRANSFER_CONFIG = TransferConfig(
multipart_threshold=32 * 1024 * 1024, # 32 MB
max_concurrency=4, # 4 concurrent threads
multipart_chunksize=32 * 1024 * 1024, # 32 MB per chunk
use_threads=True,
num_download_attempts=5,
)
def get_s3_config():
"""Retrieve S3/R2 configuration from environment variables."""
endpoint_url = os.getenv("S3_ENDPOINT_URL") or os.getenv("R2_ENDPOINT_URL")
bucket_name = os.getenv("S3_BUCKET_NAME") or os.getenv("R2_BUCKET_NAME")
access_key = os.getenv("S3_ACCESS_KEY_ID") or os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("R2_ACCESS_KEY_ID")
secret_key = os.getenv("S3_SECRET_ACCESS_KEY") or os.getenv("AWS_SECRET_ACCESS_KEY") or os.getenv("R2_SECRET_ACCESS_KEY")
region_name = os.getenv("S3_REGION", "auto")
db_key = os.getenv("S3_DB_KEY", "dbt_nba.duckdb")
missing = []
if not endpoint_url:
missing.append("S3_ENDPOINT_URL")
if not bucket_name:
missing.append("S3_BUCKET_NAME")
if not access_key:
missing.append("S3_ACCESS_KEY_ID")
if not secret_key:
missing.append("S3_SECRET_ACCESS_KEY")
if missing:
print(f"ERROR: Missing required environment variables in .env: {', '.join(missing)}", file=sys.stderr)
print("Please check .env.example for guidance.", file=sys.stderr)
sys.exit(1)
return {
"endpoint_url": endpoint_url,
"bucket_name": bucket_name,
"access_key": access_key,
"secret_key": secret_key,
"region_name": region_name,
"db_key": db_key,
}
def get_s3_client(config):
"""Create a boto3 S3 client configured for custom S3/R2 endpoints."""
return boto3.client(
"s3",
endpoint_url=config["endpoint_url"],
aws_access_key_id=config["access_key"],
aws_secret_access_key=config["secret_key"],
region_name=config["region_name"],
config=Config(
signature_version="s3v4",
s3={"addressing_style": "path"},
retries={"max_attempts": 5, "mode": "adaptive"},
),
)
class ProgressPercentage:
"""Console progress tracker for S3 file transfer."""
def __init__(self, filename, total_size, action="Transferring"):
self._filename = filename
self._total_size = float(total_size) if total_size > 0 else 1.0
self._seen_so_far = 0
self._start_time = time.time()
self._action = action
def __call__(self, bytes_amount):
self._seen_so_far += bytes_amount
percentage = (self._seen_so_far / self._total_size) * 100
elapsed = max(time.time() - self._start_time, 0.001)
speed_mb = (self._seen_so_far / (1024 * 1024)) / elapsed
mb_seen = self._seen_so_far / (1024 * 1024)
mb_total = self._total_size / (1024 * 1024)
sys.stdout.write(
f"\r{self._action} {Path(self._filename).name}: {mb_seen:.1f}/{mb_total:.1f} MB "
f"({percentage:5.1f}%) @ {speed_mb:5.1f} MB/s"
)
sys.stdout.flush()
if self._seen_so_far >= self._total_size:
sys.stdout.write("\n")
sys.stdout.flush()
def compute_file_md5(filepath: Path) -> str:
"""Compute hex MD5 hash of a local file."""
hash_md5 = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def upload_db(file_path: Path = None, key: str = None):
"""Upload DuckDB file to S3/R2 with auto-retry."""
config = get_s3_config()
target_file = Path(file_path) if file_path else DEFAULT_LOCAL_DB
object_key = key or config["db_key"]
if not target_file.exists():
print(f"ERROR: Local database file not found at {target_file}", file=sys.stderr)
sys.exit(1)
file_size = target_file.stat().st_size
print(f"==> Uploading {target_file} ({file_size / (1024*1024):.2f} MB) to s3://{config['bucket_name']}/{object_key}...")
s3 = get_s3_client(config)
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
progress = ProgressPercentage(str(target_file), file_size, action="Uploading")
s3.upload_file(
Filename=str(target_file),
Bucket=config["bucket_name"],
Key=object_key,
Config=TRANSFER_CONFIG,
Callback=progress,
)
print(f"✓ Successfully uploaded {object_key} to {config['bucket_name']}")
return
except Exception as e:
if attempt < max_retries:
print(f"\n[Attempt {attempt}/{max_retries}] Upload encountered error ({e}). Retrying in 3s...", file=sys.stderr)
time.sleep(3)
else:
print(f"\nERROR: Failed to upload to S3 after {max_retries} attempts: {e}", file=sys.stderr)
sys.exit(1)
def download_db(force: bool = False, key: str = None):
"""Download DuckDB file from S3/R2 to local destinations with auto-retry."""
config = get_s3_config()
object_key = key or config["db_key"]
s3 = get_s3_client(config)
print(f"==> Checking remote object s3://{config['bucket_name']}/{object_key}...")
try:
head = s3.head_object(Bucket=config["bucket_name"], Key=object_key)
except ClientError as e:
print(f"ERROR: Could not find or access remote object {object_key} in {config['bucket_name']}: {e}", file=sys.stderr)
sys.exit(1)
remote_size = head.get("ContentLength", 0)
last_modified = head.get("LastModified")
print(f" Remote size: {remote_size / (1024*1024):.2f} MB | Last modified: {last_modified}")
# Check if local file is already identical
if not force and DEFAULT_LOCAL_DB.exists() and DEFAULT_LOCAL_DB.stat().st_size == remote_size:
# Check if reports db also exists
if DEFAULT_REPORTS_DB.exists() and DEFAULT_REPORTS_DB.stat().st_size == remote_size:
print("✓ Local database is already up-to-date. Skipping download (use --force to re-download).")
return
# Ensure parent directories exist
DEFAULT_LOCAL_DB.parent.mkdir(parents=True, exist_ok=True)
DEFAULT_REPORTS_DB.parent.mkdir(parents=True, exist_ok=True)
temp_file = DEFAULT_LOCAL_DB.with_suffix(".duckdb.tmp")
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
progress = ProgressPercentage(str(DEFAULT_LOCAL_DB), remote_size, action="Downloading")
s3.download_file(
Bucket=config["bucket_name"],
Key=object_key,
Filename=str(temp_file),
Config=TRANSFER_CONFIG,
Callback=progress,
)
# Atomically move temp file to main db path
shutil.move(str(temp_file), str(DEFAULT_LOCAL_DB))
# Copy to reports location as well
shutil.copy2(str(DEFAULT_LOCAL_DB), str(DEFAULT_REPORTS_DB))
print(f"✓ Downloaded database to:\n - {DEFAULT_LOCAL_DB}\n - {DEFAULT_REPORTS_DB}")
return
except Exception as e:
if temp_file.exists():
temp_file.unlink()
if attempt < max_retries:
print(f"\n[Attempt {attempt}/{max_retries}] Download encountered error ({e}). Retrying in 3s...", file=sys.stderr)
time.sleep(3)
else:
print(f"\nERROR: Failed to download from S3 after {max_retries} attempts: {e}", file=sys.stderr)
sys.exit(1)
def status_db():
"""Show status of remote and local databases."""
config = get_s3_config()
object_key = config["db_key"]
s3 = get_s3_client(config)
print("=" * 60)
print("Database Storage Status (Cloudflare R2 / S3)")
print("=" * 60)
print(f"Endpoint: {config['endpoint_url']}")
print(f"Bucket: {config['bucket_name']}")
print(f"DB Key: {object_key}")
print("-" * 60)
try:
head = s3.head_object(Bucket=config["bucket_name"], Key=object_key)
remote_size = head.get("ContentLength", 0)
last_modified = head.get("LastModified")
print(f"Remote DB: Exists ({remote_size / (1024*1024):.2f} MB)")
print(f" Last Modified: {last_modified}")
except ClientError as e:
print(f"Remote DB: NOT FOUND ({e})")
print("-" * 60)
if DEFAULT_LOCAL_DB.exists():
print(f"Local DB (Data): Exists ({DEFAULT_LOCAL_DB.stat().st_size / (1024*1024):.2f} MB) -> {DEFAULT_LOCAL_DB}")
else:
print(f"Local DB (Data): Missing -> {DEFAULT_LOCAL_DB}")
if DEFAULT_REPORTS_DB.exists():
print(f"Local DB (Reports): Exists ({DEFAULT_REPORTS_DB.stat().st_size / (1024*1024):.2f} MB) -> {DEFAULT_REPORTS_DB}")
else:
print(f"Local DB (Reports): Missing -> {DEFAULT_REPORTS_DB}")
print("=" * 60)
def main():
parser = argparse.ArgumentParser(description="Manage NBA DuckDB sync with Cloudflare R2 / S3.")
subparsers = parser.add_subparsers(dest="command", required=True)
# status
subparsers.add_parser("status", help="Check remote and local database status")
# upload
upload_parser = subparsers.add_parser("upload", help="Upload local DuckDB to S3/R2")
upload_parser.add_argument("--file", help="Path to local .duckdb file (default: data/DB/dbt_nba.duckdb)")
upload_parser.add_argument("--key", help="Remote object key (default: from env or dbt_nba.duckdb)")
# download
download_parser = subparsers.add_parser("download", help="Download DuckDB from S3/R2")
download_parser.add_argument("--force", action="store_true", help="Force download even if local file matches")
download_parser.add_argument("--key", help="Remote object key (default: from env or dbt_nba.duckdb)")
args = parser.parse_args()
if args.command == "status":
status_db()
elif args.command == "upload":
upload_db(file_path=args.file, key=args.key)
elif args.command == "download":
download_db(force=args.force, key=args.key)
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
-- Extract source tables from Postgres into DuckDB
-- Requires: POSTGRES_URL environment variable
-- Usage: Run via scripts/pipeline.sh (handles variable substitution)
INSTALL postgres;
LOAD postgres;
ATTACH '${POSTGRES_URL}' AS pg (TYPE POSTGRES, READ_ONLY);
DROP TABLE IF EXISTS main.games;
DROP TABLE IF EXISTS main.line_scores;
DROP TABLE IF EXISTS main.player_game_basic_stats;
DROP TABLE IF EXISTS main.player_game_adv_stats;
DROP TABLE IF EXISTS main.player_shot_charts;
DROP TABLE IF EXISTS main.team_game_basic_stats;
DROP TABLE IF EXISTS main.team_game_adv_stats;
CREATE TABLE main.games AS SELECT * FROM pg.public.games;
CREATE TABLE main.line_scores AS SELECT * FROM pg.public.line_scores;
CREATE TABLE main.player_game_basic_stats AS SELECT * FROM pg.public.player_game_basic_stats;
CREATE TABLE main.player_game_adv_stats AS SELECT * FROM pg.public.player_game_adv_stats;
CREATE TABLE main.player_shot_charts AS SELECT * FROM pg.public.player_shot_charts;
CREATE TABLE main.team_game_basic_stats AS SELECT * FROM pg.public.team_game_basic_stats;
CREATE TABLE main.team_game_adv_stats AS SELECT * FROM pg.public.team_game_adv_stats;
DETACH pg;
SELECT 'Extraction complete' AS status;
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DB_PATH="$REPO_ROOT/data/DB/dbt_nba.duckdb"
REPORTS_DB="$REPO_ROOT/dbt_nba/reports/sources/nba/dbt_nba.duckdb"
# Load .env if it exists
if [ -f "$REPO_ROOT/.env" ]; then
set -a
source "$REPO_ROOT/.env"
set +a
fi
# Check prerequisites
if [ -z "${POSTGRES_URL:-}" ]; then
echo "ERROR: POSTGRES_URL environment variable is not set" >&2
echo "Please export POSTGRES_URL or configure it in $REPO_ROOT/.env" >&2
exit 1
fi
echo "==> [1/4] Extracting from Postgres into DuckDB..."
mkdir -p "$(dirname "$DB_PATH")"
mkdir -p "$(dirname "$REPORTS_DB")"
rm -f "$DB_PATH"
envsubst < "$REPO_ROOT/scripts/extract.sql" | duckdb "$DB_PATH"
echo "==> [2/4] Running dbt build --full-refresh..."
export DBT_PROJECT_DIR="$REPO_ROOT/dbt_nba"
export DBT_PROFILES_DIR="$REPO_ROOT/dbt_nba"
export DBT_DUCKDB_PATH="$DB_PATH"
uv run --project "$REPO_ROOT" dbt deps --project-dir "$DBT_PROJECT_DIR" --profiles-dir "$DBT_PROFILES_DIR"
uv run --project "$REPO_ROOT" dbt build --full-refresh --project-dir "$DBT_PROJECT_DIR" --profiles-dir "$DBT_PROFILES_DIR"
echo "==> [3/4] Copying database to reports directory..."
cp "$DB_PATH" "$REPORTS_DB"
echo "==> [4/4] Syncing database artifact to Cloudflare R2 / S3..."
if [ -n "${S3_ENDPOINT_URL:-}" ] || [ -n "${R2_ENDPOINT_URL:-}" ]; then
uv run --project "$REPO_ROOT" python "$REPO_ROOT/scripts/db_storage.py" upload
echo "==> Cloudflare R2 upload complete!"
else
echo "==> S3_ENDPOINT_URL not set; skipping remote upload (local DB ready)."
fi
echo "==> Pipeline complete successfully!"
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
REPORTS_DB="$REPO_ROOT/dbt_nba/reports/sources/nba/dbt_nba.duckdb"
DATA_DB="$REPO_ROOT/data/DB/dbt_nba.duckdb"
# Load .env if present
if [ -f "$REPO_ROOT/.env" ]; then
set -a
source "$REPO_ROOT/.env"
set +a
fi
FORCE_SYNC=0
for arg in "$@"; do
if [ "$arg" == "--sync" ] || [ "$arg" == "-s" ]; then
FORCE_SYNC=1
fi
done
# Check if database is missing or sync requested
if [ ! -f "$REPORTS_DB" ] || [ "$FORCE_SYNC" -eq 1 ]; then
if [ -f "$DATA_DB" ] && [ "$FORCE_SYNC" -eq 0 ]; then
echo "==> Copying local database to reports path..."
mkdir -p "$(dirname "$REPORTS_DB")"
cp "$DATA_DB" "$REPORTS_DB"
elif [ -n "${S3_ENDPOINT_URL:-}" ] || [ -n "${R2_ENDPOINT_URL:-}" ]; then
echo "==> Local database missing or --sync requested; downloading latest from Cloudflare R2 / S3..."
uv run --project "$REPO_ROOT" python "$REPO_ROOT/scripts/db_storage.py" download
else
echo "ERROR: Local database file not found at $REPORTS_DB." >&2
echo "Please either:" >&2
echo " 1. Run ./scripts/pipeline.sh to extract and build from Postgres, or" >&2
echo " 2. Configure R2 credentials in .env and run: uv run python scripts/db_storage.py download" >&2
exit 1
fi
fi
echo "==> Starting Streamlit NBA Analytics Dashboard..."
uv run --project "$REPO_ROOT" streamlit run "$REPO_ROOT/streamlit_app/app.py"
+8
View File
@@ -0,0 +1,8 @@
[theme]
primaryColor = "#3b82f6"
backgroundColor = "#0e1117"
secondaryBackgroundColor = "#1e293b"
textColor = "#f8fafc"
[server]
maxUploadSize = 5
+1391
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
streamlit>=1.55.0
duckdb==1.3.0
plotly==6.1.2
streamlit-echarts>=0.4.0
Generated
+1438
View File
File diff suppressed because it is too large Load Diff