Interactive quizzes for Quarto documents, powered by quizdown-js. MDS fork of parmsam/quarto-quizdown, extended with new question types and a print review mode.
Install:
quarto add UBC-MDS/quarto-quizdown-mds-ext
Try it: full quiz
All five question types in one quiz. Answer everything, hit ✓✓ to evaluate, then Cmd/Ctrl+P to see the print review sheet.
---
shuffle_answers: true
---
## Match each scenario to the right data loading approach!
- Small dataset that fits comfortably in memory :: `pd.read_parquet()` at startup
> Fast and simple: load once at startup, reuse throughout the session.
- Multi-GB Parquet file that must be filtered before analysis :: `ibis` + DuckDB lazy query
> Only the query result enters memory; the full file never loads.
- Deploying a Shinylive / WASM app where DuckDB file access fails :: In-memory pre-sampled data
> DuckDB file access silently fails in WASM. Embed a small pre-sampled dataset instead.
- Slow computation shared by multiple Shiny outputs :: `@reactive.calc` with eager load
> Load eagerly, then cache the filtered result so downstream outputs don't re-run the query.
- :: `spark.read.parquet()` on a cluster
> Spark is for distributed cluster computing. Overkill for these single-machine scenarios.
> Think about **where the data lives**, **how large it is**, and **what the runtime environment supports**.
## Classify each ML task as Regression or Classification!
The chips **Regression** and **Classification** are reusable: drag or click each to as many rows as needed.
> **Regression** predicts a *continuous* value (a number on a scale). **Classification** predicts a *category* (one of a finite set of classes).
- Predict house price :: Regression
> The output is a dollar amount: a continuous value.
- Detect spam email :: Classification
> The output is spam / not spam: a binary category.
- Forecast next month's sales revenue :: Regression
> Revenue is continuous, even though it's rounded to cents.
- Identify the digit in a handwritten image :: Classification
> The output is one of 10 discrete classes (0–9).
- Estimate a patient's remaining hospital stay in days :: Regression
> Days is a continuous (or at least ordinal) quantity.
- Diagnose whether a tumour is malignant or benign :: Classification
> A binary categorical outcome.
- :: Clustering
> Clustering is **unsupervised**. No target label is predicted.
## Put the EDA steps in order!
> Exploratory Data Analysis follows a natural progression from raw inspection to insight. Skipping early steps often leads to missed data quality issues.
1. Load and inspect the raw data (`df.head()`, `.info()`, `.dtypes`)
2. Check for missing values and duplicates
3. Compute summary statistics (`df.describe()`)
4. Visualize distributions of individual variables
5. Explore relationships between variables (correlations, scatter plots)
6. Identify and investigate outliers
7. Document findings and decide on data cleaning steps
# A Shiny app loads a 2 GB Parquet file at startup. It crashes on Posit Connect due to memory limits. What is the right fix?
> Think about **when** data actually enters memory, not just **where** the load call is placed.
1. [x] Switch to `ibis` + DuckDB lazy loading: load only the query result
> Correct. With `ibis`, `.execute()` runs a SQL query and loads only the filtered rows. The full 2 GB file never enters RAM.
1. [ ] Move the `pd.read_parquet()` call inside a `@reactive.calc`
> This defers the load to first use, but the full file still enters memory. The crash will still happen.
1. [ ] Convert the Parquet file to CSV, which uses less memory
> CSV is actually **larger** than Parquet on disk, and loading it still reads the full file into memory.
1. [ ] Increase the worker memory limit on Posit Connect
> Scaling up is a temporary patch. The correct fix is to never load the full file in the first place.
## Which of the following are valid reasons to choose Parquet over CSV for storing tabular data?
Select **all** that apply.
> Parquet is a binary columnar format; CSV is plain text row-by-row. Both store tabular data, but they make very different trade-offs.
- [x] Column-oriented storage enables faster column scans
> ✓ Parquet reads only the columns requested. CSV must scan every row to extract a single column.
- [x] Data types are stored in the file: no silent type coercion on read
> ✓ CSV has no type information; pandas must infer types and can silently misread integers as floats.
- [x] Columnar compression makes file sizes much smaller
> ✓ Similar values in a column compress well together: Parquet files are often 5–10× smaller than equivalent CSVs.
- [ ] Parquet files are human-readable in any text editor
> ✗ Parquet is a binary format. You need a tool like DuckDB or pandas to inspect it.
- [ ] Parquet is natively supported by Excel
> ✗ Excel cannot open Parquet files without a plugin.
Question examples
Matching and classification questions
TipItem design: situations on the left, answers on the right
Place situations, scenarios, or descriptions on the left (prompts) and answers, categories, or technologies on the right (chips). This mirrors expert reasoning: given a situation, identify the right response. It also makes the question harder to solve by elimination alone, because situations are richer and less interchangeable than short labels.
TipPractice quantity and distractor fading
Research shows students need about 7 practice opportunities per concept to reach 80% mastery (Koedinger et al., 2023). Simple associations need 3 to 6; multi-step or context-dependent skills need 10 to 15 (KLI Framework, Koedinger et al., 2012). Once mastery is reached, additional massed practice adds little. Space follow-up items across sessions instead (Corbett & Anderson, 1994).
Within a quiz block, consider a fading approach to distractors (chips with no correct match). Three distractors is a good default.
First question: no distractors. Lets students activate the relevant schema and build confidence through retrieval practice.
Follow-up matching: add distractors to require finer discrimination.
Later questions: mix in MCQ, ordering, or other types to vary the cognitive demand.
1-to-1 matching
Each right-side chip belongs to exactly one left prompt. Placed chips stay in the pool but appear dimmed so students can track progress. Distractors (:: Value) are chips that don’t match any prompt.
NoteSyntax
```quizdown## Match each scenario to the right data loading approach!- Small dataset that fits in memory :: `pd.read_parquet()` > Correct-pair feedback: shown after evaluation, > regardless of whether the student got it right.- Multi-GB file: must filter first :: `ibis` + DuckDB > Pair feedback explains the reasoning behind this pairing.- :: `spark.read.parquet()` on a cluster > Distractor feedback: shown when a student places > this chip in any slot and evaluates.> Hint text: shown when the student clicks the 💡 button.```
Situation :: Answer: a correct pair
Indented > under a pair: pair feedback, shown after evaluation for that row
:: Answer (no left side): a distractor chip with no correct match
Indented > under a distractor: distractor feedback, shown when the student places it and evaluates
Top-level >: hint, shown on 💡 click
---
shuffle_answers: true
---
## Match each scenario to the right data loading approach!
- Small dataset that fits comfortably in memory :: `pd.read_parquet()` at startup
> Fast and simple: load once, reuse throughout the session.
- Multi-GB file: must filter before loading :: `ibis` + DuckDB lazy query
> Only the query result enters memory; the full file never loads.
- Shinylive / WASM app where DuckDB file access fails :: In-memory pre-sampled data
> DuckDB file access silently fails in WASM. Embed a small pre-sampled dataset instead.
- :: `spark.read.parquet()` on a cluster
> Spark is for distributed cluster computing. Not needed here.
> Think about **where the data lives**, **how large it is**, and **what the runtime supports**.
Multi-match / classification
When the same right-side label appears more than once, it becomes a reusable chip that can be placed in multiple slots. Perfect for classify-into-bins tasks.
NoteSyntax
```quizdown## Classify each ML task as Regression or Classification!- Predict house price :: Regression > Pair feedback: the target (price) is continuous.- Detect spam email :: Classification > Pair feedback: the target is a category.- Forecast stock return :: Regression- :: Clustering > Distractor feedback: shown when a student places > Clustering in any slot and evaluates.> Hint: Regression → continuous target.> Classification → categorical target.```
Same syntax as 1-to-1 matching. When the same right-side label repeats (e.g., Regression appears on multiple rows), it automatically becomes a reusable chip.
---
shuffle_answers: true
---
## Classify each ML task as Regression or Classification!
> Regression → continuous target. Classification → categorical target.
- Predict house price :: Regression
> The target (price) is a continuous number.
- Detect spam email :: Classification
> The target is a category: spam or not spam.
- Forecast stock return :: Regression
> Returns are continuous values, not categories.
- Identify handwritten digit :: Classification
> Digits 0–9 are discrete categories.
- Estimate delivery time :: Regression
> Delivery time is a continuous quantity.
- :: Clustering
> Clustering is unsupervised: there is no target label to predict.
Multiple choice and single choice
Single choice
Exactly one correct answer. Use a numbered list (1.) with [x] on the correct option.
NoteSyntax
```quizdown# Question heading> Optional hint shown when student clicks 💡.1. [x] Correct answer > Feedback shown after evaluation.1. [ ] Wrong answer > Explain why this is wrong.1. [ ] Another wrong answer > Explain why this is wrong.```
---
shuffle_answers: true
---
# A Shiny app loads a 2 GB Parquet file at startup and crashes on Posit Connect. What is the right fix?
> Think about **when** data actually enters memory.
1. [x] Switch to `ibis` + DuckDB lazy loading: load only the query result
> Correct. `.execute()` loads only the filtered rows. The full 2 GB file never enters RAM.
1. [ ] Move the `pd.read_parquet()` call inside a `@reactive.calc`
> The full file still enters memory on first use. The crash will still happen.
1. [ ] Convert the Parquet to CSV, which uses less memory
> CSV is larger than Parquet on disk, and still reads fully into memory.
1. [ ] Increase the worker memory limit on Posit Connect
> Scaling up is a patch. The right fix is to never load the full file.
Multiple choice
One or more correct answers. Use an unordered list (-) with [x] on all correct options.
---
shuffle_answers: true
---
## Which of the following are valid reasons to choose Parquet over CSV?
> Parquet is a binary columnar format; CSV is plain-text row-by-row. Both store tabular data but make very different trade-offs.
- [x] Column-oriented storage enables faster column scans
> ✓ Parquet reads only the columns requested. CSV must scan every row.
- [x] Data types are stored in the file: no silent coercion on read
> ✓ CSV has no type info; pandas must infer types and can silently misread values.
- [x] Columnar compression makes file sizes much smaller
> ✓ Similar values in a column compress well: often 5–10× smaller than equivalent CSVs.
- [ ] Parquet files are human-readable in any text editor
> ✗ Parquet is binary. You need DuckDB, pandas, or similar to inspect it.
- [ ] Parquet is natively supported by Excel
> ✗ Excel cannot open Parquet files without a plugin.
Sequence
Students drag items into the correct order. Use a numbered list without checkboxes.
NoteSyntax
```quizdown## Put these steps in order!> Optional hint.1. First step2. Second step3. Third step4. Fourth step```
---
shuffle_answers: true
---
## Put the EDA steps in order!
> Exploratory Data Analysis follows a natural progression. Skipping early steps often leads to missed data quality issues.
1. Load and inspect the raw data (`df.head()`, `.info()`, `.dtypes`)
2. Check for missing values and duplicates
3. Compute summary statistics (`df.describe()`)
4. Visualize distributions of individual variables
5. Explore relationships between variables (correlations, scatter plots)
6. Identify and investigate outliers
7. Document findings and decide on data cleaning steps
Scenario-based case studies
A single realistic scenario followed by multiple questions of different types (matching, classification, MCQ, and sequence) that progressively test deeper understanding of the same situation.
Start with a trigger event: a concrete problem or decision point, not a textbook setup. Include specific constraints (data size, hardware, timeline) that rule out default answers.
Long stems, short responses: the scenario and left-side prompts carry the complexity; right-side chips stay concise. This keeps cognitive load on reasoning, not reading.
Schema activation — identify the pieces without interference
Q2
Classification (with distractors)
Discrimination — sort under categories, reject noise
Q3
Single-choice MCQ
Tradeoff reasoning — given constraints, pick the best action
Q4
Sequence
Procedural — order the steps to carry out the decision
Feedback (Clark SBeL; METALS):
Each feedback line explains why the answer is correct or incorrect.
For wrong answers, name the specific misconception the student likely holds.
Reference the relevant course material so students can review.
Example: from CSV to deployed model
A data scientist has a 12 GB CSV of daily weather observations (200M rows, 15 columns). They need to build a regression model predicting rainfall. They work on a 16 GB laptop and eventually need to deploy the model as an API.
---
shuffle_answers: true
---
## Q1: Match each step to the tool or concept involved
> Think about which tool solves which problem: storage format, query engine, memory layout, or serving framework.
- Convert the 12 GB CSV to a compact columnar format for repeated queries :: Parquet
> Parquet's columnar layout enables column pruning and compression. A one-time conversion pays off on every subsequent read. Review: File Formats & Parquet (L2a).
- Run `SELECT avg(rainfall) ... GROUP BY region` on the full file without loading it all into memory :: DuckDB
> DuckDB streams parquet in chunks, applies projection and predicate pushdown, and accumulates only the aggregation result. Peak RAM ≈ result size, not file size. Review: DuckDB, Strategies & Alternatives (L2e).
- Pass the filtered query result from DuckDB to pandas without copying the underlying bytes :: Arrow
> Arrow is a cross-language in-memory columnar format. DuckDB and pandas both understand it, so `.to_arrow_table().to_pandas()` avoids serialization. Review: Serialization & Arrow (L2c).
- Serve the trained model as an HTTP endpoint that accepts JSON and returns predictions :: FastAPI
> FastAPI defines the route and validates input with Pydantic. uvicorn handles HTTP connections. The model is loaded once at startup with joblib. Review: Model Deployment (W4b).
## Q2: Classify each observation by the resource bottleneck it reveals
The data scientist profiles their workflow and records these observations. Classify each by the **primary bottleneck**: Storage, Memory, or CPU.
> Think about what each resource controls: Storage = bytes moving from disk, Memory = data held in the Python heap, CPU = computation cycles. Review: Working with Big Data Locally (L1c).
- `pd.read_csv()` on the 12 GB file takes 4 minutes; CPU usage stays at 8% throughout :: Storage
> Idle CPU during a file read means the processor is waiting for bytes from disk. The CSV must be fully scanned and deserialized row by row. Review: Working with Big Data Locally (L1c).
- Loading all 15 columns into pandas raises memory from 2 GB to 14 GB on the 16 GB laptop :: Memory
> The entire file materializes in the Python heap. String columns stored as individual Python objects expand well beyond the raw file size. Review: Working with Big Data Locally (L1c).
- A `.apply()` lambda on 200M in-memory rows takes 10 minutes; RAM usage stays flat :: CPU
> Flat memory and slow execution: the data is already loaded and the bottleneck is row-by-row Python computation. Vectorized operations would be faster. Review: Working with Big Data Locally (L1c).
- Switching from CSV to Parquet cuts load time from 4 min to 40 seconds with no code change :: Storage
> Parquet's binary columnar encoding reduces the bytes that move from disk. The saving is in I/O, not computation. Review: File Formats & Parquet (L2a).
- DuckDB `GROUP BY` on 200M rows completes in 0.8s using all cores at 400% CPU :: CPU
> Data is already accessible via parquet; vectorized computation across multiple cores is CPU-bound. This is a healthy bottleneck: the CPU is fully utilized. Review: DuckDB, Strategies & Alternatives (L2e).
- :: Network
> Network bandwidth matters for cloud data transfers (S3, API calls), but this scenario describes local file processing on a laptop. No network involved.
- :: GPU
> GPUs accelerate deep learning and matrix operations, but pandas, DuckDB, and scikit-learn use CPU. GPU is not a relevant bottleneck in this pipeline.
# Q3: The model training exceeds the laptop's resources. What is the most appropriate next step?
Cross-validation requires fitting 200 hyperparameter combinations. Each fit on the 12 GB dataset takes 8 minutes and peaks at 15 GB RAM, dangerously close to the laptop's 16 GB limit. Several fits have already crashed with `MemoryError`. The team needs results by end of day.
> Think about what kind of resource problem this is and which tool is designed to solve it. Review: ML on the Cluster (W3f), Worked Example (W3b).
1. [ ] Add swap space and run the fits sequentially overnight on the laptop
> 200 × 8 min = 26 hours, well past the EOD deadline. Swap space may prevent crashes but will slow each fit further due to disk-based virtual memory. This does not solve the time or memory problem. Review: ML on the Cluster (W3f).
1. [x] Provision an EMR cluster and distribute the search across Spark MLlib executors
> Spark MLlib's `CrossValidator` distributes parameter combinations across YARN executors in parallel. Each executor has its own memory, so the 15 GB peak per fit is no longer a problem. With 8 executors, the search completes in under an hour. After finding the best parameters, refit with scikit-learn for a portable model. Review: ML on the Cluster (W3f), Worked Example (W3b).
1. [ ] Reduce the training set to 10% so each fit uses less memory
> Subsampling avoids the memory crash but throws away 90% of the data, degrading model quality. The real problem is that the laptop is too small for the dataset, not that the dataset is too large for the model. Review: ML on the Cluster (W3f).
1. [ ] Upload the data to S3 and use Athena to run the model training
> Athena is a serverless SQL engine for analytical queries. It cannot run scikit-learn or Spark MLlib. It has no mechanism for model fitting. Review: SQL on the Cluster (W4c).
## Q4: Put the deployment steps in order
After finding the best hyperparameters on Spark and refitting with scikit-learn, the data scientist deploys the model as a prediction API on EC2.
> Think about what must exist before each subsequent step can work. Review: Model Deployment (W4b).
1. Save the trained model to a file with `joblib.dump()`
2. Write a FastAPI app with a `/predict` endpoint that loads the model at startup
3. Launch an EC2 instance and upload the model file and app code
4. Start the server with `uvicorn app:app --host 0.0.0.0` inside a `screen` session
5. Test the endpoint with `curl` or `requests.post()` from another machine
Print review mode
After a student answers all questions and clicks ✓✓ evaluate, pressing Cmd/Ctrl+P replaces the interactive quiz with a static review sheet showing:
Every question with its student-selected answers
The correct answer(s) highlighted
Per-option / per-pair feedback written into the question
Pass / needs review status per question
All questions appear at once, no navigation needed. This makes printed quizzes useful as study notes.
Tip
Try it: go back to the full quiz at the top, answer all five questions, click ✓✓, then press Cmd/Ctrl+P. The print dialog will show the full review sheet.
Embedding quizzes in your pages
Basic usage
Add the filter to your document front matter, then write a quiz inside a ```quizdown code block:
You can embed a quiz directly inside a Quarto callout. Students expand it when they’re ready:
::: {.callout-note collapse="true" title="✏️ Check your understanding"}```quizdown## Which of these is a supervised learning task?1. [ ] Clustering customers by behaviour1. [x] Predicting whether a loan will default > Correct. The model is trained on historical loan outcomes (labels).1. [ ] Dimensionality reduction with PCA```:::
Live example:
Note✏️ Check your understanding
---
shuffle_answers: true
---
## Which of these is a supervised learning task?
> Supervised learning requires labelled training data: examples where the correct output is already known.
1. [ ] Clustering customers by purchase behaviour
> No. Clustering is unsupervised. There is no target label.
1. [x] Predicting whether a loan will default
> Yes. The model is trained on historical loan outcomes (default / no default) as labels.
1. [ ] Reducing 100 features to 2 dimensions with PCA
> No. PCA is unsupervised dimensionality reduction.
1. [ ] Grouping news articles by topic with LDA
> No. LDA topic modelling is unsupervised.
Quiz configuration options
Pass YAML front matter inside the quiz block to configure it:
A single ```quizdown block can hold multiple questions. Question type is inferred from the list syntax.
Question type
List syntax
Correct marker
Single choice
Ordered list 1.
[x] on one item
Multiple choice
Unordered list -
[x] on one or more items
Sequence
Ordered list 1.
No [x] — order is the answer
Matching / classification
Unordered list -
Left :: Right pairs
Feedback and hints
## Question headingOptional overall hint as a blockquote.> Hint text shown when student clicks the lightbulb 💡.- [x] Correct option > Per-option feedback: shown after evaluation.- [ ] Wrong option > Explain why this is wrong.
For matching and classification questions:
- Situation or scenario :: Answer or label > Per-pair feedback: shown after evaluation for this row.- :: Distractor answer > Distractor feedback: shown when the student places > this chip in any slot and evaluates.