Week 3 — SQL Is the Source of Truth¶
Course: Applied ML Foundations for SaaS Analytics
Who this is for: Engineers who already write SELECT. Read this after Week 2. The CSV in data/ is a fixture. Production is a warehouse.
A model trained on a file someone emailed you is a demo. A model trained on as of midnight, this partition is a job.
🎯 What you will be able to do¶
- Treat CloudWave’s CSVs as tables you would query, not as “the data”
- Write the Customer 360 as SQL with a date bound
- Catch a grain bug with a test, the same way Week 2 caught an exploding join
- Know when to stay in SQL / DuckDB and when to come back to Pandas
Think of it like… the database is git. The CSV is a checkout.
You would not ship from a zip file on someone’s laptop. You ship from main. The warehouse is main. as_of is the commit you checked out. Retraining on a new CSV you cannot reproduce is training on a dirty working tree.
If you already write software¶
Your backend This week
──────────────────────── ──────────────────────────────
Postgres / Snowflake the warehouse (here: CSV + DuckDB)
dbt model a SELECT that has a grain
WHERE created_at < :as_of the time-machine rule, in SQL
unit test on a fixture row COUNT(*) after the join
ORM in the request path Pandas after the extract
Week 2 built Customer 360 in Pandas. That is the ORM. This week is the query that should have produced it.
Watch out — the CSV is a snapshot of all time
load_customer_360() sums every usage row in the file, including next quarter. Fine for learning verbs. Illegal for a model you will score on Tuesday. The extract must take an as_of.
Picture the extract¶
subscriptions feature_usage user_events
one row = one user one row = user×feat×day one row = one click
\ | /
\ | as_of = 2024-06-01 /
\ | (drop later rows) /
\ ▼ /
└────────► customer_360_as_of ◄────┘
one row = one user
tenure_so_far = as_of − signup
usage only through as_of
The same 360, in SQL¶
DuckDB reads the files as if they were warehouse tables. The SQL is what you would schedule.
import duckdb
from lib.course_data import find_data_dir
DATA = find_data_dir()
con = duckdb.connect()
con.execute(f"""
CREATE OR REPLACE VIEW subscriptions AS
SELECT * FROM read_csv_auto('{(DATA / "subscriptions.csv").as_posix()}');
CREATE OR REPLACE VIEW feature_usage AS
SELECT * FROM read_csv_auto('{(DATA / "feature_usage.csv").as_posix()}');
CREATE OR REPLACE VIEW user_events AS
SELECT * FROM read_csv_auto('{(DATA / "user_events.csv").as_posix()}');
""")
as_of = "2024-06-01"
sql_360 = f"""
WITH at_risk AS (
SELECT user_id, plan_type, mrr, signup_date,
datediff('day', signup_date, DATE '{as_of}') AS tenure_so_far
FROM subscriptions
WHERE signup_date <= DATE '{as_of}'
AND (churn_date IS NULL OR churn_date > DATE '{as_of}')
),
usage_cut AS (
SELECT user_id,
SUM(usage_count) AS total_usage,
COUNT(DISTINCT feature_name) AS features_adopted
FROM feature_usage
WHERE date <= DATE '{as_of}'
GROUP BY 1
),
events_cut AS (
SELECT user_id,
COUNT(*) AS total_events,
SUM(CASE WHEN event_type = 'support_message' THEN 1 ELSE 0 END) AS n_support
FROM user_events
WHERE timestamp <= TIMESTAMP '{as_of}'
GROUP BY 1
)
SELECT a.user_id, a.plan_type, a.mrr, a.tenure_so_far,
COALESCE(u.total_usage, 0) AS total_usage,
COALESCE(u.features_adopted, 0) AS features_adopted,
COALESCE(e.total_events, 0) AS total_events,
COALESCE(e.n_support, 0) AS n_support
FROM at_risk a
LEFT JOIN usage_cut u USING (user_id)
LEFT JOIN events_cut e USING (user_id)
"""
frame = con.execute(sql_360).df()
print(frame.shape, frame.columns.tolist())
print(frame.head(3))
That query is pipelines.features.build_features. Pandas is allowed after this. Pandas is not allowed to be the only copy of the grain rules.
Watch out — DATE vs TIMESTAMP
Usage date <= DATE '2024-06-01' includes all of June 1 (the column is a calendar day). Events timestamp <= TIMESTAMP '2024-06-01' is midnight on June 1 — later that day’s clicks are dropped. Same as_of string, two clocks.
Engineer mental model
One query, one grain, one as_of. If the warehouse team changes a column, the model job fails at extract — not three weeks later when CS notices the scores went weird. Put the SQL (or the Python that is the SQL) in git. Review it like an API.
Grain tests are unit tests¶
n_users = con.execute(f"""
SELECT COUNT(*) FROM subscriptions
WHERE signup_date <= DATE '{as_of}'
AND (churn_date IS NULL OR churn_date > DATE '{as_of}')
""").fetchone()[0]
assert len(frame) == n_users, "360 picked up extra rows — you joined the many-side raw"
assert frame["user_id"].is_unique
assert (frame["tenure_so_far"] >= 0).all()
Week 2’s exploding join was a print. Here it is a red CI.
Freshness¶
Ask of every extract:
- What is the newest row I am allowed to see? (
as_of) - When did this table last land? (if
max(date)is three days old, you are scoring on a weekend of silence) - Can I rerun last Tuesday and get the same frame?
print(con.execute("SELECT min(date), max(date) FROM feature_usage").fetchall())
print(con.execute("SELECT min(timestamp), max(timestamp) FROM user_events").fetchall())
CloudWave usage and events stop at 2024-11-30. Billing is clipped there too (~49k customers). That is the observation end of this universe. An as_of after that date is asking questions the warehouse cannot answer. Week 8 calls running past the last log censoring.
Ship / don’t ship
Ship a model whose training table is a query plus a date. Do not ship a model whose training table is final_final_v3.csv on a laptop. If you cannot answer “what as_of built this pickle?”, you do not have a pipeline. You have a souvenir.
✍️ Exercise¶
Do the exercises. The SQL lives in your head and in pipelines/features.py.
🤔 Reflection¶
- Why is
tenure_daysonsubscriptionsthe wrong column once you have anas_of? - A PM emails you a new CSV “with extra features.” What is your first question?
- When would you keep the 360 in SQL (DuckDB, warehouse) instead of bringing it into Pandas?
Before you leave¶
Try one self-check (Predict / Diagnose / Choose / Defend). Write the answer before you open the block.
🔗 Next¶
If you came from Week 2: go on to Week 4 (charts).
If you already finished classification: Week 8 is labels, delay, and why ~6.4% churn is not “just use AUC.”