Merge pull request 'complete app' (#3) from dev into main
test / test (push) Successful in 6s
test / test (push) Successful in 6s
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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 |
@@ -0,0 +1,4 @@
|
||||
|
||||
target/
|
||||
dbt_packages/
|
||||
logs/
|
||||
@@ -0,0 +1 @@
|
||||
id: 428675f2-8539-4353-aae8-d37a4d91a7f7
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- name: dbt_utils
|
||||
package: dbt-labs/dbt_utils
|
||||
version: 1.3.1
|
||||
sha1_hash: 1b75844a5c14558fe9ae17335b3632c080d6784c
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- package: dbt-labs/dbt_utils
|
||||
version: 1.3.1
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.evidence/template
|
||||
.svelte-kit
|
||||
build
|
||||
node_modules
|
||||
.DS_Store
|
||||
static/data
|
||||
*.options.yaml
|
||||
.vscode/settings.json
|
||||
.env
|
||||
.evidence/meta
|
||||
@@ -0,0 +1,3 @@
|
||||
loglevel=error
|
||||
audit=false
|
||||
fund=false
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"evidence.evidence-vscode"
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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": { }
|
||||
Generated
+17662
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
UW–Milwaukee 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
|
||||
|
@@ -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
|
||||
UW–Milwaukee 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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
Executable
+302
@@ -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()
|
||||
@@ -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;
|
||||
Executable
+47
@@ -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!"
|
||||
Executable
+41
@@ -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"
|
||||
@@ -0,0 +1,8 @@
|
||||
[theme]
|
||||
primaryColor = "#3b82f6"
|
||||
backgroundColor = "#0e1117"
|
||||
secondaryBackgroundColor = "#1e293b"
|
||||
textColor = "#f8fafc"
|
||||
|
||||
[server]
|
||||
maxUploadSize = 5
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
streamlit>=1.55.0
|
||||
duckdb==1.3.0
|
||||
plotly==6.1.2
|
||||
streamlit-echarts>=0.4.0
|
||||
Reference in New Issue
Block a user