Lab 03 — Warehouse-Native ML (BigQuery-ML style)
Phase 27 · Lab 03 · Phase README · Warmup
The problem
The classic ML workflow exports data out of the warehouse — CSVs to a training box, a Python
environment, a serving stack — and every hop is a place for preprocessing to drift, PII to leak,
and pipelines to rot. BigQuery ML's counter-move: bring the model to the data. Training is a
SQL statement over a table (CREATE MODEL ... AS SELECT ...), scoring is a query
(ML.PREDICT), metrics are a query (ML.EVALUATE), and — the underrated part — preprocessing
can be declared in a TRANSFORM clause that is stored inside the model, so serving re-applies
exactly the preprocessing training used. That last piece is a structural fix for
training-serving skew: you cannot forget to scale a feature at predict time, because the
scaler travels with the model.
The pieces you must understand mechanically, not just by name:
- Deterministic training. Logistic regression by full-batch gradient descent with fixed (zero) initialization and fixed hyperparameters: same table in, same weights out — every run, every machine. Reproducibility is a choice you engineer, not luck.
- Fit-on-train-only preprocessing. The z-score's mean/std and the one-hot vocabulary come from the training rows. Re-fitting on serving data (or the test set) is leakage's quieter sibling; an unseen category at serving time must degrade gracefully (all-zeros), never crash.
- A hash-based train/test split.
MOD(ABS(FARM_FINGERPRINT(key)), 10) < 8— the warehouse idiom for a split that is stable across runs and across dataset growth, because each row's side depends only on its own key, never on a shuffle order. - Metrics from the confusion matrix. Accuracy, precision, recall, F1 — computed on a table where you can hand-check every cell.
What you build
| Piece | What it does | The lesson |
|---|---|---|
Table | rows-of-dicts with select/where | just enough warehouse to train from |
hash_split | FARM_FINGERPRINT-style deterministic split | reproducible eval; a row never switches sides |
Transform.fit / apply | z-score + one-hot, fit on train, stored with the model | preprocessing is part of the model, not the caller's job |
Warehouse.create_model | CREATE MODEL: zero-init, fixed-epoch gradient descent | training as a deterministic function of (rows, options) |
Model.predict_proba | sigmoid over transformed features | the serving path reuses the fitted transform |
ml_predict | ML.PREDICT: input rows + predicted_* columns | scoring as a table-to-table operation |
ml_evaluate | ML.EVALUATE: accuracy/precision/recall/f1 + mse/mae | metrics you can verify against a known confusion matrix |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python solution.py) |
test_lab.py | 32 tests: split determinism, transform fit/apply, training determinism, prediction reproducibility, metric correctness, anti-skew |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # worked example
Success criteria
-
hash_splitis deterministic, partitions every row, and a row never switches sides when more rows are added (it's a hash of the key, not a shuffle). -
Transform.fitlearns mean/std (population) and a sorted one-hot vocabulary from training rows only; a constant column z-scores to 0.0 instead of dividing by zero. -
An unseen category at predict time is all-zeros, not an error; a missing numeric column
is a
SchemaError. -
create_modeltrains byte-identical weights on the same data (zero init, fixed epochs and learning rate) and reaches 100% accuracy on a linearly separable toy set. - Labels are validated (0/1 only), empty tables and unsupported model types are structured errors.
-
ml_predictpreserves input columns, addspredicted_<label>andpredicted_<label>_prob, and is reproducible. -
ml_evaluatereproduces exact metrics on a hand-checkable confusion matrix (TP/FP/TN/FN = 1/1/1/1 → everything 0.5). - The anti-skew tests pass: prediction through the model equals a manual apply-the-fitted-transform computation, and predicting on shifted data never re-fits the scaler.
-
All 32 tests pass under both
labandsolution.
How this maps to the real stack
CREATE MODEL/ML.PREDICT/ML.EVALUATEare the real BigQuery ML verbs, with the same shapes:CREATE MODELtakesOPTIONS(model_type='logistic_reg', input_label_cols=['y'])and aSELECT;ML.PREDICTreturns the input columns pluspredicted_<label>and a probability struct;ML.EVALUATEreturns a metrics row (precision, recall, accuracy, f1_score, log_loss, roc_auc for logistic regression). Our dict-of-metrics is that row.TRANSFORMis the real anti-skew feature: preprocessing declared there (e.g.ML.STANDARD_SCALER(x) OVER ()) is embedded in the model and automatically applied byML.PREDICT. Snowflake ML and Redshift ML have the same "model owns its preprocessing" design; scikit-learn'sPipelineand Vertex AI's preprocessing-in-SavedModel are the same idea outside the warehouse.hash_splitis the documented BigQuery pattern for reproducible splits:MOD(ABS(FARM_FINGERPRINT(CAST(key AS STRING))), 10) < 8. We usemd5because it's stdlib; the property that matters — side depends only on the key — is identical.- Real BigQuery ML training uses closed-form or iterative optimizers ("normal equation" for small linear models, batch gradient descent with line search otherwise), runs distributed, and supports many model types (boosted trees via XGBoost, DNNs via TensorFlow, ARIMA, k-means, matrix factorization) plus imported TensorFlow/ONNX models and remote Vertex endpoints. Ours is one model type with a fixed-step optimizer, because the contract (deterministic train, owned transform, table-in/table-out predict) is the lesson, not the model zoo.
- When warehouse ML fits: the data is already in the warehouse, the model is tabular and moderate-sized, and the team speaks SQL — dashboards' churn scores, LTV, propensity. When it doesn't: deep learning on unstructured data, sub-100ms online serving (pair the warehouse model with Lab 01's online store instead), or custom architectures — that's Vertex AI / Databricks territory, per the phase README's comparison table.
Limits of the miniature. No SQL parser — the API is Python methods shaped like the SQL verbs. Population (not sample) std; fixed learning rate with no convergence check (fixed epochs); binary labels only; probability-vs-label mse/mae rather than BigQuery's log-loss/roc_auc. The one-hot's all-zeros-for-unseen policy matches common warehouse encoders but hides the "new category alert" a production feature-monitoring layer would raise.
Extensions (your own machine)
- Add k-means as a second
model_type(fixed initial centroids = first k distinct rows, Lloyd's iterations,ML.EVALUATEreturning inertia / Davies-Bouldin) — BigQuery ML supports exactly this for segmentation. - Add
ML.WEIGHTS: expose the trained coefficients as a table, the way analysts inspect a BigQuery ML model's feature attribution. - Implement early stopping: track mean log-loss per epoch and stop when the improvement drops
below
min_rel_progress— the realOPTIONSknob. - Run the real thing on the BigQuery sandbox (free tier): the same churn table as a real
CREATE MODEL ... TRANSFORM(...), then compareML.EVALUATE's metrics with your miniature's.
Interview / resume signal
"Built a BigQuery-ML-style warehouse ML engine: CREATE MODEL as deterministic logistic regression (zero-init full-batch gradient descent — same table, same weights, every run), a TRANSFORM clause whose standardization and one-hot encoding are fit on training data and stored inside the model so ML.PREDICT cannot skew from training, FARM_FINGERPRINT-style hash splits for reproducible evaluation, and ML.EVALUATE metrics verified against a hand-computed confusion matrix."