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 the 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 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 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 · ~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 · ~8 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 · ~8 hours (Tier 100 capstone: +8) · Section 7 (tier 100) + Section 4 (all tiers)
Tier 100: build the capstone, a governed text-to-SQL agent on your warehouse, and measure how much your curation lifts a bare model. 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 (per person) |
|---|---|---|---|
| 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. | 26 to 30 hours. You write ~150-310 SQL + ~60-120 Python; the ~190-line substrate is given. |
| 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. | 31 to 35 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. | 36 to 40 hours. + ~30-60 Python LOC. |
| Tier 100 capstone |
+ Section 7: the capstone. Build a governed text-to-SQL agent on the warehouse you
built (a definitions file + a reference doc + an eval set), then measure bare vs.
grounded accuracy. Your dataset shapes the eval questions (forecasting, geospatial,
extraction, or scale).
"The portfolio piece you'd open the interview with." |
Your dataset. + ~$1 to $5 (mostly agent calls, no 22 GB scan). | 44 to 48 hours (+8 for the capstone). + ~100-170 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 $30 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: a governed text-to-SQL agent on your warehouse + the bare-vs-grounded measurement | 7 |
| Tier 100 total | 100 | |
One capstone, worth 7 points, the Section 7 rung of the tiers table above (hours are counted there, per person). This is the payoff of the whole SQL arc: you built the stack by hand in Module 1B, and Anthropic runs it as company infrastructure (bare model 21%, grounded ~95%). Now you build it on the warehouse you shipped in Tiers 80 to 93, and measure the lift yourself. The generator is a commodity and is not graded; your curation and the measured lift are.
What "bare vs grounded" means. One question, the same model, two ways to answer it:
"How many people actively listen to Taylor Swift?"
| Bare (no vocabulary) | Grounded (your vocabulary) | |
|---|---|---|
| How it answers | Guesses what "actively" means (played once? this week? five times?) and picks one silently. | Composes your pinned terms: active_since(30) ∩ listener_of('Taylor Swift'). |
| The number | Confident, and a different number on the next run. | Your number, the same on every run. |
That gap is the whole deliverable. On Anthropic's own data, grounding took a bare model from 21% to ~95% accurate. You saw this worked out on the Spotify schema in Module 1B; the capstone is you measuring the same lift on your own dataset. The numbers below (6 to 8 definitions, about 10 questions) are just how much of that vocabulary and eval you build.
| What you build | The bar |
|---|---|
| 1. The knowledge model curation |
definitions.sql: 6 to 8 business terms, each a BigQuery table
function (CREATE TABLE FUNCTION listener_of(artist STRING) AS (...)),
parameterized and callable by name. reference.md: the schema plus a join
policy (which joins are allowed, which are traps) and every NULL-prone column. Seed both
from your Project 1 schema write-up and your Sections 1 to 4 verified queries. |
| 2. The agent execution |
A text-to-SQL loop that composes your table functions by name. You hand the model a
manifest (the function names, parameters, and one-line descriptions) plus
the schema and join policy as context, and tell it to compose the vocabulary first and
drop to raw SQL only when nothing fits. Generate with ML.GENERATE_TEXT (zero
setup) or any model API; the model is swappable and not graded. |
| 3. The eval set coverage |
About 10 English business questions with known-correct ground truth (a verified query you trace). Generate the questions from your domain description, not your definitions, so they surface gaps. Span your definitions and include ≥ 2 gotcha questions (NULLs, no-activity rows, dedup), e.g. "average rating per user," which a bare model quietly gets wrong by counting the users who never rated. |
| 4. The measurement the deliverable |
Run the agent bare (no knowledge model) vs. grounded on that question set; report both accuracies. Grounded must beat bare, or your writeup explains the failure mode. Then close the gap: failures become required curation, re-measure. |
Your dataset shapes the questions. The eval set is where the old career narratives
live: a time-series dataset should include forecast-style questions, a geographic one
ST_DISTANCE-style questions, a text-heavy one extraction and semantic questions, a large
one scale questions. Same capstone, your flavor.
Graded on curation, not the model. Coverage (fraction of the independent question set answered correctly, target ≥ 85%), lift (grounded minus bare), breadth (about 10 questions spanning your definitions, ≥ 2 gotchas), and reference-doc completeness (every join path and NULL-prone column documented). A thin knowledge model produces a small lift and a low score; that is the point.
Optional extension. Add a per-query adversarial-review pass that challenges the agent's assumptions, and report the accuracy, token, and latency tradeoff (Anthropic saw +6% accuracy for +32% tokens and +72% latency). This is not the graded bonus (that is the +5 storytelling award); it is a way to push the project further.
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.
Build a governed text-to-SQL agent on the warehouse you shipped in Tiers 80 to 93, then measure
how much your curation lifts a bare model. It is the same three-file recipe you learned in
Module 1B, on your
own dataset: a definitions.sql (6 to 8 business terms, each pinned to one SQL
query), a reference.md (tables, join paths, every NULL-prone column), and an
eval set (about 10 questions with known answers). The agent must consult the definitions
first, generate SQL, run it, and answer.
BigQuery supports exactly this: a CREATE TABLE FUNCTION is a persistent,
parameterized, table-valued function, the same idea as the Postgres example in Module 1B. You
define each term once and call it by name:
CREATE TABLE FUNCTION myds.listener_of(artist STRING) AS (
SELECT DISTINCT l.user_id
FROM myds.Listens l JOIN myds.Songs s USING (song_id)
WHERE s.artist = artist
);
-- compose by name, no re-derivation:
SELECT user_id FROM myds.Users
EXCEPT DISTINCT
SELECT user_id FROM myds.listener_of('Taylor Swift');
Then the agent does not discover these on its own. You supply a manifest in the model's context: the function names, their parameters, and a one-line description each, alongside the schema and join policy, with the instruction to compose the vocabulary first and write raw SQL only when nothing fits. The table functions are where the vocabulary lives and runs; the manifest is how the model learns it exists. (Prefer not to create database objects? The alternative is a manifest-only approach: keep the definitions as a YAML or markdown of name to SQL template and have the agent inline them as CTEs. That is the dbt / Cube style, and it is fine too.)
No, you are not training anything. The agent is a small loop (about 100 to 170 lines) around a commodity model. Per question it: (1) assembles context, loading your manifest (the vocabulary names and descriptions), the schema, and the join policy into the prompt; (2) asks the model to write BigQuery SQL for the question, composing your table functions where they fit; (3) runs that SQL on BigQuery; (4) returns the answer, and for an eval, compares it to the confirmed answer. The model's job is the open-ended part: map fuzzy English to the right composition of your vocabulary and parameters, and write raw SQL for anything the vocabulary does not cover. Your job is the harness plus the grounding, the manifest and policy that make that mapping reliable. For a small domain you paste the whole manifest into the prompt; with hundreds of terms you would retrieve just the relevant ones, which is the same idea at scale.
The bare-vs-grounded measurement. Run your agent with no knowledge model, then with it, on the same question set, and report both accuracies. Grounded must beat bare, or your writeup explains the failure mode (a valid outcome; it is how Anthropic learned that raw query history was worthless). Then run the gap-closing loop: every failed question becomes required curation, and you re-measure.
Generate the questions from your domain description, not your definitions, so they surface gaps you did not plan for (this is how Anthropic builds "long-tail" evals). Author about 10, spanning your definitions and including at least 2 gotcha questions (NULLs, no-activity rows, dedup). Ground truth for each is a verified SQL query you trace by hand, the Module 1 debug-table skill. You are graded on coverage (fraction answered correctly, target ≥ 85%), lift, breadth, and reference-doc completeness, not on the model you called.
The generator is a commodity and is not graded, so use whatever is easiest.
The zero-setup default is BigQuery ML ML.GENERATE_TEXT (or
AI.GENERATE_TABLE), which calls Gemini from inside BigQuery with no external key
and bills against your $50 credit; the template ships the remote-model connection set up for
you. If you already have a Claude, OpenAI, or Gemini API key, use it. Running an open model in
Colab is allowed but not recommended: a small open model is weaker at text-to-SQL and muddies
the lift you are trying to measure.
Yes, and its shape lives in your eval questions. A time-series dataset should include
forecast-style questions, a geographic one ST_DISTANCE-style questions, a
text-heavy one extraction and semantic questions, a large one scale questions. Same capstone,
your flavor. Your agent may even call a BQML model, and a prediction can be one of your
definitions.
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.