Quarto Workshop

🤖 AI-assisted reproducible publishing with Quarto

Ravi Kalia, PhD

🌍 Prefer Spanish or another language? The Google Translate extension translates these slides; code stays as written.

🤖 AI Usage Statement for These Slides

These slides were written with Claude Code (Claude Opus 5.5, Sonnet 5.5, Opus 5 and Fable 5.1), which drafted slides, Quarto files and code and re-rendered the deck. The author directed the work, reviewed and ran the code, and takes full responsibility for the content.

🏷️ Tool + version · 🛠️ Tasks · ✅ Checks · 🙋 Accountability · 📜 what a statement needs

🗓️ Agenda

🇬🇧 Part 1: a London guide · 💧 Part 2: Colombian water-quality research

⏱️ Block Min
0:00 👋 Welcome · 🎯 objective · 🏁 outcome 10
0:10 🧭 What Is Quarto? 5
0:15 🧰 Ready to Prompt · ✋ fix failed checks · boxes · 🗣️ prompting 15
0:30 🇬🇧 Part 1 · London: Markdown → HTML, rules, environment 35
1:05 ☕ Break · 🛟 catch up 15
1:20 🇬🇧 Part 1 · .qmd · 🗂️ three audiences · 🌍 publish · 🔁 change an input 65
2:25 🍽️ Meal Break · 🛟 catch up 30
2:55 💧 Part 2 · Colombian water quality: why · data · 🔍 explore · 🧹 prepare · 🧩 cluster 85
4:20 ☕ Break · 🛟 catch up 15
4:35 💧 Part 2 · 🏷️ Interpret · ⚠️ correlation ≠ causation · 📤 publish many 75
5:50 🏁 Done · 🔗 show your sites · 👋 finish at 6:00 10

🏠 before the workshop: 🛠️ Setup, 6 steps · then 📖 Read Before the Day · ⭐ = optional, self-paced

🎯 Objective · 🏁 Outcome

🎯 Objective

Publish reproducible content with Quarto, written by a coding assistant you direct and check.

🏁 Outcome

Plain-text Markdown becomes

📄 docx 📕 pdf 📊 pptx 🎞️ revealjs 📖 epub ⭐ 🌐 website ✍️ blog 📈 dashboard

🤖 with the code written by your coding assistant

🔁 change an input, rebuild, and explain what updated

🌍 ¿Prefieres leer en español? La extensión Google Translate traduce estas diapositivas; el código no cambia. · Google Translate

🌍 Expected Outcomes

Create and publish online content like these examples:

🇬🇧

London guide

Travel guide · dashboard · flyer

💧

Colombian water safety

Analysis · report · blog

✍️

ML blog

Posts · code · math

🧩 All three, and these slides (📄 index.qmd), are built with Quarto.

🔗 Finish with your own shareable websites.

🧭 What Is Quarto?

⏱️ 0:10

Quarto = Markdown → Anything

Two inputs, a .md Markdown file and a .qmd file with code in Python, R, bash, SQL, MATLAB, Java, JavaScript or C, flow into Quarto, shown by its four-quadrant logo, which combines Pandoc with Jupyter and knitr. Quarto produces four groups of formats: documents (html, pdf, docx, odt and, highlighted, epub); presentations (revealjs, pptx, beamer); the web (website, blog, dashboard); and more (Markdown, Jupyter notebooks, JATS, wikis).

🆓 Free and open source · no licence to buy, for anyone · MIT License

📚 All Quarto output formats · Quarto logo: Posit, PBC

🚫🤖 Quarto Is Not AI

⚙️

Traditional software

same input → same output, every time

📚

Built for publishing

quality documents, slides and websites

🤖

The assistant is separate

it writes the files · Quarto turns them into formats

🔁 No AI inside Quarto: it follows your source exactly

🔁 Reproducible: Same Source, Same Result

📄

One source

text + code + data in plain files

🧮

Numbers are computed

never typed or pasted in

▶️

One command

quarto render rebuilds everything

🕰️

Versioned with git

every change saved · uv.lock pins the tools

✏️ Change one value in the data → render → table, chart and sentence all update

🏷️ git (set up next) records which version of the files produced which result

💬 You correct one number in the data. What do you retype in the report?

🛠️ Setup

🏠 before the workshop

🧭 6 steps, in order · ✅ the 🧪 test run says 42 = ready

🧭 Setup Map: 6 Steps

🏠 before the workshop

🧭 Do this ✅ It worked when
1 🧰 Get your computer ready git --version runs · 🪟 wsl -l -v shows VERSION 2
2 📁 Move around the terminal after cd ~ the prompt shows ~
3 🧩 Install a coding assistant it starts, signed in
4 🐧 Assistant installs the tools it prints every version
5 🛠️ Setup check every check says OK
6 🧪 Test run hello.pdf says 42

🧭 = do it · 📖 = just read · 🆘 stuck → AI chat, then Discord · ✅ all 6 done? 📖 Read before the day

🎒 Have ready: a free GitHub account · a paid assistant plan or API key · the Discord invite

📶 You Need a Stable, Fast Internet Connection

🏠 before the workshop 📖 reading

⬇️

Big downloads

Ubuntu, Quarto, the assistant, Python packages, data

☁️

The assistant is online

every reply needs the connection · it stops when the Wi-Fi drops

🏠

Do setup at home

wired or strong Wi-Fi · avoid hotels, trains, firewalled work networks

🔌 Plugged in · 📵 No VPN or proxy if you can · 🧪 Test run done at home = ready

🆘 Error? Ask an AI Chat

🏠 before the workshop 🆘 when stuck

📋

Copy

the command you ran + the full error text

🌐

Paste into a web chat

Gemini, Claude or ChatGPT in your browser, with your system (Windows + Ubuntu, or Mac)

🔧

Try the fix

one suggestion at a time · read it before you run it

🔁

Still failing?

paste the new error · then Discord #help

🔒 No passwords, keys or private data in the paste

💬 Discord: Our Workshop Room

🏠 before the workshop 💬 get help

🔗

Join the server

open the invite link from the organiser · pick a name we know

#️⃣

Text channels

one topic each, like #help · type, paste, share screenshots

🔊

Voice channels

click to join, click to leave · mute until you speak

📣

Tag someone

type @ and their name · they get a notification

🆘 Stuck? Post in the help channel: what you ran, the error, a screenshot

🔁 Discord ↔︎ WhatsApp

🏠 before the workshop 💬 get help

💬 Discord 📱 WhatsApp
Server Community
Text channel Group chat
Voice channel Group call you can join and leave
@name tag @name mention
Reply to a message Reply to a message
Direct message Chat with one person
Invite link Group invite link
Reaction Reaction

⚠️ Unlike a WhatsApp group, a Discord server has many channels: pick the right one

🧰 Get Your Computer Ready

🏠 before the workshop 🧭 Step 1/6 do only your system’s line

🪟 Windows 👉 Open the Windows setup page

🍎 Mac

xcode-select --install

🐧 Ubuntu, Debian

sudo apt update && sudo apt install -y curl git

🪟 9 short steps · 🛡️ admin account · 🔁 one restart: 📱 open it on your phone too

🍎 ⌘ Space, type Terminal, Enter · paste · click Install · wait

🐧 Ctrl+Alt+T · password stays hidden · other Linux: install curl and git your usual way

Exercise · ⏱️ 20 min

git --version prints a version. 🪟 In Ubuntu, and wsl -l -v shows VERSION 2.

📁 Move Around the Terminal

🏠 before the workshop 🧭 Step 2/6 🚀 Open it: 🪟 Start → Ubuntu · 🍎 ⌘ Space → Terminal · 🐧 Ctrl+Alt+T

📍 pwd where am I?

📅 date today

👀 ls what is here?

🆕 mkdir name make a folder

🚪 cd name go in · ⬆️ cd .. up one

🏠 cd ~ home, from anywhere

🗑️ rmdir name delete folder

Exercise · ⏱️ 4 min

you typed these, rmdir raised no error, and the prompt shows ~.

👀 Watch, then type the bold words, Enter after each · ~ = your home folder · 🍎 Mac: ana@mac ~ %, last folder only

🧩 Install a Coding Assistant

🏠 before the workshop 🧭 Step 3/6 🪟 in Ubuntu · 🍎 claude not found? Paste the fix it printed, then a new tab

✳️ Claude Code · setup

curl -fsSL https://claude.ai/install.sh | bash

🌀 Codex · guide

curl -fsSL https://chatgpt.com/codex/install.sh | sh

♊ Antigravity · docs

curl -fsSL https://antigravity.google/cli/install.sh | bash

1️⃣ Pick one: paste its line, press Enter · 2️⃣ New tab: 🪟 Ctrl+Shift+D · 🍎 ⌘T · 🐧 Ctrl+Shift+T

3️⃣ Start it: mkdir -p ~/setup && cd ~/setup, then claude, codex or agy · trust this folder? ↓ to Yes, then Enter (👀 screens)

4️⃣ Sign in: no browser? Copy the link it prints, whole, into yours · a code appears? Paste it back here

Exercise · ⏱️ 5 min

the assistant answers when you type hello. Then type /exit.

👀 Started? What You Should See

🏠 before the workshop 🧭 Step 3/6, screen by screen

⌨️ Start: in the terminal, type the bold words, then Enter · ✳️ first asks for a text style: Enter · ➡️ each next screen

🔑 Sign in: Enter on 1. · no browser? Copy the link it prints, whole, into yours · 🌀 or 2., a code

🛡️ Trust: ✳️ ↓ to Yes, I trust this folder, then Enter · quit by mistake? Type claude again · 🌀 not in ~/setup: later, in a git folder

✅ Ready: type hello, then Enter, and it answers · /exit quits

⌨️ Two Places to Type

🏠 before the workshop 📖 read before step 4

🖥️ The terminal · its line ends in $ (🍎 %)

commands: pwd, cd, quarto preview

🤖 The assistant · it runs inside the terminal

> Which tools are missing?

plain language, and / commands: /exit

🚪 In: type claude · Out: type /exit · box colours say which place

➕ Assistant busy and a green box to do? Second tab: 🪟 Ctrl+Shift+D · 🍎 ⌘T · 🐧 Ctrl+Shift+T

😌 Wrong place? Usually harmless: command not found, or the assistant just reads it

💬 The assistant is running and a slide says to type gh auth login. Where do you type it?

🐧 The Assistant Installs the Tools

🏠 before the workshop 🧭 Step 4/6 🪟 in Ubuntu · 👋 /exit first · not Claude? codex or agy

mkdir -p ~/setup && cd ~/setup && claude
Check which of `git`, `curl`, `uv`, `quarto` (1.10 or newer) and the GitHub CLI `gh` are missing on this computer. Install only those, using each tool's official method for my system. Never run `sudo`, or an installer that asks for a password, yourself: print each such command alone in a code block, using full paths, then wait until I say done. Then print every version.

👀 Read each command it asks about, then approve · 🔐 A command for you (sudo = administrator)? Paste it into a second tab, type your password (hidden), then type done here

🍎 Password = your Mac login · “Command line developer tools” pop-up? Click Install, wait, then type done

Exercise · ⏱️ 10 min

it prints a version for all five tools. Then type /exit.

🛠️ Setup Check

🏠 before the workshop 🧭 Step 5/6 🪟 in Ubuntu · ➕ new tab first · not Claude? codex or agy

cd ~/setup && claude
Print the versions of `quarto` (1.10 or newer), `uv`, `gh` and this assistant, and run `quarto check` (no Jupyter or LaTeX yet is fine: count it as OK). If `git config --global user.name` or `user.email` is unset, ask me for my name and my GitHub email, then set them. Clone https://github.com/project-delphi/quarto-workshop.git to `~/quarto-workshop` (if it exists, `git pull` only when its working tree is clean) and list its `checkpoints/`. Run `gh auth status`. Run every check even if one fails, then report each as OK or not.

🔑 Not “Logged in”? In a second tab type gh auth login: GitHub.com → HTTPS → Yes → web browser, copy the code to github.com/login/device, then ask the assistant to check gh again · 🔧 Tool missing? Rerun the 🐧 install prompt

Exercise · ⏱️ 4 min

the assistant reports every check as OK, including gh “Logged in”.

🧪 Test Run: Quarto → PDF

🏠 before the workshop 🧭 Step 6/6 👋 /exit first · not Claude? codex or agy

mkdir -p ~/quarto-test && cd ~/quarto-test
claude
Run `uv init --bare` and `uv add ipykernel nbclient nbformat pyyaml`. Create hello.qmd (html and typst formats): a Python cell computing `6 * 7` and an inline-code sentence showing it. Render it with `uv run quarto render hello.qmd`; check hello.html and hello.pdf both say 42.

👀 Finished? /exit, then open the folder: 🪟 explorer.exe . · 🍎 open . · 🐧 xdg-open .

🤷 Part 1 explains hello.qmd · ⚠️ “Kernel is running over TCP”: harmless

Exercise · ⏱️ 5 min

you opened both files, each says 42, and you told the organiser.

📖 Read Before the Day

🏠 before the workshop

🤖 the assistant · 💸 costs · 📜 AI policy · 📝 Markdown

🧠 Model + 🔧 Harness = 🤖 Assistant

🏠 before the workshop 📖 reading

🧠

Model

reads and writes text, asks for tools

🔧

Harness

files, commands, permissions

🤖

Coding assistant

model + harness in your terminal

🧠 The model decides · 🔧 the harness does · 👀 you direct and check

🔁 Many harnesses can run several models: same tool, different results

🏢 Who Makes Which Assistant?

🏠 before the workshop 📖 reading

🤖 Assistant Type this 🏢 Made by 🧠 Default models
✳️ Claude Code claude Anthropic Claude
🌀 Codex codex OpenAI (also makes ChatGPT) GPT
♊ Antigravity agy Google Gemini

✅ The first three are installed in Step 3 · pick one

💬 Same company for assistant and model: does that mean it is the best fit for your task?

📏 Main Models: Pick by Task

🏠 before the workshop 📖 reading in each column: cheapest at the top

✳️ Claude Good for 🌀 ChatGPT Good for
Haiku 5.5 ✅ from $ simple, high-volume tasks GPT-6 Luna $ focused, high-volume tasks
Sonnet 5.5 ✅ $$$ daily coding: speed + intelligence GPT-6.1 Terra $$$ visual reasoning, computer use, clear writing
Opus 5.5 $$$$ long-running coding and knowledge work GPT-6.1 Sol ✅ $$$ complex work, balancing intelligence and cost
Fable 5.1 $$$$$ demanding reasoning, long-horizon work GPT-6 Astra $$$$$ the most demanding work

✅ Enough today: Haiku or Sonnet on medium · Sol on low

📉 Expect the same capability 100–1000× cheaper

💬 Which would you pick to fix a typo, and which to join two datasets?

each $ ≈ 3× the API price per 1M tokens · uses paraphrased from the vendors’ docs · rows are not equivalents · Claude · ChatGPT, checked 2026-11-06 · 📉 so far ≈ 10× or more a year: a16z · Epoch AI

⌨️ Steer the Assistant: /model · /clear · /exit

🏠 before the workshop 📖 reading

🧠

/model

bigger + higher effort = more credits · Haiku or Sonnet on medium

🧹

/clear

task done? saves credits · files stay

👋

/exit · /quit

leave the assistant · type claude to return

💸 Every reply re-reads the whole chat: a long conversation costs more tokens each turn

🔁 Next task → /clear · 🪫 Prompt keeps failing → /model, higher effort · 👋 Done for today → /exit

💬 One task is finished and an unrelated one starts. Which command first?

🐦 One Prompt · 4 Models · 3 Efforts

🏠 before the workshop 📖 reading Same words each time: Generate an SVG of a pelican riding a bicycle

Haiku 5.5 Sonnet 5.5 Opus 5.5 Fable 5.1
low Haiku at low effort: white pelican with one orange leg on a simple red bicycle, bright green grass, yellow sun. 0.09¢ Sonnet at low effort: white pelican with a wing reaching the handlebars of a red bicycle, yellow sun. 1.63¢ Opus at low effort: white pelican on a red bicycle with speed lines and a yellow sun. 3.79¢ Fable at low effort: black-outlined pelican on a black bicycle with speed lines, pale background. 10.02¢
medium Haiku at medium effort: grey-winged pelican on a red bicycle with a proper frame and speed lines. ✅ 0.14¢ Sonnet at medium effort: white pelican on a thin red bicycle, legs bent towards the pedals, yellow sun. ✅ 1.80¢ Opus at medium effort: crested pelican with a large pouch on a red bicycle with a chain ring, yellow sun. 5.89¢ Fable at medium effort: white pelican on a red bicycle with black wheels, pale sky. 9.91¢
high Haiku at high effort: pelican leaning forward on a red bicycle, two orange legs, yellow sun. 0.25¢ Sonnet at high effort: pelican with a curved neck and feathered wing on a red bicycle with a ground shadow. 2.34¢ Opus at high effort: crested pelican on a small red bicycle with a chain ring, clouds and speed lines. 6.74¢ Fable at high effort: white pelican with a long wing on a black bicycle, on a road with dashed lines. 13.09¢

💬 ✅ is enough here. Is 13¢ worth 90 times a 0.14¢ pelican? · 🕰️ Every pelican since 2024

Images: Simon Willison’s pelican series, one run per cell (Haiku, Sonnet, Opus, Fable) · ¢ = its tokens at API list prices, checked 2026-11-06 · 🌀 GPT grid

📂 Your Root Folder = The Assistant’s World

🏠 before the workshop 📖 reading

📍

Start it in a folder

that folder is its root: it reads, edits and runs commands there

🤝

“Trust this folder?”

answer yes only for a folder you chose for the work

🚧

Not ~, not Documents

a small project folder keeps everything else out of reach

🔄

New project?

/exit · cd to its folder · start the assistant again

🏠 pwd before you start: it prints the root

📜 Using AI in Research: Say It, Own It

🏠 before the workshop 📖 reading

🚫

Never an author

AI cannot be accountable

🔍

You verify everything

code, numbers, references

🔒

Keep private data out

unpublished or sensitive work stays out of public tools

📝

Disclose the use

tool + version · task · your checks

⚖️ Never generate or alter primary data or images: that is misconduct

💬 Which needs a statement: fixing a typo, or computing a table? · 📝 An example

📖 Markdown in Y Minutes

🏠 before the workshop 📖 reading

👉 learnxinyminutes.com/markdown · 🇪🇸 en español

#

# headings

✳️

*emphasis* · **bold**

📋

- and 1. lists

🔗

[link](url)

![image](file)

🧮

| tables | · ``` code fences

Exercise · ⏱️ 5 min

you can name the Markdown for a heading, a link and a table.

🧰 Ready to Prompt

⏱️ 0:15

✋ Setup check or 🧪 test run failed? Ask a helper now

🧰 Three Boxes, Three Roles

👀 An example: you will paste prompts like this from Part 1 on.

Create london.md with a level-1 heading "Visit London" and one welcome paragraph.

writes london.md · may run quarto render london.md

approve its commands · open london.html in your browser

🧪 Prompts Are Engineered · 🔍 Ask When Unsure

🧪

Engineered prompts

designed with AI to reduce errors

❓

Don’t understand it?

ask an AI or search the docs

🔍

Dig deeper

what it does, why, when to use it

Explain what a Markdown table is and when to use it. Then name the official docs page where I can verify your answer.

🗣️ How to Prompt Your Coding Assistant

🎯

Be specific

file names, columns, chart types

☝️

One step at a time

one task per prompt

✅

Ask it to run & check

“…render it and tell me about errors”

👀

Check what changed

you are responsible for the result

🌍 ¿Español? Your first prompt can be: Responde siempre en español; deja el código y los nombres de archivo en inglés.

💬 A Free Web Chat Can Draft Your Prompt

An animated example in five steps. A free web chat, such as Gemini, gets a request typed in plain words: a one-page London visitor guide, and a prompt for a coding assistant, naming the tools Quarto and Markdown, of one to four lines with one task, concrete file names and a check. The chat replies with a prompt: Use Quarto and Markdown, create london.md with the title Visit London, a level-1 heading and a two-sentence welcome, add a heading Top sights with three bullets each naming a place and why to visit, run quarto render london.md, then list the headings in london.html and any errors. The Copy button is pressed. The prompt is pasted into a coding assistant in the terminal, which writes london.md, runs quarto render london.md, lists the headings in london.html and reports no errors.

Plain words in a web chat → a ready prompt → your coding assistant runs it.

💬 Web chat: draft · ✅ you read it · 🤖 assistant: run it. Never paste private data.

🇬🇧 Part 1 · London in Markdown

⏱️ 0:30

🗺️ Part 1 in Five Blocks

📝

0:30 Markdown → HTML · rules · environment

🧪

1:20 .md → .qmd: YAML · code · docx + PDF

🗂️

1:40 🤖 three audiences in parallel

🌍

1:55 publish to GitHub Pages

🔁

2:15 change an input, no prompt given

🏁 a live dashboard URL on your phone 📱 · 🔁 you explain what a changed input updates

☕ 1:05 after the first block · 🍽️ 2:25 before 💧 Part 2

🏛️ You Work for London’s Tourism Office

✍️ write one guide → ♻️ reuse it for every audience

📁 ~  (your home folder)
├── 📁 quarto-workshop/  ← clone
└── 📁 london-guide/     ← you
cd ~
mkdir -p london-guide && cd london-guide
claude

or codex / agy instead of claude

😰 Stuck for 2 min? Paste the exercise’s Catch me up prompt: 💾 backup → 📋 checkpoint → ▶️ render → ➡️ rejoin at the next ⏱️ block · ✋ approve access to ../quarto-workshop when asked

1️⃣ Heading · Paragraph · Emphasis

Create london.md with a level-1 heading "Visit London" and a welcome paragraph with one bold and one italic phrase.
# Visit London

Welcome to **London**: free museums, royal history and *riverside walks*.

Visit London

Welcome to London: free museums, royal history and riverside walks.

Exercise · ⏱️ 1 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

london.md has a heading and a welcome paragraph.

▶️ Render It

Render london.md with quarto render and tell me which file it wrote.

quarto render london.md → london.html

in a second terminal tab (Windows Terminal: Ctrl+Shift+D):

cd ~/london-guide && quarto preview london.md

live view, reloads on every change · Ctrl+C stops it

🚫 YAML · 🚫 code · ✅ a web page

Exercise · ⏱️ 3 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

london.html opens and updates whenever london.md changes.

📜 AGENTS.md: Rules the Assistant Reads

🎯

Scope

what the project is, and is not

📏

Rules

what must always hold

⌨️

Commands

how to build and check it

🔁

Every session

Claude Code, Codex and Antigravity read it first

✍️ say it once → every session follows it

📜 Rules for the Guide

Create AGENTS.md for this project. Scope: a London visitor guide for the tourism office, written once and reused for every audience. Rules: every number comes from a cited file in data/, photos are openly licensed, credited and show no people, never invent facts, keep data/ and images/ unchanged, and call ALVA counts "ALVA member attractions". Show me the file.

writes AGENTS.md: a scope and a list of rules

approve · read it: does it say what you meant?

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

AGENTS.md states the guide’s scope and its three rules.

2️⃣ Lists

In london.md, add a "Five sights to start with" numbered list (British Museum, Natural History Museum, Tate Modern, National Gallery, Tower of London) and a "Getting around" bullet list: 🚇 Tube, 🚌 Bus, 🚲 Santander Cycles.
## Five sights to start with

1. British Museum
2. Natural History Museum
3. Tate Modern
4. National Gallery
5. Tower of London

## Getting around

- 🚇 Tube
- 🚌 Bus
- 🚲 Santander Cycles

Five sights to start with

  1. British Museum
  2. Natural History Museum
  3. Tate Modern
  4. National Gallery
  5. Tower of London

Getting around

  • 🚇 Tube
  • 🚌 Bus
  • 🚲 Santander Cycles

Exercise · ⏱️ 1 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

each list has its own heading.

4️⃣ Table

Copy ../quarto-workshop/examples/london/data here. In london.md, add a "Sights at a glance" table from data/sights.csv: a photo from images/ (height=60, no unit; alt text), Sight, Price band, Nearest tube and a "Read more" link to its `official_page`. Credit the photos (images/ATTRIBUTION.md) and the sources.
| Photo | Sight | … | More |
|---|-----|---|----|
| ![The British Museum's glass roof](images/british-museum.jpg){height=60} | British Museum | … | [Read more](https://www.britishmuseum.org/visit) |
Photo Sight Price band Nearest tube More
The British Museum’s glass roof British Museum Free Tottenham Court Road Read more
A turret of the White Tower Tower of London Paid Tower Hill Read more

2 of your 5 rows: one free, one paid

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

every sight has a photo and a working “Read more” link.

5️⃣ Code Fence

In london.md, add a "Before you go" section: a three-item checklist inside a plain text code block.
## Before you go

```text
[ ] Contactless card or Oyster card
[ ] Free museum tickets booked
[ ] Comfortable shoes and an umbrella
```

Before you go

[ ] Contactless card or Oyster card
[ ] Free museum tickets booked
[ ] Comfortable shoes and an umbrella

Exercise · ⏱️ 1 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

the preview keeps the checklist’s line breaks and brackets.

🧰 Environments: Then and Now

💻

OS

macOS · Windows · Linux

🔩

Chip

Intel/AMD x86_64 · Apple/ARM arm64

🗣️

Language

Python · R · JS … which version?

📦

Dependencies

packages · versions · compiled C

🕰️

Then

conda · compilers · PATH · kernels → ⏳ hours

⚡

Now 🐍 uv

🔒 one lockfile, every OS and chip → ⏱️ seconds

🤖 either way: the assistant sets it up · you approve

📦 A Python Environment for the Guide

Give this folder its own Python environment with `uv`: run `uv init --bare`, then `uv add ipykernel nbclient nbformat pyyaml pandas matplotlib plotly`. From now on run `quarto` and `python` through `uv run`, and add that rule to AGENTS.md. Show me the Python line of `uv run quarto check jupyter`.

📦 pyproject.toml · 🔒 uv.lock · .venv/ · a rule in AGENTS.md

approve its commands

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

uv run quarto check jupyter reports the Python in this folder’s .venv.

🛟 Stuck? Catch Up, Then Rejoin

⏱️ stuck 2 min → 🛟 Catch me up → ➡️ next ⏱️ block

Catch me up: follow ../quarto-workshop/checkpoints/01-london-markdown/README.md

💾 ../london-guide-backup-01-<time> · 📋 copies the checkpoint · ▶️ renders · ➡️ names the slide to rejoin at

👀 watch the demo · behind, or london.md broken? paste it now

☕ Break

⏱️ 1:05

☕ 15 min · back at 1:20 for 🧪 .qmd

🛟 london.md won’t render? Ask for help now, or paste the 🛟 slide’s Catch me up

🧪 From .md to .qmd

⏱️ 1:20

🧾 YAML Front Matter

Rename london.md to london.qmd. Replace the "# Visit London" heading with YAML front matter: title "Visit London", author "London Tourism Office", date today, format html. Do not render; my preview rebuilds it.
---
title: "Visit London"
author: "London Tourism Office"
date: today
format: html
---

renames to london.qmd · edits the top

preview tab: Ctrl+C, then uv run quarto preview london.qmd

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/02-london-qmd/README.md

london.html shows the title, author and date.

🐍 Code That Runs

In london.qmd, add a Python cell that reads data/attractions.csv and finds the most visited attraction and total visits_2025, plus a sentence showing both with inline code. The preview re-renders it: check london.html.
```{python}
import pandas as pd
attractions = pd.read_csv("data/attractions.csv")
top = attractions.loc[attractions["visits_2025"].idxmax()]
top_name = top["attraction"]
top_visits = f"{top['visits_2025']:,}"
total_visits = f"{attractions['visits_2025'].sum():,}"
```
Most visited in 2025: the `{python} top_name`, with `{python} top_visits` visits.
London's ten most visited ALVA member attractions drew `{python} total_visits` visits.

Most visited in 2025: the Natural History Museum (South Kensington), with 7,116,929 visits. London’s ten most visited ALVA member attractions drew 39,692,434 visits.

💬 if attractions.csv changes, which numbers update?

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/02-london-qmd/README.md

the sentence shows numbers computed from the CSV.

🏷️ Code Cell Options

Add a matplotlib cell: a horizontal bar chart of visits_2025 by attraction. Hide its code, label it fig-top10, add a caption and alt text. The preview re-renders it: check london.html.
```{python}
#| label: fig-top10
#| echo: false
#| fig-cap: "Visits in 2025 (ALVA)."
#| fig-alt: "Bar chart of visits by attraction."
top10 = attractions.sort_values("visits_2025")
top10.plot.barh(x="attraction", y="visits_2025", legend=False)
```
Horizontal bar chart of 2025 visits to ten London attractions, longest bar at the top: the Natural History Museum is highest and Royal Museums Greenwich lowest.
Figure 1: Visits in 2025 (ALVA).

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/02-london-qmd/README.md

the figure has a number, a caption and no code.

🖨️ One Source, Three Formats

first: Ctrl+C in the preview tab, so two renders don’t collide · restart it after

In london.qmd, add docx and typst (label "PDF") formats with download links on the HTML page. Render all three; check each has the computed sentence and chart.
format:
  html:
    format-links:
      - docx
      - {format: typst, text: PDF}
  docx: default
  typst: default

uv run quarto render london.qmd → london.html · london.docx · london.pdf

🎯 one format only: --to docx

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/02-london-qmd/README.md

london.docx and london.pdf show the sentence and the chart.

🎞️ Three More Formats · ⭐ optional

In london.qmd, add pptx, epub and revealjs (london-slides.html) formats, each with a download link on the HTML page. Render all; check each has the computed sentence and chart.
format:
  html:
    format-links:
      # … docx, PDF
      - pptx
      - epub
      - {format: revealjs, text: Slides}
  pptx: default
  epub: default
  revealjs: {output-file: london-slides.html}

uv run quarto render london.qmd → six files

👀 watch the demo · try it later: PowerPoint or LibreOffice, an e-book reader, a browser

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/02-london-qmd/README.md

all six files show the sentence and the chart.

🗂️ One Guide, Five Audiences

⏱️ 1:40

🧠 Content Once, Format per Audience

👔

Managers

📈 dashboard

format: dashboard

📧

Partners

email attachment

format: docx

🧾

Walk-ins

A5 flyer · pdf

typst · papersize: a5

📱

Social · ⭐

12 cm square ad · pdf

raw {=typst} page size

🌐

Visitors

website · next block

project: website

🔁 change data/ once → every version updates

🤖 Sub-Agents: One Lead, Many Hands

🧑‍✈️

Lead

your assistant: splits the work, waits, renders, checks

🤖🤖🤖

Sub-agents

one task each, own context, all at once

📄

One file each

no two agents edit or render the same file

⏱️ three tasks ≈ the slowest one · 🪙 more tokens

Claude Code · Codex · Antigravity · checked 2026-10-31

🗂️ One Prompt, Three Audiences

Run three sub-agents in parallel, one per item (none? do the items in turn). Each reads london.qmd, data/ and images/, hides code, follows AGENTS.md and only writes its own files: no rendering.
1. london-dashboard.qmd (format: dashboard): Attractions page (value boxes: top attraction, total `visits_2025`, number free; Plotly bars by attraction; a Plotly line of visits by `year` from data/visits.csv; end each chart cell with `fig`, assigning `fig = fig.update_layout(…)`); Churches page (data/churches.csv, names linked to `visit_london`).
2. london-email.qmd: docx partner brief (2025 numbers as inline code, five sights with official links, getting around, sources); london-email-message.md: its covering email in a text block, no numbers.
3. london-flyer.qmd: one A5 Typst page (papersize: a5): Big Ben photo, five sights with price band and nearest tube, local tip.
Then render the three .qmd files yourself, one at a time, and check each.

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/04-london-audiences/README.md

you open a two-page dashboard, a Word brief and its email, and a one-page flyer.

📱 A Square Ad · ⭐ optional

Create london-social.qmd: one 12 cm square Typst page (raw Typst page size) for social media: "Visit London", the Big Ben photo, the top attraction and its `visits_2025`. Hide code, follow AGENTS.md, render it, and check that the PDF is one square page.

writes london-social.qmd · uv run quarto render london-social.qmd

open london-social.pdf

Exercise · ⭐ self-paced

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/london/README.md

london-social.pdf is one square page showing the top attraction.

🗺️ The Sights in 3-D · ⭐ optional

🗺️ 5 sights · ⛪ 10 churches · 〰️ the Thames between the boroughs

⬆️ height = 2025 visits (ALVA): a million = 250 m

🔄 up close · most visited: Natural History Museum, 7.1 M · paid: Tower of London

Visits: ALVA · places: Wikidata · boroughs: ONS, OGL v3.0, contains OS data © Crown copyright and database right 2024 · 🖱️ drag to turn · scroll to zoom

✨ Ask for three.js · ⭐ optional

A 3-D bar chart on a dark floor, drawn by three.js: 2025 visits to five ALVA member attractions. Natural History Museum 7.1 million, British Museum 6.4 million, Tate Modern 4.5 million, National Gallery 4.1 million, Tower of London 2.8 million.

🧊 three.js: 3-D in any browser

🗺️ It drew the last slide’s map

🆓 Free · MIT License

👀 Gallery · 🐦 One-prompt pelican

Create visits-3d.html with `three.js` from a CDN: a labelled 3-D bar per `attraction` in data/visits.csv, as tall as its `visits` in the latest `year`, that I can drag to turn. Put the numbers in the page so it opens by double-click, then check them against the CSV.

after the sub-agents finish · open visits-3d.html in your browser

Exercise · ⭐ self-paced

the bars turn, and the tallest bar’s number matches data/visits.csv.

🪟 WSL ↔︎ Windows · ⭐ optional

🐧 Ubuntu

  • ➕ Ctrl+Shift+D, then cd ~/london-guide
  • 📂 explorer.exe . in File Explorer
  • 💽 ls /mnt/c/Users/ Windows files
  • 🧑‍💻 code . VS Code, if installed

🪟 PowerShell

  • ➕ ▾ menu → PowerShell
  • 📋 wsl -l -v Ubuntu · Running · 2
  • 🔑 wsl --user root reset a password
  • ⚠️ wsl --shutdown stops every Ubuntu tab

🪟 Windows only · 🍎 open . · 👀 approve in the assistant’s tab

Exercise · ⭐ self-paced

File Explorer shows your london-guide files and wsl -l -v lists Ubuntu.

⌨️ Terminal Keys · ⭐ optional

📋 Ctrl+Shift+C / V

copy · paste (🍎 ⌘C / ⌘V)

🖱️ Right-click

🪟 copy a selection, or paste

✋ Ctrl+C

stop a command (🪟 copies a selection)

⇥ Tab

complete a name

↔︎️ Ctrl+A / Ctrl+E

line start · end

✂️ Ctrl+U / Ctrl+K

delete before · after the cursor (🍎 U: whole line)

🧹 Ctrl+L

clear the screen

🔎 Ctrl+R

search past commands

Ctrl+Shift+D (🍎 ⌘T) → ls ~/quarto-w Tab Enter → Ctrl+L → Ctrl+R ls → Ctrl+A → Ctrl+E → Ctrl+U

Exercise · ⭐ self-paced

Ctrl+R found your ls command and Ctrl+U cleared it.

🌍 Publish to GitHub Pages

⏱️ 1:55

🌳 Git: Every Change, Safely

🕰️ every saved version kept · 🐙 shared on GitHub

🌿

branch

main stays untouched

💾

commit

a snapshot + why

☁️

push

copy it to GitHub

🔀

pull request

propose the change

👀

review

someone reads the diff

✅

merge

into main

👤 today: straight to main · 👥 a team: the whole loop

🌐 GitHub Pages

Three steps: your source lives in a GitHub repository; quarto publish gh-pages renders the site and pushes the HTML to a gh-pages branch; GitHub serves that branch at user.github.io/repo.

🆓 free static hosting, straight from a GitHub repo

🏗️ A Project: One Folder → One Website

Make this folder a Quarto website: _quarto.yml (render only the .qmd files; navbar: Guide, Dashboard, a Downloads menu for the docx, pdf, pptx, epub and london-slides.html files that exist), index.qmd, and a .gitignore for /_site/, /.quarto/, /.venv/ and *_files/. Render and check the links.
project:
  type: website
website:
  title: "Visit London"
  navbar:
    left:
      - {text: Guide, href: london.qmd}
      - {text: Dashboard, href: london-dashboard.qmd}
      - {text: Downloads, menu: [...]}

writes 3 files · uv run quarto render → _site/

then: Ctrl+C in the preview tab, uv run quarto preview

Exercise · ⏱️ 3 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/london/README.md

the site opens with Guide, Dashboard and Downloads.

🚀 Publish

Initialise a git repository here. Replace README.md in this folder with a short one linking to https://<your-github-username>.github.io/london-guide/ and listing the files in the site's Downloads menu. Commit everything except _site/, .quarto/, .venv/ and *_files/, then create a public GitHub repository named `london-guide` with gh and push to it. Set the repository's website to the same URL with `gh repo edit --homepage`. Show me the README and the website URL you set.

approve gh · second tab: uv run quarto publish gh-pages, Enter at “Publish site … (Y/n)” · open the URL on your phone 📱 · 404? wait a minute, reload

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/london/README.md

https://<your-github-username>.github.io/london-guide/london-dashboard.html opens on your phone 📱

🆓 Free Static Hosts · ⭐ optional

📁 _site/ is plain files: ✅ any static host can serve it

GitHub Pages

📍 today · 📤 uv run quarto publish gh-pages

Netlify

📤 uv run quarto publish netlify

Cloudflare

📤 upload _site/ with wrangler

Vercel

⚠️ personal, non-commercial

GitLab Pages

⚙️ built by CI → gitlab.io

Render

⚙️ builds from Git → onrender.com

📤 you upload · ⚙️ the host builds: needs Quarto + Python · checked 2026-11-07

🔁 Change an Input

⏱️ 2:15

🔁 Change One Input, Explain the Rebuild

🧪 No prompt is given: write your own.

  1. 🔮 Predict: set the Tower of London’s visits_2025 in data/attractions.csv to 8000000. Which outputs will change?
  2. 🔁 Rebuild the site
  3. 🔎 Explain: which outputs updated, which did not, and why?
  4. ↩︎️ git restore data/attractions.csv, then rebuild so no trial number stays in your files

edit the number yourself: nano data/attractions.csv (Ctrl+O, Enter, Ctrl+X) · AGENTS.md keeps data/ unchanged for the assistant · ↩︎️ git restore needs the 🌍 Publish step

Exercise · ⏱️ 4 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/london/README.md

you can name the outputs that changed, the ones that did not, and why.

📑 A White Paper · ⭐ optional

Create london-whitepaper.qmd, an A4 Typst white paper in "New Computer Modern": an abstract (no inline code), numbered Introduction, Methods, Results and Conclusions, then References via `#set bibliography(title: [References])` (references.bib from `url`). Analyse data/visits.csv vs 2019 and data/attractions.csv by `admission`. Add it to `render` and the "Downloads" menu; render; check citations. Figures must be vector: set `fig-format: svg` and check that the files in `figure-typst/` end in .svg.
bibliography: references.bib
format:
  typst:
    mainfont: "New Computer Modern"
    section-numbering: "1.1"
    bibliographystyle: ieee

renders london-whitepaper.qmd

open london-whitepaper.pdf

Exercise · ⏱️ 8 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/london/README.md

london-whitepaper.pdf shows an abstract and numbered references.

📚 Part 1 Sources

🍽️ Meal Break

⏱️ 2:25

🍽️ 30 min · back at 2:55 for 💧 Part 2

🛟 London site not live yet? Ask for help now, or paste the 🚀 Publish slide’s Catch me up

💧 Part 2 · Colombia Water Safety and Infant Mortality

⏱️ 2:55

🗺️ Part 2 in Seven Stages

💧

2:55 why · rules · data

🔍

3:15 explore

🧹

3:35 prepare

🧩

3:55 cluster

🏷️

4:35 interpret

⚠️

4:55 correlation ≠ causation

📤

5:10 publish many

🏁 a live site: report · slides · blog · 🔁 an explained rebuild

☕ 4:20 after cluster · 🏁 5:50 show your sites

🏛️ Where the Data Comes From

🧪

INS · water quality (IRCA, from utilities)

🗺️

DANE · town codes · map

🎓

ICFES · MEN · exams · school dropout

💰

DNP · coverage · poverty · spending

💡 useful: find 📍 which towns get unsafe water · 🔁 anyone can rerun it

All open data via datos.gov.co and DNP TerriData · sources and licences: examples/water/data/README.md

📏 Two Measures: IRCA and Saber 11

💧 IRCA · water-safety risk

  • 🧪 tap-water samples against health limits
  • ⚖️ failed tests weighted by risk: E. coli 25 · turbidity 15 · pH 1.5
  • 📏 0 = no risk → 100 = unfit

🎓 Saber 11 · ICFES exam

  • 🧑‍🎓 students finishing secondary school
  • 📚 5 tests: reading, maths, social studies, science, English
  • 📏 global score 0–500

🧮 both are averages: IRCA over water samples · Saber 11 over students

Resolución 2115 de 2007, arts. 13–14 (normas.cra.gov.co) · ICFES, Saber 11.° Guía de orientación 2017-1, pp. 101 and 104 (icfes.gov.co)

🚰 Why Drinking Water?

0–5

✅ sin riesgo · no risk

5.1–14

🟡 bajo · low

5.7

🏙️ urban

7.4

🏭 utilities

14.1–35

🟠 medio · medium

16.4

🇨🇴 national

35.1–80

🔴 alto · high

36.5

🌾 rural

43.7

🪣 self-supplied

80.1–100

⛔ inviable sanitariamente · unfit

INS IRCA for 2023: MinSalud, MinVivienda, INS and Superservicios, INCA 2023 (Bogotá, January 2025), Tabla 4 · bands: Resolución 2115 de 2007, art. 15 (normas.cra.gov.co)

🧠 The Question

🧩 Which kinds of municipality exist?

🎯 How do 👶 health and 🎓 education outcomes differ between them?

📦 One Analysis, Many Deliverables

Municipal water data flows into one analysis script, analysis.py, which writes cluster outputs once. Seven publications read those outputs: a docx report, a pdf report, an epub report, revealjs and pptx slides for the council, a blog post, a website and a dashboard. All are published together on GitHub Pages.

🗃️ The Dataset

cd ~ && mkdir -p water-quality && cd water-quality
claude
Copy ../quarto-workshop/examples/water/data into this folder as data/, then list the files in data/ with their sizes.

1 row = 🏘️ municipality × 🏙️/🌾 zone × 📅 year

📊 31,416 rows · 29 columns · 1,122 codes · 2010–2023 · 📖 data/README.md

📜 Rules for the Analysis

Read data/README.md, then create AGENTS.md for this project. Scope: describe Colombian municipal water safety and infant mortality from data/ only. Rules: keep data/ unchanged, label IRCA as utility-supplied (prestadores), never cluster on outcomes, and say comparisons are descriptive, not causal. Show me the file.

reads data/README.md · writes AGENTS.md: scope, rules, the data’s caveats

approve · check that every rule is there

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/05-water-explore/README.md

AGENTS.md states the analysis’ scope and its four rules.

📦 The Project’s Environment

Give this project its own Python environment with `uv`: run `uv init --bare`, then `uv add ipykernel nbclient nbformat pyyaml pandas numpy matplotlib plotly scikit-learn pyarrow itables tabulate`. Run `quarto` and `python` through `uv run`, and add that rule to AGENTS.md. Show me the Python line of `uv run quarto check jupyter`.

📦 pyproject.toml · 🔒 uv.lock · .venv/ · a rule in AGENTS.md

approve · preview each page you make: uv run quarto preview explore.qmd

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/05-water-explore/README.md

uv run quarto check jupyter shows this project’s .venv Python.

🧩 Features vs 🎯 Outcomes

🧩 Cluster on

Water

  • 💧 IRCA (prestadores)
  • 🚰 piped water
  • 🚽 sewer

People & place

  • 💸 poverty (IPM)
  • 🌾 rural share
  • ⛰️ altitude (derived)
  • 🌧️ rainfall

Government

  • 🏛️ municipal performance (MDM)
  • 💰 water transfers per person (SGP)

🎯 Only compare

👶 infant mortality

🎓 Saber 11 score

🎒 school dropout

🚫 never cluster on outcomes

🔁 Compute Once, Publish Many

data/water_municipal.csv
        │   uv run python scripts/analysis.py
        ▼
outputs/clusters.parquet  +  outputs/cluster_profiles.csv
        │   every page reads them
        ▼
explore · clusters · report · slides · blog · ⭐ dashboard

🧮 compute once → 📤 never copy a number by hand

🔍 Explore

⏱️ 3:15

🔍 Look Before You Model

📐

rows × columns

❔

missing by column × zone

🎚️

IRCA by band × zone

📈

IRCA by year

🌾 Rural: Less Data, More Risk

Two horizontal stacked bars, urban and rural. The urban bar is mostly no-risk and low-risk IRCA with a small hatched no-data share. The rural bar is more than half hatched no-data, and the largest share of the rest is in the high-risk band.
Figure 2: All rows, 2010–2023, by zone: no IRCA reported (hatched grey) and Resolución 2115 risk band. Utility-supplied (prestadores) IRCA, INS open data nxt2-39c3.

❔ rural rows without IRCA: 53% · 🔴 rural rows with IRCA in high or unfit: 51%

🏙️ Explore Tunja Yourself

Tunja (DIVIPOLA 15001), water_municipal.csv, 2010–2023 · 💡 pick a view, then a variable

🔍 Explore the Data

Read data/README.md. Create explore.qmd from data/water_municipal.csv (divipola as text): rows and columns, % missing per column by `zone`, IRCA counts by Resolución 2115 band and `zone`, and median IRCA by `year` and `zone`, labelled utility-supplied (prestadores). Render it; report errors.

writes explore.qmd · uv run quarto render explore.qmd

open explore.html in your browser

Exercise · ⏱️ 10 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/05-water-explore/README.md

explore.html shows IRCA by zone for 2010–2023.

🧹 Prepare

⏱️ 3:35

🧹 Four Steps from Table to Features

✂️

select 2016–2023 · municipalities · with IRCA

🔄

transform log SGP · capitals use the municipal total 🚩

🩹

impute year × zone median 🚩

📏

standardise z-score within year

🚩 = a flag column, so nothing is filled silently

🧮 What We Keep

step rows
🏘️ municipality rows, 2016–2023 17,648
❌ no IRCA 6,411
❌ more than 2 features missing 2
✅ kept: 7,534 urban + 3,701 rural 11,235

🏘️ 1,090 municipalities · 🩹 653 rows imputed · 🏛️ 214 capital rows from totals

📏 Why Scale?

Horizontal bar chart with nine features. In raw units, rainfall and altitude together make up almost all of the distance and the other seven bars are invisible. After z-scores every feature contributes the same share, about 11 percent.
Figure 3: Share of the squared distance between rows that each feature contributes, in raw units and after z-scores (kept rows, 2016–2023).

⛰️🌧️ raw units: altitude + rainfall = 99.8% of the distance

🧹 Select and Transform

Read data/README.md. Create scripts/analysis.py with prepare(df): rows with entity_type "municipality", 2016–2023, with irca; features irca, water_coverage, sewer_coverage, ipm, rural_share, altitude_m, rainfall_mm, mdm, log(sgp_water_pc); fill missing coverage of "urban" rows with department_capital == 1 from *_total, flagged in <col>_from_total. Run it.

writes scripts/analysis.py · runs uv run python scripts/analysis.py

Exercise · ⏱️ 5 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/06-water-prepare/README.md

prepare() returns one row per kept zone-year with the nine features.

🧹 Impute and Scale

In prepare(), drop rows missing more than 2 features, fill the rest with the `year` × `zone` median plus <col>_imputed flags, then add z_<col> (z-score within `year`) beside the original columns; keep keys, outcomes and flags. Save outputs/prepared.parquet, print rows kept and dropped per step. Run it.

edits prepare() · uv run python scripts/analysis.py

Exercise · ⏱️ 5 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/06-water-prepare/README.md

the printed counts match 🧮 What we keep: 11,235 rows kept.

🧩 Cluster

⏱️ 3:55

🧩 k-Means in Four Steps

① drop 3 centres at random points

💧 water data: k = 4 is a hyperparameter, picked from k = 2–8 by an elbow rule · silhouette stays low (about 0.2), so k is a judgement call

🫧 A Picture of the Clusters

Scatter plot of about eleven thousand dots in four coloured groups that overlap at their edges. Cluster 4 spreads to the right, cluster 1 to the lower left and cluster 2 to the upper left; cluster 3 sits in the middle, drawn on top.
Figure 4: Each dot is a zone-year on the first two principal components of the 9 z-scored features, coloured by cluster. PCA is only for the picture; k-means used all 9 features.

🌀 Turn the Cloud

🫧 2 axes · 51% of variance · cluster 3: 53% of near neighbours in it

🔄 3 axes · 67% · 90%

PC3 ≈ ⛰️ altitude (derived) − 🌧️ rainfall: cluster 3 sits below the others

Each point is a zone-year on the first three principal components of the 9 z-scored features, by cluster · 🖱️ drag to turn · scroll to zoom

🏔️ Where the Clusters Stand

🗺️ 966 urban zones, 2023: a column each, coloured by cluster

⛰️ height = head-town altitude, ×50

🏔️ cluster 2: median 1,766 m · cluster 3: 130 m

Columns stand at municipality centroids; heights are derived SRTM altitudes of the head towns · labels: the 10 most populous department capitals and Tunja · 🖱️ drag to turn · scroll to zoom

🛤️ Use the Model, or Build It

📤

Use it · the normal route

copy each block’s finished analysis.py, then build the pages

🧗

Build it · ⭐ challenge

paste the ⭐ prompts · stuck? the 📤 prompt

Copy ../quarto-workshop/checkpoints/07-water-cluster/scripts/analysis.py over scripts/analysis.py (keep mine as scripts/analysis-before-07.py), run it with `uv run python scripts/analysis.py`, and tell me k.

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/07-water-cluster/README.md

outputs/clusters.parquet exists and you know k.

🧩 Run k-Means · ⭐ challenge

In scripts/analysis.py, run k-means on the z_ features for k=2–8 (random_state=42, n_init=20); save inertia and silhouette to outputs/k_selection.csv; pick k by the elbow, check silhouette and 5-seed ARI; number clusters by centroid IRCA; save outputs/clusters.parquet (prepared columns + `cluster`, pc1, pc2). Run it; explain k.

edits scripts/analysis.py · uv run python scripts/analysis.py

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/07-water-cluster/README.md

outputs/clusters.parquet has a cluster for every kept row, and 🤔 you can explain its choice of k.

🧩 The Clusters Page

Create clusters.qmd that reads outputs/k_selection.csv and outputs/clusters.parquet and shows an elbow plot, silhouette by k and a PCA scatter (pc1, pc2) coloured by cluster, each with a caption and alt text. Render it and tell me about errors.

writes clusters.qmd · uv run quarto render clusters.qmd

open clusters.html in your browser

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/07-water-cluster/README.md

clusters.html shows the elbow, the silhouette and the PCA picture.

☕ Break

⏱️ 4:20

☕ 15 min · back at 4:35 for 🏷️ Interpret

🛟 No clusters.html yet? Ask for help now, or paste the 🧩 Clusters Page slide’s Catch me up

🏷️ Interpret

⏱️ 4:35

🌡️ What Sets Each Cluster Apart?

Heatmap with four rows, clusters 1 to 4, and nine feature columns. Cluster 4 is red for IRCA and poverty and dark blue for water and sewer coverage. Cluster 1 is dark blue for rural share and SGP per person and red for municipal performance. Cluster 3 is dark blue for altitude. Cluster 2 is red for altitude, rural share and coverage.
Figure 5: Cluster centroids in z units: standard deviations above (red) or below (blue) the year’s average. IRCA: utility-supplied (prestadores). Altitude: derived SRTM point.

🏷️ Name Them by a Rule

🏷️ name = 🏙️ setting + 🚰 coverage + 🎚️ median IRCA band

Kind Zone-years Municipalities Median IRCA
1 Cities and large towns, no-risk water 2,271 284 1.6
2 Highland towns, no-risk water 3,750 558 4.0
3 Lowland towns, low-risk water 2,451 479 9.2
4 Rural, low-coverage, high-risk water 2,763 729 35.2

“low-coverage” = 0.5 SD or more below the year’s average, not “unserved” · risk word = band of the cluster’s median utility-supplied (prestadores) IRCA, not every zone-year’s; a median near a band edge can flip the word

🎯 Outcomes by Cluster

Three small line charts by year, one line per cluster. Cluster 1, cities and large towns, has the lowest infant mortality, around 9 per 1,000, and the highest Saber 11 scores. Cluster 2, highland towns, has the highest infant mortality. Cluster 3, lowland towns, has the lowest Saber 11 scores. Dropout medians stay between about 1.7 and 4.7 percent and dip in 2020 for every cluster.
Figure 6: Median by cluster and year. Infant mortality: 2017–2019 and 2021–2023 only (its method changes in 2017; 2020 looks like another source). Saber 11: published years.

⚠️ Descriptive only: these differences are not causal effects of water safety. Infant mortality and dropout are municipal values.

🌡️ Outcomes in SD Units

Heatmap with one row per named cluster and three outcome columns: infant mortality, Saber 11 and school dropout. Each cell prints the cluster's mean outcome in z units and, in brackets, the mean itself, coloured on the features heatmap's blue-to-red scale.
Figure 7: Cluster mean outcome in z units (SD above red, below blue the year’s average); the mean itself in brackets. Colour scale as in the features heatmap.

⚠️ Descriptive only: not causal effects of water safety. Infant mortality: 2017–2019 and 2021–2023; Saber 11: published years.

🎛️ Every Variable, Cluster by Cluster

Box plots of IRCA, utility-supplied (0–100), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 1.6; cluster 2 (Highland towns, no-risk water) 4.0; cluster 3 (Lowland towns, low-risk water) 9.2; cluster 4 (Rural, low-coverage, high-risk water) 35.2.Box plots of Piped water coverage (%), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 92.7; cluster 2 (Highland towns, no-risk water) 100.0; cluster 3 (Lowland towns, low-risk water) 99.5; cluster 4 (Rural, low-coverage, high-risk water) 23.6.Box plots of Sewer coverage (%), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 79.0; cluster 2 (Highland towns, no-risk water) 98.2; cluster 3 (Lowland towns, low-risk water) 89.1; cluster 4 (Rural, low-coverage, high-risk water) 0.9.Box plots of Multidimensional poverty, 2018 (%), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 18.0; cluster 2 (Highland towns, no-risk water) 21.4; cluster 3 (Lowland towns, low-risk water) 40.4; cluster 4 (Rural, low-coverage, high-risk water) 48.7.Box plots of Rural share of population, one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 0.2; cluster 2 (Highland towns, no-risk water) 0.7; cluster 3 (Lowland towns, low-risk water) 0.5; cluster 4 (Rural, low-coverage, high-risk water) 0.6.Box plots of Altitude of head town, derived SRTM point (m), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 1279.0; cluster 2 (Highland towns, no-risk water) 1784.0; cluster 3 (Lowland towns, low-risk water) 129.0; cluster 4 (Rural, low-coverage, high-risk water) 1400.0.Box plots of Rainfall (mm/year), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 1668.0; cluster 2 (Highland towns, no-risk water) 1736.5; cluster 3 (Lowland towns, low-risk water) 2112.0; cluster 4 (Rural, low-coverage, high-risk water) 1851.0.Box plots of Municipal performance, MDM, one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 64.2; cluster 2 (Highland towns, no-risk water) 51.9; cluster 3 (Lowland towns, low-risk water) 46.5; cluster 4 (Rural, low-coverage, high-risk water) 51.6.Box plots of Water transfers per person (log SGP), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 10.3; cluster 2 (Highland towns, no-risk water) 11.4; cluster 3 (Lowland towns, low-risk water) 11.2; cluster 4 (Rural, low-coverage, high-risk water) 11.2.Box plots of Infant mortality (per 1,000 live births), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 9.2; cluster 2 (Highland towns, no-risk water) 15.4; cluster 3 (Lowland towns, low-risk water) 12.9; cluster 4 (Rural, low-coverage, high-risk water) 13.2.Box plots of Saber 11 mean score (0–500), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 255.1; cluster 2 (Highland towns, no-risk water) 249.3; cluster 3 (Lowland towns, low-risk water) 229.1; cluster 4 (Rural, low-coverage, high-risk water) 234.0.Box plots of School dropout, public schools (%), one per cluster. Medians: cluster 1 (Cities and large towns, no-risk water) 3.5; cluster 2 (Highland towns, no-risk water) 2.6; cluster 3 (Lowland towns, low-risk water) 3.6; cluster 4 (Rural, low-coverage, high-risk water) 2.9.

▭ middle half · ━ median · ┅ mean · whiskers 5th–95th percentile · all zone-years 2016–2023; outcomes in their years, municipal ones once per municipality

📤 Use the Outcome Rules and Names

Copy ../quarto-workshop/checkpoints/08-water-interpret/scripts/analysis.py over scripts/analysis.py (keep mine as scripts/analysis-before-08.py), run it with `uv run python scripts/analysis.py`, and show me the cluster names.

copies · runs · writes outputs/outcomes_by_year.csv and outputs/cluster_profiles.csv

🧗 building it yourself? the next two ⭐ slides instead

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/08-water-interpret/README.md

the assistant shows a name for every cluster.

🏷️ Outcome Rules · ⭐ challenge

In scripts/analysis.py, add outcome rules: infant_mortality only in 2017–2019 and 2021–2023; infant_mortality and dropout_rate are municipal, so count each municipality once per `cluster` and `year`; saber11_mean stays per zone-year. Save outputs/outcomes_by_year.csv (median, n). Run it.

adds the outcome rules and outcomes_by_year() · uv run python scripts/analysis.py

Exercise · ⏱️ 3 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/08-water-interpret/README.md

outputs/outcomes_by_year.csv has no infant-mortality rows for 2016 or 2020.

🏷️ Profiles and Names · ⭐ challenge

Write outputs/cluster_profiles.csv: rows, municipalities, mean raw and z features, outcome medians and n under the same rules. Name each cluster by a rule: setting, relative coverage ("low-coverage" if mean z_water_coverage ≤ −0.5), median IRCA band. Show me the names.

adds profiles() and a naming rule · uv run python scripts/analysis.py

Exercise · ⏱️ 3 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/08-water-interpret/README.md

every cluster has a name you can defend from its profile.

🏷️ Heatmap and Outcomes

Add to clusters.qmd a heatmap of the cluster mean z-scores (features × clusters, blue to red) and one line chart per outcome from outputs/outcomes_by_year.csv, with gaps, not joined lines, in missing years. Use the cluster names and colours. Render it.

edits clusters.qmd · uv run quarto render clusters.qmd

reload clusters.html in your browser

💬 while it renders, with a neighbour: describe one cluster using two features.

Exercise · ⏱️ 4 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/08-water-interpret/README.md

clusters.html shows the heatmap and outcomes by cluster and year.

🏔️ Dropout Terrain by Cluster · ⭐ optional

Saber 11 · 201→284infant mortality · 3→36dropout · 0.8→8.2 %

📊 Outcomes the clustering never used, by year. Pick a cluster, or fix a year.

📅 2017 · cluster dropout means 2.83–3.84 %

x Saber 11 · z infant mortality · height school dropout ×2.4 · 🖱️ drag to turn

🫧 Clusters as Hulls · ⭐ optional

Saber 11 · 201→284infant mortality · 3→36dropout · 0.8→8.2 %

🫧 Each point is a municipality; each hull wraps its cluster’s nearest 85 %.

📅 2017 · cluster dropout means 2.83–3.84 %

Hull = convex hull of a cluster’s 85 % of municipalities nearest its mean · 🖱️ drag to turn

🥞 Stacked Years · ⭐ optional

Saber 11 · 201→284infant mortality · 3→36year ↑

🥞 One plane per year. Bright lines: a municipality that changed cluster.

📅 2017 · cluster dropout means 2.83–3.84 %

120 sampled municipalities joined across planes · 🖱️ drag to turn

🪐 Cluster Orbits · ⭐ optional

Saber 11 · 201→284infant mortality · 3→36dropout · 0.8→8.2 %

🪐 Ring radius = how far the cluster mean is from the overall mean.

📅 2017 · cluster dropout means 2.83–3.84 %

Sun = overall mean on Saber 11 and infant mortality · height = mean dropout ×2.4 · ring radius ×3 · 🖱️ drag to turn

🧱 Voronoi Dropout Columns · ⭐ optional

Saber 11 · 201→284infant mortality · 3→36dropout · 0.8→8.2 %

🧱 Each cell = the places nearest one cluster mean; height = its mean dropout.

📅 2017 · cluster dropout means 2.83–3.84 %

Voronoi cells of the cluster means · 36 × 36 squares · height ×2.4 · 🖱️ drag to turn

⚠️ Correlation ≠ Causation

⏱️ 4:55

📉 Worse Water, More Infant Deaths?

Four small panels, one per cluster, each titled with the cluster's number and name. Each shows median infant mortality across the five IRCA risk bands, labelled N, L, M, H and U for no risk, low, medium, high and unfit. The lines are nearly flat in every cluster, and the Spearman correlations printed on the panels are all close to zero.
Figure 8: Median infant mortality by utility-supplied (prestadores) IRCA band, per cluster. Infant mortality changes method in 2017, and 2020 looks like another source: 2017–2019 and 2021–2023 are pooled. Bands under 20 municipality-years are not drawn.

⚠️ descriptive only: not a causal effect of water safety

🤔 What Else Could Explain It?

💸

poverty

🌾

rurality

🏥

health-service access

📝

reporting quality

🔍

ecological level: municipal outcome, zone IRCA

📤 Use the Within-Cluster Check

Copy ../quarto-workshop/checkpoints/09-water-causation/scripts/analysis.py over scripts/analysis.py (keep mine as scripts/analysis-before-09.py), run it with `uv run python scripts/analysis.py`, and print Spearman rho and n per cluster from outputs/irca_infant.csv.

copies · runs · writes outputs/irca_infant.csv and outputs/irca_infant_bins.csv

🧗 building it yourself? the next ⭐ slide instead

Exercise · ⏱️ 2 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/09-water-causation/README.md

ρ and n are printed for every cluster.

🧮 Within-Cluster Check · ⭐ challenge

In scripts/analysis.py, for 2017–2019 and 2021–2023, make one row per municipality-year within each cluster: its infant_mortality and the mean IRCA of its zones in that cluster. Save and print Spearman rho and n per cluster, and median infant_mortality by IRCA band. Run it.

adds irca_vs_infant() · uv run python scripts/analysis.py

Exercise · ⏱️ 3 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/09-water-causation/README.md

ρ and n are saved and printed for every cluster.

⚠️ Show It + Discuss

Add a section to clusters.qmd that reads those outputs: median infant_mortality by IRCA band per cluster, with rho and n, and one sentence stating this is descriptive and does not establish a causal effect of water safety. Render it.

edits clusters.qmd · uv run quarto render clusters.qmd

💬 while it runs, with a neighbour: pick one confounder: could it hide a real effect, or create a fake one?

Exercise · ⏱️ 3 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/09-water-causation/README.md

clusters.html shows ρ and n per cluster, and you can say how one confounder could bias it.

📤 Publish Many

⏱️ 5:10

📄 5:10 · Report → docx + PDF

Create report.qmd (formats html, docx, typst) from outputs/: summary, question, data (IRCA = utility-supplied prestadores IRCA), method, heatmap, outcomes, correlation ≠ causation (a chart of median infant_mortality by IRCA band per cluster), limits, sources. Matplotlib figures with captions and alt text; hide code. Render all formats; report errors.

writes report.qmd · uv run quarto render report.qmd

open report.docx (Word or LibreOffice) and report.pdf

Exercise · ⏱️ 8 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/10-water-report/README.md

report.docx and report.pdf open with the heatmap, the outcomes and the within-cluster figures.

📊 5:20 · Slides → revealjs + pptx

Create slides.qmd (revealjs and pptx) for the municipal council, 6 slides with the title: the question, a table of the named clusters, the heatmap, outcomes by cluster, what this does and does not show. Read outputs/, use matplotlib, hide code. Render both; count the slides.

writes slides.qmd · uv run quarto render slides.qmd

open slides.html in your browser and slides.pptx in PowerPoint or LibreOffice

Exercise · ⏱️ 4 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/11-water-slides/README.md

slides.html and slides.pptx both have 6 slides.

📖 Report → ePub · ⭐ optional

In report.qmd, add an epub format (toc: true), set lang: en, and add ePub to the html `format-links`. Render report.qmd and check that report.epub contains the three figures and the sources.
lang: en        # the e-book's language
format:
  html:
    format-links:
      # … Word, PDF
      - text: ePub
        format: epub
  epub:
    toc: true

edits report.qmd · uv run quarto render report.qmd

open report.epub in an e-book reader (Calibre, or Books on a Mac)

Exercise · ⏱️ 4 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/12-water-epub/README.md

report.epub opens with the three figures and the sources.

✍️🌐 5:26 · Blog + Website

# _quarto.yml
project:
  type: website
  execute-dir: project
  pre-render: scripts/analysis.py
execute:
  freeze: false
# index.qmd
listing:
  contents: posts

🔁 pre-render rebuilds outputs/

🧪 freeze: false re-runs every page → always the new numbers

⭐ later, for slow sites: freeze: auto reuses old results
until you run rm -rf _freeze

✍️🌐 Website + Post

Make this folder a Quarto website: _quarto.yml (execute-dir: project, `pre-render` scripts/analysis.py, freeze: false, render list, navbar), index.qmd listing posts/, and one post titled with the real k, e.g. "Four kinds of municipal zone". Render it; check the links.

writes 3 files · uv run quarto render → _site/

uv run quarto preview

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/13-water-website/README.md

the site lists one post whose title states the real k.

🌍 5:34 · Publish It

Initialise a git repository here with a .gitignore for _site/, .quarto/, .venv/, outputs/ and *_files/. Replace README.md in this folder with a short one linking to https://<your-github-username>.github.io/water-quality/ and listing the files in the site's Downloads menu. Commit everything not ignored, then create a public GitHub repository named `water-quality` with gh and push to it. Set the repository's website to the same URL with `gh repo edit --homepage`. Show me the README and the website URL you set.

approve gh · when it shows the website URL, second tab: uv run quarto publish gh-pages, Enter at “Publish site … (Y/n)” · open the live URL

💬 while it publishes: what is the difference between your folder and the live site?

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/13-water-website/README.md

https://<your-github-username>.github.io/water-quality/ opens in a browser.

🔁 5:42 · Change an Input

🧪 No prompt is given: write your own.

  1. 🔮 Predict: in scripts/analysis.py change MAX_MISSING = 2 to 1. Which outputs will change?
  2. 🔁 Rebuild the site
  3. 🔎 Explain: which outputs updated, which did not, and why?
  4. ↩︎️ undo the change and rebuild, so the outputs match the real settings

Exercise · ⏱️ 6 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/checkpoints/13-water-website/README.md

you can name the outputs that changed, the ones that did not, and why.

📈 Dashboard · ⭐ optional

Map of Colombia's municipalities coloured by the 2023 IRCA band of their head town, with a legend from hatched no data through no risk, low, medium and high to unfit. Most of the Andean interior is in the darker no-risk and low-risk colours, scattered municipalities are bright high-risk, and much of the east and the Amazon is hatched for no data.
Figure 9: Urban zones, 2023: utility-supplied (prestadores) IRCA band.

🗺️ choropleth: municipios.geojson joined on divipola

🎚️ year slider: 2016 → 2023

🔎 search a department → its municipalities

🗂️ two pages: map · table

📈 Build It · ⭐ optional

Create dashboard.qmd (format: dashboard): a Plotly choropleth of the IRCA band of the "urban" rows of outputs/clusters.parquet on data/municipios.geojson (featureidkey="properties.divipola"), a year slider over their years (frames update only z), an itables table of outputs/clusters.parquet (maxBytes=0); end each chart cell with `fig`. Make it polished: IRCA bands in order from no risk to unfit, light to dark, with a legend, titled cards and readable labels. Add it to the navbar; render, check it looks good at laptop size and report errors.

writes dashboard.qmd · edits _quarto.yml · uv run quarto render · ask it to run uv run quarto publish gh-pages --no-prompt

reload the preview (or uv run quarto preview) → 📈 Dashboard

Exercise · ⭐ self-paced

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/water/README.md

the map changes when you move the year slider.

📈 Three More Dashboard Pages · ⭐ optional

Add Kinds, Outcomes and Method pages to dashboard.qmd from outputs/ only: a heatmap of cluster_profiles.csv's `z_` columns; outcomes_by_year.csv medians by `year` and `cluster`, and irca_infant.csv's ρ; k_selection.csv's inertia and silhouette by k. Use each cluster's `color` and axis units; say it is descriptive, not causal. Render; check each page at laptop size.

🧬 Kinds · 🎯 Outcomes · 📐 Method (why k = 4), from outputs/

reload 📈 Dashboard · open each page

Exercise · ⏱️ 8 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/water/README.md

the dashboard has Map, Kinds, Outcomes, Method and Municipalities pages.

📑 A White Paper · ⭐ optional

Create whitepaper.qmd from outputs/: A4 Typst, "New Computer Modern"; an abstract (no inline code), numbered Introduction, Methods with equations, Results and Conclusions, then References via `#set bibliography(title: [References])`. Cite data/README.md's sources and the k-means, silhouette and ARI papers in references.bib; open each DOI. Add it to `render` and the "Downloads" menu; render; check equations and citations. Figures must be vector: set `fig-format: svg` and check that the files in `figure-typst/` end in .svg.
$$
W = \sum_c \sum_i \|z_i - m_c\|^2
$$ {#eq-kmeans}

k-means [@lloyd1982] minimises @eq-kmeans.

uv run quarto render whitepaper.qmd

open whitepaper.pdf

Exercise · ⏱️ 10 min

😰 Stuck?

Catch me up: follow ../quarto-workshop/examples/water/README.md

whitepaper.pdf shows numbered equations and references.

🚀 Extra Challenges

🔀

Sankey: cluster moves between years

⭐

positive deviants: good outcomes despite risky water

🧪

GMM · HDBSCAN: soft or density clusters

🦠

SIVIGILA: diarrhoeal disease incidence, once available

📚 Sources

🏁 Done

⏱️ 5:50

🙌 What You Built

🇬🇧

London guide: html · docx · pdf · dashboard · email · flyer · live site (⭐ ad · pptx · revealjs · epub)

💧

Water analysis: report (docx · pdf · ⭐ epub) · slides (pptx · revealjs) · blog · live site (⭐ dashboard)

🔁

One source, many outputs, rebuilt with one command: you can say which ones a changed input updates

🔗 Show your live sites: paste both URLs in 💬 Discord

📚 Quarto guide · Dashboards · Typst · GitHub Pages

➡️ Next: AI workflows: promotion experiments and sales outcomes