This exercise builds Stage 3 of the capstone pipeline: the dbt-style model hierarchy with tests and Semantic Layer metrics. Building on the star schema and fact table from Lesson 32, you will implement staging and intermediate transformation layers, build the Gold mart with bowler season statistics, apply dbt schema test equivalents, and implement the ratio-correct economy_rate metric verified against the averaging error. The exercise also simulates dbt documentation generation by producing a model metadata manifest.
The exercise covers two steps. Step 1 builds the staging and intermediate transformation layers from the fact table created in Lesson 32, joins with the SCD Type 2 player dimension using the version-specific surrogate key, and runs schema tests for not_null, unique, and accepted_values. Step 2 builds the Gold mart with the economy_rate metric, runs the averaging-error correctness test, and generates a simulated dbt manifest documenting the model lineage.
Step 1 — Staging, Intermediate Layers and Schema Tests
Build the staging model that joins `fact_ipl_delivery` with `dim_player` using the surrogate `bowler_key` to enrich with `player_name` and `bowling_style`, build the intermediate model that joins with `dim_match` and `dim_venue` for full context, and apply schema tests: not_null on `delivery_key`, unique on `delivery_key`, accepted_values on `phase`, and a relationship test verifying every `bowler_key` exists in `dim_player`. Assert all tests pass and that row count is preserved across both transformation layers.
# capstone_dbt_models.py — Step 1: Staging, intermediate, schema tests
import duckdb
import pandas as pd
# Reuses con, fact_ipl_delivery, dim_player, dim_match, dim_venue from Lesson 32
# ── Staging model: join fact with player dimension ────────────────────────────
stg_sql = """
SELECT
f.delivery_key, f.match_key, f.venue_key, f.bowler_key, f.batting_team_key,
f.over_number, f.phase, f.runs_scored, f.is_wicket,
p.player_name AS bowler_name, p.bowling_style, p.current_team
FROM fact_ipl_delivery f
LEFT JOIN dim_player p ON f.bowler_key = p.player_key
"""
stg_df = con.execute(stg_sql).df()
con.register("stg_delivery_enriched", stg_df)
# ── Intermediate model: join with match and venue ─────────────────────────────
int_sql = """
SELECT
s.*, m.season, m.match_date, v.venue_name, v.city
FROM stg_delivery_enriched s
LEFT JOIN dim_match m ON s.match_key = m.match_key
LEFT JOIN dim_venue v ON s.venue_key = v.venue_key
"""
int_df = con.execute(int_sql).df()
con.register("int_delivery_full", int_df)
# ── Schema tests ────────────────────────────────────────────────────────────────
def test_not_null(df, col): return df[col].notna().all()
def test_unique(df, col): return not df[col].duplicated().any()
def test_accepted_values(df, col, values): return set(df[col].unique()).issubset(values)
def test_relationship(child_df, child_col, parent_df, parent_col):
return set(child_df[child_col]).issubset(set(parent_df[parent_col]))
tests = {
"not_null:delivery_key": test_not_null(int_df, "delivery_key"),
"unique:delivery_key": test_unique(int_df, "delivery_key"),
"accepted_values:phase": test_accepted_values(int_df, "phase", {"powerplay","middle","death"}),
"relationship:bowler_key": test_relationship(int_df, "bowler_key",
con.execute("SELECT * FROM dim_player").df(), "player_key"),
"not_null:bowler_name": test_not_null(int_df, "bowler_name"),
"not_null:venue_name": test_not_null(int_df, "venue_name"),
}
print("dbt schema test results:")
all_pass = True
for name, result in tests.items():
print(f" {'✓' if result else '✗'} {name}")
if not result: all_pass = False
assert all_pass
# Row conservation across layers
fact_count = con.execute("SELECT COUNT(*) FROM fact_ipl_delivery").fetchone()[0]
assert len(stg_df) == fact_count
assert len(int_df) == fact_count
print(f"\nRow conservation: fact={fact_count} = stg={len(stg_df)} = int={len(int_df)} ✓")
print("Step 1 ✓: Staging, intermediate, and schema tests complete")Step 2 — Gold Mart, Semantic Layer Metric and Manifest
Build the Gold mart with bowler season statistics, compute the economy_rate metric using the ratio-correct pattern (aggregated numerator and denominator), verify the averaging error by comparing against a naive average-of-per-match-rates computation, and assert the two methods produce different results. Generate a simulated dbt manifest documenting the model dependency graph from staging through to the Gold mart, and verify the manifest correctly identifies all upstream dependencies for the Gold mart model.
# capstone_dbt_models.py — Step 2: Gold mart, Semantic Layer, manifest
import pandas as pd
import numpy as np
np.random.seed(44)
# ── Gold mart: bowler season statistics ───────────────────────────────────────
gold_sql = """
SELECT
bowler_name,
season,
COUNT(*) AS deliveries,
SUM(runs_scored) AS runs_conceded,
SUM(CAST(is_wicket AS INTEGER)) AS wickets,
ROUND(SUM(runs_scored)/NULLIF(COUNT(*)/6.0,0),2) AS economy_rate
FROM int_delivery_full
GROUP BY bowler_name, season
ORDER BY economy_rate
"""
gold_df = con.execute(gold_sql).df()
print("Gold mart — bowler season stats:")
print(gold_df.to_string(index=False))
# Delivery conservation
assert int(gold_df["deliveries"].sum()) == len(int_df)
print(f"\nGold conservation: {int(gold_df['deliveries'].sum())} == {len(int_df)} ✓")
# ── Semantic Layer ratio-correctness test ─────────────────────────────────────
# Generate per-match stats with unequal delivery counts to test correctness
matches_data = []
for mid in [10001, 10002]:
for bowler in ["Hardik Pandya"]:
n = np.random.randint(15, 30)
runs = np.random.randint(15, 40)
matches_data.append({"bowler": bowler, "match_id": mid, "deliveries": n, "runs": runs})
mdf = pd.DataFrame(matches_data)
mdf["per_match_economy"] = (mdf["runs"]/(mdf["deliveries"]/6)).round(2)
con.register("match_econ", mdf)
correct_economy = con.execute(
"SELECT bowler, ROUND(SUM(runs)/NULLIF(SUM(deliveries)/6.0,0),2) AS correct FROM match_econ GROUP BY bowler"
).df()
incorrect_economy = con.execute(
"SELECT bowler, ROUND(AVG(per_match_economy),2) AS incorrect FROM match_econ GROUP BY bowler"
).df()
comparison = correct_economy.merge(incorrect_economy, on="bowler")
comparison["differs"] = comparison["correct"] != comparison["incorrect"]
print("\nSemantic Layer ratio correctness test:")
print(comparison.to_string(index=False))
assert comparison["differs"].any(), "Test data must produce different correct/incorrect results"
print("Ratio metric correctness verified ✓")
# ── dbt manifest simulation ────────────────────────────────────────────────────
MANIFEST = {
"nodes": {
"model.ipl_dw.stg_delivery_enriched": {
"depends_on": {"nodes": ["model.ipl_dw.fact_ipl_delivery", "model.ipl_dw.dim_player"]},
},
"model.ipl_dw.int_delivery_full": {
"depends_on": {"nodes": ["model.ipl_dw.stg_delivery_enriched",
"model.ipl_dw.dim_match", "model.ipl_dw.dim_venue"]},
},
"model.ipl_dw.mart_bowler_season_stats": {
"depends_on": {"nodes": ["model.ipl_dw.int_delivery_full"]},
},
}
}
def get_full_lineage(manifest: dict, model: str) -> set:
"""Recursively resolve all upstream dependencies."""
direct = set(manifest["nodes"].get(model, {}).get("depends_on", {}).get("nodes", []))
all_deps = set(direct)
for dep in direct:
all_deps |= get_full_lineage(manifest, dep)
return all_deps
lineage = get_full_lineage(MANIFEST, "model.ipl_dw.mart_bowler_season_stats")
print(f"\nGold mart full lineage: {sorted(lineage)}")
assert "model.ipl_dw.fact_ipl_delivery" in lineage
assert "model.ipl_dw.dim_player" in lineage
print("dbt manifest lineage resolution verified ✓")
print("Step 2 ✓: Gold mart, Semantic Layer, and manifest complete")