Skip to content

Commit 41c3263

Browse files
committed
Scipy talk post
1 parent f3cd2b8 commit 41c3263

File tree

6 files changed

+271
-0
lines changed

6 files changed

+271
-0
lines changed
125 KB
Loading
739 KB
Loading
181 KB
Loading
511 KB
Loading
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
---
2+
author:
3+
- name: "Charlotte Wickham"
4+
title: "From One Notebook to Many Reports: Parameterized reports with the `jupyter` engine"
5+
description: |
6+
Learn how to transform a single Jupyter notebook into a parameterized report generator that automatically creates customized outputs for different scenarios.
7+
date: "2025-07-22"
8+
categories:
9+
- Authoring
10+
- Teaching
11+
- Jupyter
12+
image: thumbnail.png
13+
image-alt: |
14+
A slide with a screenshot of a Jupyter notebook with a graph and text, then an arrow pointing to a stack of PDF files each with a graph and text.
15+
lightbox: true
16+
---
17+
18+
::: callout-tip
19+
20+
## Based on a talk at SciPy 2025
21+
22+
This post is based on the talk "From One Notebook to Many Reports: Automating with Quarto" delivered at [SciPy 2025](https://www.scipy2025.scipy.org), July 2025 by Charlotte Wickham.
23+
You can find the slides at [cwickham.github.io/one-notebook-many-reports](https://cwickham.github.io/one-notebook-many-reports/) and example code at [github.com/cwickham/one-notebook-many-reports](https://github.com/cwickham/one-notebook-many-reports).
24+
25+
:::
26+
27+
## The Problem: Repetitive Reporting
28+
29+
Would you rather read a generic "Climate summary" or a "Climate summary for _exactly where you live_"? Reports that are personalized to a specific situation increase understanding, engagement, and connection. But producing many customized reports manually is tedious and error-prone.
30+
31+
Quarto solves this with parameterized reports—-you create a single document template, then render it multiple times with different parameter values to generate customized outputs automatically.
32+
33+
A great example is the customized soil health reports from Washington Soil Health Initiative's [State of the Soils Assessment](https://washingtonsoilhealthinitiative.com/state-of-the-soils/), presented at posit::conf(2023) by [Jadey Ryan](https://jadeyryan.com) (watch on [YouTube](https://youtu.be/lbE5uOqfT70?si=C-d5U5Q2VXo1wlDs)). Jadey demonstrated this approach using R and plain text Quarto files (`.qmd`).
34+
35+
This post shows you how to apply the same principles using Python: I'll walk through converting a Jupyter notebook (`.ipynb`) into a parameterized report, then automating the generation of multiple customized outputs. Then I'll show you how to make those reports look professionally polished.
36+
37+
## The Solution: Parameterized Reports
38+
39+
### Start with a notebook
40+
41+
As an example, let's start with a Jupyter notebook analyzing climate data for Corvallis, Oregon.
42+
43+
![[`corvallis.ipynb`](https://github.com/cwickham/one-notebook-many-reports/blob/main/01-one-notebook/corvallis.ipynb)](corvallis-ipynb.png){#corvallis-ipynb .column-margin fig-alt="Screenshot of a Jupyter notebook with code cells and output, including a plot and text summary."}
44+
45+
You can see the full notebook, [`corvallis.ipynb`, on GitHub](https://github.com/cwickham/one-notebook-many-reports/blob/main/01-one-notebook/corvallis.ipynb), but here are the key pieces:
46+
47+
- The code cells import some data for all of Oregon, and filter it to just rows relevant for Corvallis, then produce a summary sentence and a plot.
48+
49+
- The document options specify `echo: false` so no code appears in the final output, and `format: typst` so the output is a PDF produced via [Typst](https://typst.app), a modern alternative to LaTeX.
50+
51+
This single notebook can be rendered:
52+
53+
```{.bash filename="Terminal"}
54+
quarto render corvallis.ipynb
55+
```
56+
57+
The result is a PDF file, `corvallis.pdf`, a simple report with the title "Corvallis" and a single sentence summary of the climate data, along with a plot highlighting the mean temperature for this year against the last 30 years.
58+
59+
![`corvallis.pdf`](corvallis-pdf.png){.column-margin fig-alt="Screenshot of a PDF file with the title 'Corvallis' that contains a single sentence summary and a plot."}
60+
61+
Now, imagine we want to create this report for the 50 largest cities in Oregon.
62+
Here's the steps we'll take:
63+
64+
1. Turn hardcoded values into variables
65+
2. Make those variables parameters by tagging the cell
66+
3. Render the notebook with different parameter values
67+
4. Automate rendering with many parameter values
68+
69+
### 1. Turn hardcoded values into variables
70+
71+
We want a report for each city.
72+
We'll start by creating a variable, `city`, which we'll designate a parameter in our next step.
73+
In a new code cell at the top of our notebook, we define the variable:
74+
75+
````{.python filename="code"}
76+
city = "Corvallis"
77+
````
78+
79+
Then anywhere we previously hardcoded `"Corvallis"` in the notebook, we replace it with this variable.
80+
81+
The first occurrence is in the title of the document.
82+
Originally, we had a markdown cell defining a level 1 heading:
83+
84+
```{.markdown filename="markdown"}
85+
# Corvallis
86+
```
87+
88+
We replace it with a code cell that uses an f-string to produce markdown for a level 1 heading based on the `city` variable:
89+
90+
```{.markdown filename="code"}
91+
Markdown(f"# {city}")
92+
```
93+
94+
In the filtering step the replacement is straightforward, we just change the string to the variable:
95+
96+
::: {layout-ncol="2"}
97+
98+
:::{}
99+
Before:
100+
101+
```{.python filename="code"}
102+
tmean = tmean_oregon.filter(
103+
pl.col("city") == "Corvallis",
104+
)
105+
```
106+
:::
107+
108+
:::{}
109+
After:
110+
111+
```{.python filename="code"}
112+
tmean = tmean_oregon.filter(
113+
pl.col("city") == city,
114+
)
115+
```
116+
:::
117+
:::
118+
119+
Finally, the plot code (using [plotnine](https://plotnine.org)), sets the title of the plot to include the city name:
120+
121+
```{.python filename="code"}
122+
...
123+
+ labs(title = "Corvallis, OR", ...)
124+
...
125+
```
126+
127+
We can also use an f-string here to include the `city` variable:
128+
129+
```{.python filename="code"}
130+
...
131+
+ labs(title = f"{city}, OR", ...)
132+
...
133+
```
134+
135+
Now, we should be able to test our changes by explicitly setting the `city` variable to something other than "Corvallis" and re-running the cells.
136+
Since our report is no longer specific to Corvallis, we can rename it `climate.ipynb`.
137+
138+
### 2. Make variables into parameters
139+
140+
Now we have a variable that represents the parameter, we need to let Quarto know it's a parameter.
141+
Quarto's parameterized reports are implemented using [Papermill](https://papermill.readthedocs.io/en/latest/), and inherits Papermill's approach: tag the cell containing the parameter with `parameters`.
142+
143+
In Jupyter, you can add this tag through the cell toolbar:
144+
145+
![](corvallis-add-tag.png){fig-alt="Screenshot of a Jupyter notebook cell with a tag 'parameters' added to it."}
146+
147+
::: {.callout-tip}
148+
149+
## `jupyter` vs `knitr`
150+
151+
Setting parameters differs based on the engine you are using in Quarto.
152+
With a Jupyter notebook (`.ipynb`), or a plain text Quarto document (`.qmd`) with only Python code cells, Quarto will default to the `jupyter` engine.
153+
As described above, the `jupyter` engine uses cell tags to identify parameters.
154+
155+
If you are working in a `.ipynb` file, your IDE will likely provide a way to add these tags through the cell toolbar.
156+
If you are working in a `.qmd` file, you can add the tags as code cell options:
157+
158+
````markdown
159+
```{{python}}
160+
#| tags: [parameters]
161+
city = "Corvallis"
162+
```
163+
````
164+
165+
If you are working in a Quarto document (`.qmd`) with R code cells, Quarto will default to the `knitr` engine.
166+
With the `knitr` engine, you set parameters in the document header under `params`:
167+
168+
```yaml
169+
---
170+
params:
171+
city: "Corvallis"
172+
---
173+
```
174+
175+
:::
176+
177+
You can see the updated notebook (now a parameterized report), [`climate.ipynb`, on GitHub](https://github.com/cwickham/one-notebook-many-reports/blob/main/03-many-reports/climate.ipynb).
178+
179+
### 3. Render with different parameter values
180+
181+
If we render the notebook now, it will still produce the same report for Corvallis, because we haven't changed the parameter value:
182+
183+
```{.bash filename="Terminal"}
184+
quarto render climate.ipynb
185+
```
186+
187+
But we can now pass parameter values to Quarto when we render with the `-P` flag:
188+
189+
```{.bash filename="Terminal"}
190+
# Generate report for Portland
191+
quarto render climate.ipynb -P city:Portland --output-file portland.pdf
192+
193+
# Generate report for Eugene
194+
quarto render climate.ipynb -P city:Eugene --output-file eugene.pdf
195+
```
196+
197+
We've also added `--output-file` to ensure each report gets its own filename.
198+
199+
### 4. Automate rendering with many parameter values
200+
201+
To generate all 50 reports, we need to run `quarto render` 50 times, each time with a different city as the parameter value.
202+
You could automate this in many ways, but let use a Python script.
203+
For example, you might have a dataset of cities and their corresponding output filenames:
204+
205+
```python
206+
cities = pl.DataFrame({
207+
"city": ["Portland", "Cottage Grove", "St. Helens", "Eugene"],
208+
"output_file": ["portland.pdf", "cottage_grove.pdf", "st_helens.pdf", "eugene.pdf"]
209+
})
210+
```
211+
212+
I've generated a small example above, but in reality you would likely read `cities` in from a CSV file.
213+
Then you could iterate over the rows of this dataset, rendering the notebook for each city:
214+
215+
```python
216+
from quarto import render
217+
for row in cities.iter_rows(named=True):
218+
render(
219+
"climate.ipynb",
220+
execute_params={"city": row["city"]},
221+
output_file=row["output_file"],
222+
)
223+
```
224+
225+
Run this script once, and you'll get all 50 custom reports!
226+
227+
## Pretty Reports: Brand and Typst
228+
229+
The steps above to produce parameterized reports apply to any output format supported by Quarto.
230+
However, if you are targeting `typst` you can take advantage of additional features to create beautiful PDF reports.
231+
232+
### Brand.yml
233+
234+
Quarto supports [brand files](https://posit-dev.github.io/brand-yml/) that automatically apply your organization's colors, fonts, and logos across your reports:
235+
236+
```{.yaml filename="_brand.yml"}
237+
color:
238+
palette:
239+
forest-green: "#2d5a3d"
240+
charcoal-grey: "#555555"
241+
foreground: charcoal-grey
242+
primary: forest-green
243+
typography:
244+
fonts:
245+
- family: Open Sans
246+
source: google
247+
base:
248+
family: Open Sans
249+
logo:
250+
medium: logo.png
251+
```
252+
253+
Place this file in your project, and Quarto automatically applies your branding to all generated reports.
254+
You can see a full example of using `_brand.yml` with `climate.ipynb` at [cwickham/one-notebook-many-reports/04-branded-reports](https://github.com/cwickham/scipy-talk/tree/main/04-branded-reports).
255+
256+
257+
### Typst
258+
259+
Learning a little bit of Typst syntax can take your reports from basic to beautiful.
260+
You can include [raw Typst syntax](https://quarto.org/docs/output-formats/typst.html#raw-typst) in your notebooks, or wrap elements in Typst functions using the [typst-function Quarto extension](https://github.com/christopherkenny/typst-function).
261+
As an example, you could add a header with the city name and a map of the location:
262+
263+
![`corvallis.pdf`](corvallis-pretty-pdf.png){fig-alt="The `corvallis.ipynb` notebook rendered by Quarto to `pdf`. The document has dark green header with the city in white text and a map next to it with the location as an orange dot." }
264+
265+
You can see the source for this example at [cwickham/one-notebook-many-reports/05-pretty-reports](https://github.com/cwickham/one-notebook-many-reports/tree/main/05-pretty-reports).
266+
267+
## Wrapping Up
268+
269+
Parameterized reports turn one notebook into many customized outputs.
270+
You've seen the process of going from a notebook with a hardcoded value to a parameterized report that can be rendered with different values.
271+
You can then automate the rendering in any way you choose to generate dozens of reports at once.
373 KB
Loading

0 commit comments

Comments
 (0)