Dear data scientists, Project 1 was the appetizer.
Real data work starts where Project 1 ended. Your team's BigQuery warehouse is real. The supplementary data isn't a clean CSV someone handed you, it lives somewhere else and needs a Spark job to land. The text columns aren't joinable by keyword alone, so you embed them. The model has to be tied to a decision a product manager will live with. Every layer is its own discipline, and in this project you touch all of them.
In Project 1 you queried BigQuery. In Project 2 you build the data stack a modern data team ships before lunch: Spark ELT, vector embeddings in BigQuery, a partitioned warehouse, and a model tied to a business question.
.ipynb) plus a 5-minute screen
recording, due Nov 20.cs145_project2_science_template_fa26.ipynb in the
course Drive folder. Pre-structured with all 8 rubric
sections, Spark bootstrap, embedding scaffolding, and four worked filter recipes (FHIR,
NYC taxi, finance, music) for you to adapt.Proposal due Oct 30. Final due Nov 20. Five weeks of work, about 8 hours a week. Each week has a punchline; the rest is detail.
Oct 17 to Oct 23 · 8 to 10 hours · Phase 0
Spark loads a 1 GB Wikipedia slice into BigQuery next to your dataset. Filter to your
domain, run one keyword join, parity-check against
bigquery-public-data.samples.wikipedia. The proof of Phase 0 lands here.
Oct 24 to Oct 30 · 8 to 10 hours · Phase 1 + proposal
Embed your filtered slice in BigQuery with ML.GENERATE_EMBEDDING. Run one
VECTOR_SEARCH query that finds neighbors your keyword join missed. Submit Phase 0
+ Phase 1 output as your proposal by Oct 30.
Oct 31 to Nov 6 · 7 to 8 hours · Sections 1 to 3
Six non-simple queries, at least 2 using your Wiki layer. Three visualizations tied to specific queries. Train and evaluate your BQML model. Pull query plans and compute I/O costs at 10x and 100x scale.
Nov 7 to Nov 13 · 7 to 9 hours · Section 5 (tier 87+) + Section 6 (tier 93+)
Tier 87+: scale your fact layer to 5 GB+, design partition and cluster keys, build a materialized view, benchmark one query before and after. Tier 93+: embed 1 GB of Wikipedia text, build a real BQ vector index, run one hybrid keyword + semantic query, compare latency vs. brute-force.
Nov 14 to Nov 20 · 5 to 7 hours · Section 7 (tier 100) + Section 4 (all tiers)
Tier 100: pick one of six capstones and ship it. Everyone: write up the project, draft 2 or 3 AI anecdotes, record the 5-minute demo, submit to Gradescope.
Pick one row before you write the proposal. You can climb later, but the proposal asks which tier you are aiming for so partners (and we) know how to plan the work. Tier 87 is where the warehouse work starts paying off; Tier 93 is where the production retrieval layer becomes indexable; Tier 100 is the capstone.
| Tier | What you ship | Dataset and BQ cost | Effort |
|---|---|---|---|
| Tier 80 | The base project: Spark ELT for a 1 GB Wikipedia slice, BQ embeddings, 6 queries, BQML
model, scale math, writeup.
"The notebook you'd send to a hiring manager." |
1 GB Wikipedia + your dataset. ~$2 to $8 of your $50 BQ credit. | 29 to 38 hours. ~200-300 Python LOC + ~150-310 SQL lines. |
| Tier 87 warehouse layout |
+ Section 5: scale fact layer to ≥ 5 GB, design partition and cluster keys, build a
materialized view, before/after benchmark.
"The stack you'd ship before lunch." |
5+ GB combined fact layer. + ~$1. | 34 to 43 hours. + ~50-80 Python LOC. |
| Tier 93 production retrieval |
+ Section 6: scale embed pass to 1 GB of text, build a real BQ vector index, run a
hybrid keyword + semantic query, compare indexed vs. brute-force latency.
"The real-scale retrieval layer." |
~5 GB of vector data. + ~$8 to $15 for the embed pass. | 39 to 49 hours. + ~30-60 Python LOC. |
| Tier 100 capstone |
+ Section 7: pick one of six career-narrative capstones (Wikipedia 22 GB / retrieval
eval rigor / multi-model + calibrated / forecasting / geospatial / LLM extraction).
"The portfolio piece you'd open the interview with." |
Depends on capstone. + ~$1 to $15. | 44 to 54 hours. + ~80-200 Python LOC. |
| +5 bonus | Awarded at Tier 93 or above for quality of storytelling: analysis depth, engineering judgment, and how clearly the project's insights come through. | n/a | Not LOC; the polish you put on the writeup. |
Final score = tier points + bonus. Each tier is additive (Phase 0 + Phase 1 + Sections 1-4 = 80; + Section 5 = 87; + Section 6 = 93; + Section 7 = 100). Typical total BQ spend $5 to $35 of your $50 credit; running out should not be the bottleneck, understanding should be. Watch the query estimator before scanning > 1 GB.
Choosing between the two Project 2 options? In this project tiers are additive: each one adds a section of work. The Systems project (NanoMem) grades tiers the other way, as a scale ceiling: the same code on bigger data.
Per-tier ceilings, effort, and cost are in Tiers at a glance above. This section is the quality side: which metrics you must report at every tier, and the rubric that scores each section. A polished Tier 87 project earns 87. A sloppy Tier 100 project earns less than a tight Tier 87 one, because section quality is graded on its own merit.
Each section has its rubric, its expected output, and a "what good looks like" inline in the template. Read the section header before you start.
| Section | Title | Points |
|---|---|---|
| Tier 80: required for everyone | ||
| Phase 0 | Spark + 1 GB Wikipedia slice + keyword join + proposal (Oct 30). Banked on time; forfeit if late. | 10 |
| Phase 1 | Embed slice in BigQuery (≤ 100 MB text) + one semantic join query | 10 |
| 1 | 6 non-simple queries (2+ using Wiki layer) + 3 visualizations | 20 |
| 2 | BQML model + evaluation | 15 |
| 3 | Scale math + query plans (10x and 100x I/O) | 10 |
| 4 | Writeup + AI takeaways + 5-min demo | 15 |
| Tier 80 total | 80 | |
| Tier 87 unlock | ||
| 5 | Warehouse layout: ≥ 5 GB fact layer + partition + cluster + MV + before/after bench | 7 |
| Tier 87 total | 87 | |
| Tier 93 unlock (requires Section 5) | ||
| 6 | Production retrieval at scale: 1 GB embed + BQ vector index + hybrid keyword + semantic query + latency comparison | 6 |
| Tier 93 total | 93 | |
| Tier 100 unlock (requires Section 6) | ||
| 7 | Capstone: pick any one of six | 7 |
| Tier 100 total | 100 | |
Six options, each worth the same 7 points and 5 to 8 hours on top of Section 6. Pick the one that matches the role you want to explore, or the shape of your dataset.
| Capstone | What you ship | Hours |
|---|---|---|
| (a) Wikipedia scale-up data platform / data eng |
Scale your Spark ELT to ≥ 10 GB of Wikipedia (target 22 GB). Partition + cluster the loaded table. At least one analytic query that combines this layer with your dataset in a way that the 1 GB slice could not answer. | 6-8 |
| (b) Retrieval eval rigor AI / search |
Take Section 6's hybrid retrieval to full eval rigor. Author 20+ eval queries with known ground truth. Report R@1, R@5, R@10 for keyword-only, vector-only, and the hybrid you built. Tune the hybrid weighting; report which signal wins on which query shape and why. | 5-7 |
| (c) Multi-model + calibrated decision product DS |
Train 3+ model classes (e.g., logistic regression, boosted tree, deep). Slice analysis on a fairness or cohort axis. Threshold tied to a business metric (e.g., max expected revenue at cost C). Report what you'd ship. | 5-7 |
| (d) Forecasting + backtest quant / ops research |
Train a BQML ARIMA_PLUS (or similar) on a time-series target. 4-week holdout.
Compare against a naive baseline (last-value or seasonal-naive). Seasonality decomposition.
Backtest report. |
5-7 |
| (e) Geospatial analytics spatial / urban DS |
Join your dataset + Wikipedia on geography using BQ's ST_* functions (or H3). Spatial
clustering (ST_CLUSTERDBSCAN), heatmap, distance-decay analysis. At least one
visualization that renders on a map. |
5-7 |
| (f) LLM-augmented extraction modern data eng / AI |
Use ML.GENERATE_TEXT (or AI.GENERATE_TABLE) at scale to extract
structured fields from your Wikipedia slice. Join the extracted structure into your
analysis. Sanity-check extraction quality on a 100-row sample. |
5-8 |
Core rule: use the production tools (Spark, BigQuery, BQML, ML.GENERATE_EMBEDDING, VECTOR_SEARCH). The point of this project is to build with the real stack, not from scratch. What you write yourself is the SQL, the model spec, the analytic logic, the capstone work, and the writeup.
Use AI freely to learn the concepts and unblock yourself. Ask Claude to explain how
ML.GENERATE_EMBEDDING works. Ask it to debug a Spark schema mismatch. Paste a query plan
and have it walk you through what each stage is doing. The intuition layer is exactly what AI is good
at, and you should use it.
Write the analytic logic yourself. Your 6 queries, your filter design, your model specification, your capstone work, your writeup. Not because typing matters, but because writing them yourself is what tells you whether you actually understood the intuition you just absorbed. The line, in one sentence: if you could not explain your queries and your model to a TA in plain English, with the chat window closed, you crossed it.
bigquery-public-data.samples.wikipedia) as a free parity check for your
Phase 0 work.You must follow the Stanford Honor Code:
You can reuse: your project overview, table descriptions, and setup code. Your Project 1 fact tables stay in their BigQuery dataset and become one half of every join in Project 2.
Must be new: all 6 queries, all 3 visualizations, your Wikipedia layer, your embeddings, and the BQML model. If you partner, choose one of the two Project 1 notebooks as your starting point; you do not merge both. If you start fresh, your new dataset still needs 3+ tables and at least 500 MB.
The proposal IS Phase 0, worth 10 points. Submit by Oct 30 with Phase 0 actually working and the 10 points are yours; miss the deadline and Phase 0 forfeits. One markdown cell at the top of your notebook (with Phase 0 code cells run above it), covering:
Inline in the template Colab notebook. Each section has its rubric, its expected output, and a "what good looks like" cell. The summary on this page is the macro picture; the per-section details live in the template.
The template's first cell installs a pinned PySpark and starts a local SparkSession.
Colab's free CPU runtime is enough for the 1 GB filtered slice. Do not try to set up a Dataproc
cluster, you do not need one and it would cost you credits you don't have.
Three recipes the template ships, pick one and adapt:
title or first paragraph matches. Cheap and obvious.categories
array. Filter on prefixes like Category:Medicine,
Category:Companies_of_*. Higher precision than keyword.The template includes four worked recipes (FHIR, NYC taxi, finance, music) you can copy and edit. Report your filter's hit rate (sampled) in 1-3 sentences.
Three examples to anchor on. Same shape, different domain:
LEFT JOIN wiki_domain w ON LOWER(w.title) LIKE CONCAT('%', LOWER(c.condition_name),
'%'). The chart is condition coverage from highest to lowest.Two mitigations. First, checkpoint to Drive: write the filtered Parquet to a mounted Drive folder before doing the BQ load. If the session dies, you don't restart the whole pipeline. Second, filter early: apply your keyword filter inside the Spark read, not after. The full 22 GB doesn't need to materialize in RAM, Spark can stream-filter it.
Run the same title-keyword query against bigquery-public-data.samples.wikipedia
(a public 314M-row Wikipedia revision sample with a title column, no upload
required). Counts should agree within sampling tolerance once you align on title case and
keyword set. If they disagree wildly, your filter is wrong before you spend a week on
analysis. The template ships this parity helper.
It depends on how much text you embed. ML.GENERATE_EMBEDDING at
text-embedding-005 charges roughly $0.025 per 1k characters. Honest numbers for
a typical filtered slice:
Default rule: embed first paragraphs only, cap at ~100 MB of text. Total typical cost: $1 to $3. If you want to embed more, the FAQ math is right there; you make the call.
Four statements. Template ships them; this is the shape:
-- 1. Register the remote embedding model
CREATE OR REPLACE MODEL `myproj.cs145_p2.embed_model`
REMOTE WITH CONNECTION `myproj.us.vertex_ai_connection`
OPTIONS(ENDPOINT = 'text-embedding-005');
-- 2. Embed your filtered slice (input table must have a column named `content`)
CREATE OR REPLACE TABLE `myproj.cs145_p2.wiki_domain_embedded` AS
SELECT
article_id, title, url, first_paragraph,
ml_generate_embedding_result AS embedding
FROM ML.GENERATE_EMBEDDING(
MODEL `myproj.cs145_p2.embed_model`,
(SELECT article_id, title, url, first_paragraph,
first_paragraph AS content
FROM `myproj.cs145_p2.wiki_domain`)
)
WHERE ml_generate_embedding_status = '';
-- 3. Index it (optional for slices < 5 GB; brute-force search works at small scale)
CREATE OR REPLACE VECTOR INDEX wiki_idx
ON `myproj.cs145_p2.wiki_domain_embedded`(embedding)
OPTIONS(distance_type = 'COSINE', index_type = 'IVF');
-- 4. Query it semantically. VECTOR_SEARCH returns query.*, base.*, distance.
SELECT query.query_text, base.title, base.url, distance
FROM VECTOR_SEARCH(
TABLE `myproj.cs145_p2.wiki_domain_embedded`, 'embedding',
(SELECT
'my query text' AS query_text,
ml_generate_embedding_result AS embedding
FROM ML.GENERATE_EMBEDDING(
MODEL `myproj.cs145_p2.embed_model`,
(SELECT 'my query text' AS content))),
top_k => 5, distance_type => 'COSINE');
The shape is always: embed your query strings, run VECTOR_SEARCH against the
embedded Wiki slice, join the top-K hits back to your dataset. Three concrete forms:
Document what your semantic join found that the Phase 0 keyword join missed. That delta is the lesson.
Three things to check, in order. First: did your filter produce articles actually in your
domain? Run the filter precision sample again. Garbage in, garbage out. Second: are you
embedding the same field you're searching? If you embedded first_paragraph but
your query text is a single keyword, cosine similarity won't reward you. Third: does your
query text match the corpus register? Short keyword queries against long descriptive
paragraphs is the classic mismatch.
No. BQ vector indexes require ~5 GB of vector data minimum, which a 100 MB-text Phase 1 slice
is well short of. Brute-force VECTOR_SEARCH is the right call at this scale.
Tier 93 (Section 6) is where you scale the embed pass up enough to build a real index.
Combines 2+ of {JOINs, CTEs, window functions, subqueries, complex CASE, VECTOR_SEARCH}. A VECTOR_SEARCH joined back to your dataset is non-simple by definition. Six queries total; at least 2 must use the Wiki layer (keyword join, semantic join, or both).
Joins must demonstrate a new insight, not just technical ability. Each chart should directly illustrate findings from a specific query. Include a caption like "This chart shows results from Query #3, revealing that..."
-- Train
CREATE OR REPLACE MODEL `your_dataset.model_name`
OPTIONS(
model_type='linear_reg',
input_label_cols=['target_column']
) AS
SELECT feature1, feature2, feature3, target_column
FROM `your_dataset.training_table`;
-- Evaluate
SELECT * FROM ML.EVALUATE(
MODEL `your_dataset.model_name`,
(SELECT feature1, feature2, target_column
FROM `your_dataset.test_table`));
Model types: linear_reg (continuous), logistic_reg (binary
classification), boosted_tree_regressor / boosted_tree_classifier
(advanced), arima_plus (forecasting, capstone d).
Yes, and it's a great move. Use ML.GENERATE_EMBEDDING on the relevant text column
of your training rows, then feed the embedding into a boosted_tree_* model.
Saves your bullet point: "I used semantic features in my model."
Something product-relevant that you can't trivially calculate. "Predict tip amount from trip
features" is good; "Predict trip_id" is not.
Red flag: if your model hits 99.9% accuracy, you probably have data leakage.
The formulas (BigSort, BNLJ, SMJ, HPJ) are covered in the Big Sort lesson and Module 3 join lessons; use the equation sheet. The recipe: pick the operator that shows up in your query plan, substitute your table page counts and buffer size, then compute the same expression with P(R) replaced by 10 × P(R) and 100 × P(R). Report both numbers.
No. Section 5 requires the fact layer to be ≥ 5 GB. Below that, partitioning teaches almost nothing: the optimizer's small-table tricks dominate, $/query is rounding error, and your before/after benchmark won't show measurable improvement. Scale your Spark ELT to ≥ 5 GB first, then Section 5 starts paying out.
Partition on a date / time column most of your queries filter on. BigQuery
physically separates rows into partitions, so a WHERE date BETWEEN ... skips
most of the scan. Cluster on a categorical column most of your queries
group or filter on (1 to 4 columns). Clustering re-sorts rows within a partition for faster
range reads.
Run your chosen query before and after applying the layout. Report bytes scanned and $/query in both. If the layout didn't help, your keys don't match your queries; pick different ones.
Precomputed, incrementally maintained query results. For an ML feature pipeline,
CREATE MATERIALIZED VIEW feature_v AS SELECT ... FROM fact ... means your
training query reads the MV instead of recomputing the aggregation. Cheaper, faster, and
if your fact layer keeps growing, the MV updates incrementally instead of from scratch.
CREATE VECTOR INDEX on the
scaled embedded table; BQ's index minimum is ~5 GB of vector data, which a 1 GB text
embed pass clears.VECTOR_SEARCH and brute-force VECTOR_SEARCH (force brute force
with options => '{"use_brute_force": true}'). Report both.The standard shape: run vector search and a keyword filter as two CTEs, fuse the scores in a final SELECT. Reciprocal-rank fusion is the simplest defensible fuser; a weighted-sum on normalized scores is fine too. Document the fusion you chose in a comment.
WITH vec AS (
SELECT base.article_id, base.title, distance,
ROW_NUMBER() OVER (ORDER BY distance) AS vec_rank
FROM VECTOR_SEARCH(
TABLE wiki_domain_embedded, 'embedding',
(SELECT ML.GENERATE_EMBEDDING(...)),
top_k => 50)
), kw AS (
SELECT article_id, title,
ROW_NUMBER() OVER (ORDER BY LENGTH(title)) AS kw_rank
FROM wiki_domain
WHERE LOWER(title) LIKE '%diabetes%'
LIMIT 50
)
SELECT COALESCE(vec.article_id, kw.article_id) AS id,
1.0/(60+vec.vec_rank) + 1.0/(60+kw.kw_rank) AS rrf_score
FROM vec FULL JOIN kw USING (article_id)
ORDER BY rrf_score DESC LIMIT 10;
Probably not. BQ's vector index has a fixed build cost and a query overhead; below a few million vectors, brute force can beat the index on small queries. That is itself the finding: document where the crossover is and why. Real-scale retrieval teams pick indexed vs. brute force based on exactly this kind of empirical measurement.
No; the BQ vector index will not build below its data minimum, so the "production retrieval" story doesn't work at Phase 1 scale. If $15 is a budget problem, embed first-paragraphs of a broader filter (cheaper per article) rather than full text of a narrow filter. The cost table shows typical totals stay under your $50 credit.
Pick by curiosity. Either the role you want to explore, or the shape of your dataset:
Pick one. You may not earn capstone credit for partial work across multiple options.
You must scale your Spark ELT to at least 10 GB of Wikipedia (target the full 22 GB). The loaded BigQuery table must be partitioned and clustered. At least one analytic query in your notebook must combine this scaled layer with your dataset in a way that the 1 GB Phase 0 slice could not have answered. Expect 30 to 60 min of Spark wall time and at least one Colab session crash you have to recover from.
Section 6 already shipped one hybrid query. The capstone takes that to full eval discipline. Author 20+ eval queries in your domain with known-correct ground truth. Run all three retrievers (keyword-only, vector-only, your hybrid from Section 6) and report R@1, R@5, R@10 for each. Tune your hybrid weighting; report which signal wins on which query shape and why. The hybrid must beat the better single-signal baseline on at least one metric, OR your writeup explains the failure mode.
Train 3+ model classes on the same prediction target. Pick one based on a holdout metric. Do a slice analysis on a cohort or fairness axis you can defend. Tie a threshold to a business metric (max revenue at cost C, max recall subject to precision floor, etc.). The deliverable is "this is the model I'd ship and here's why," not "I trained three models."
BQML ARIMA_PLUS (or equivalent) on a time-series target with clear seasonality.
4-week (or domain-appropriate) holdout window. Compare against a naive baseline (last-value
or seasonal-naive). Decompose the seasonality and report it. Show the backtest table.
At least 3 queries using BQ's geo functions (ST_DISTANCE,
ST_INTERSECTS, ST_CLUSTERDBSCAN, or H3 functions). Join your dataset
and your Wikipedia layer on geography (e.g., neighborhood polygons, lat/lng buckets). At
least one visualization renders on an actual map (geopandas, plotly, kepler.gl, your choice).
Use ML.GENERATE_TEXT or AI.GENERATE_TABLE to extract structured
fields from your Wikipedia article text at scale (1k+ articles). Examples: "for each drug
article, extract mechanism of action," "for each company article, extract HQ city and
founding year." Join the extracted structure into your analysis. Sanity-check extraction
quality on a 100-row sample and report precision/recall on a defined schema.
Use AI freely. The real bar is whether you can talk through your filter design, your queries, your model, and your capstone in plain English. That skill is what data jobs actually lean on, even more than typing the SQL yourself. See the AI Policy for the full rule.
Short and informal. Half a page, not a report. 2 to 3 anecdotes (a few sentences each) plus one plain-English walkthrough of one piece of your pipeline. Good anecdotes capture a moment:
VECTOR_SEARCH query embedded the same model for docs
and query, but compared cosine across the wrong axis."ML.EVALUATE returned accuracy instead of R²."Proposal due Oct 30. Final notebook + demo recording due Nov 20. Both submitted to Gradescope.
.ipynb file. Start from
cs145_project2_science_template_fa26.ipynb.What you build, the five-week climb, the four tiers, where the capstone fits.
A file-by-file tour of the template notebook: what's given, what you write at each tier.
Optional refresher for the Spark side of Phase 0: the Spark and Distributed SQL colab walkthrough from Module 5.