@@ -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
|
||||
Reference in New Issue
Block a user