cassionData Analysis

Back to the lessonLesson 5 of 8One template, many outputs

One template, twelve districts, no copy anywhere

The same deck as the downloads, rendered as a page. Start the slideshow to present it full screen — arrow keys or a click advance one slide, Escape leaves.

Slides · PDFSlides · PowerPoint

  1. Slide 1 / 28

    What this lesson covers

    • The report that becomes twelve reports
    • Quarto, which this platform already uses
    • Rendering all twelve
    • Writing a template that survives its own parameters
    • What belongs in the template and what belongs in the pipeline
    • The parameter you should always have
    • Where this platform does the same thing
    • Report it whole
    • What comes next
    Speaker notes
    Twelve district reports produced by copy-paste are twelve documents that will disagree by March. One parameterised template rendered twelve times is one document that cannot, and the mechanism is a single line in the frontmatter.
  2. Slide 2 / 28

    The report that becomes twelve reports

    • By March those twelve files disagree — Someone fixes the commune-name normalisation in one of them
    • A parameterised template is one document — Fixing it fixes twelve reports, and there is nowhere for a difference to hide
    Speaker notes
    A cluster wants a report per district. The obvious approach is to write one, copy it eleven times and change the filter at the top of each. By March those twelve files disagree. Someone fixes the commune-name normalisation in one of them. Someone adds a caveat to three. The indicator definition changes and eight get updated. Nothing in any build can detect that they have drifted, because they are twelve unrelated documents. A parameterised template is one document. Fixing it fixes twelve reports, and there is nowhere for a difference to hide.
  3. Slide 3 / 28

    Quarto, which this platform already uses — Example

    ---
    title: "Nutrition surveillance: `r params$district`"
    params:
      district: "Artibonite"
      as_of: "2025-03-31"
    format: pdf
    ---
  4. Slide 4 / 28

    Quarto, which this platform already uses — Shell

    quarto render report.qmd -P district:Nord-Ouest -P as_of:2025-03-31
  5. Slide 5 / 28

    Quarto, which this platform already uses

    • The document declares its parameters and the renderer supplies them — The body refers to params$district — in R — or…
    Speaker notes
    The document declares its parameters and the renderer supplies them. The body refers to params$district — in R — or to district from the injected parameters cell in Python, and nothing else in the file mentions a specific district.
  6. Slide 6 / 28

    Quarto, which this platform already uses — In Python

    # | tags: [parameters]
    district = "Artibonite"
    as_of = "2025-03-31"
  7. Slide 7 / 28

    Quarto, which this platform already uses — In R

    params$district
  8. Slide 8 / 28

    Quarto, which this platform already uses

    • The default value in the frontmatter is what makes the template previewable — You open it, it renders for Artibonite,…
    Speaker notes
    The default value in the frontmatter is what makes the template previewable. You open it, it renders for Artibonite, and you are editing something you can see.
  9. Slide 9 / 28

    Rendering all twelve — In Python

    import subprocess, pathlib
    
    DISTRICTS = ["Artibonite", "Centre", "Grande-Anse", "Nippes", "Nord",
                 "Nord-Est", "Nord-Ouest", "Ouest", "Sud", "Sud-Est"]
    
    for district in sorted(DISTRICTS):
        slug = district.lower().replace(" ", "-")
        subprocess.run([
            "quarto", "render", "report.qmd",
            "-P", f"district:{district}",
            "-P", f"as_of:{AS_OF}",
            "--output", f"outputs/reports/{slug}.pdf",
        ], check=True)
  10. Slide 10 / 28

    Rendering all twelve — In R

    for (d in sort(districts)) {
      quarto::quarto_render(
        "report.qmd",
        execute_params = list(district = d, as_of = as_of),
        output_file = paste0(tolower(gsub(" ", "-", d)), ".pdf")
      )
    }
  11. Slide 11 / 28

    Rendering all twelve

    • check=True is the important argument — Without it a failed render is a missing file that the loop steps over…
    • Sort the district list — for the reason the last lesson gave: the loop's output order should not depend on how the list…
    Speaker notes
    check=True is the important argument. Without it a failed render is a missing file that the loop steps over silently, and you discover it when someone asks for the Nippes report. Sort the district list, for the reason the last lesson gave: the loop's output order should not depend on how the list was assembled.
  12. Slide 12 / 28

    Writing a template that survives its own parameters

    • No hard-coded numbers in the prose — Every figure in the text comes from the data
    Speaker notes
    Four things a copied report gets away with and a template does not. No hard-coded numbers in the prose. Every figure in the text comes from the data.
  13. Slide 13 / 28

    Writing a template that survives its own parameters — Example

    Coverage in `r params$district` is
    `r scales::percent(coverage, accuracy = 0.1)`, against a national figure of
    `r scales::percent(national, accuracy = 0.1)`.
  14. Slide 14 / 28

    Writing a template that survives its own parameters

    • Handle the district with no data — One of the twelve will have an empty table, and a template that assumes rows…
    Speaker notes
    Handle the district with no data. One of the twelve will have an empty table, and a template that assumes rows produces a stack trace or, worse, a page of NaN.
  15. Slide 15 / 28

    Writing a template that survives its own parameters — In Python

    if summary.empty:
        print(f"No screening was conducted in {district} during this period.")
    else:
        ...
  16. Slide 16 / 28

    Writing a template that survives its own parameters — In R

    if (nrow(summary) == 0) cat("No screening was conducted in this period.")
  17. Slide 17 / 28

    Writing a template that survives its own parameters

    • Handle the singular — "1 communes" in eleven reports is the sign of a template nobody proofread
    • Handle the small denominator — A district with 40 screenings gets an interval three times wider than one with 400, and…
    Speaker notes
    Handle the singular. "1 communes" in eleven reports is the sign of a template nobody proofread. Handle the small denominator. A district with 40 screenings gets an interval three times wider than one with 400, and the template has to say so rather than printing both to one decimal place as though they were comparable.
  18. Slide 18 / 28

    What belongs in the template and what belongs in the pipeline

    In the templateIn the pipeline
    Layout, prose, the shape of the argumentReading raw data
    A summary table computed from prepared dataCleaning and normalisation
    Figures, drawn from prepared dataAny transformation more than a line or two
    Caveats, thresholds and definitionsAnything two reports would share
  19. Slide 19 / 28

    What belongs in the template and what belongs in the pipeline

    • The template should read a prepared file, not the raw export — If twelve renders each redo the cleaning, the cleaning…
    Speaker notes
    The template should read a prepared file, not the raw export. If twelve renders each redo the cleaning, the cleaning runs twelve times, takes twelve times as long, and — worse — can be edited in the template for one district only.
  20. Slide 20 / 28

    What belongs in the template and what belongs in the pipeline — In Python

    # run.py
    clean()                       # once
    build_figures()               # once
    for district in DISTRICTS:    # twelve times, from prepared data
        render(district)
  21. Slide 21 / 28

    What belongs in the template and what belongs in the pipeline — In R

    # run.R, same shape
  22. Slide 22 / 28

    The parameter you should always have — Example

    params:
      district: "Artibonite"
      as_of: "2025-03-31"
      data_version: "v1"
  23. Slide 23 / 28

    The parameter you should always have

    • A data version parameter makes the report say which file it read — which is the provenance question a reader asks six…
    • And an as_of parameter rather than the clock — for the reason lesson 4 gave: a report that says "generated 14 March"…
    Speaker notes
    A data version parameter makes the report say which file it read, which is the provenance question a reader asks six months later. Print it in the colophon. And an as_of parameter rather than the clock, for the reason lesson 4 gave: a report that says "generated 14 March" changes every time it is rebuilt, and a report that says "data as of 31 March" does not.
  24. Slide 24 / 28

    Where this platform does the same thing

    • slide-plan.mjs plans the deck once from the lesson body — and the Beamer PDF, the PowerPoint file and the in-browser…
    • The reasoning is the same as the twelve districts', at a different scale — A slides: array in the frontmatter would…
    • The consequence for authors is real and worth naming — Because the deck is derived, lessons are written with lists,…
    Speaker notes
    Every lesson on this site produces a slide deck in two languages and three formats — a Beamer PDF, a PowerPoint file and the LaTeX source, six files per lesson — and none of them was authored. slide-plan.mjs plans the deck once from the lesson body, and the Beamer PDF, the PowerPoint file and the in-browser slide reader are three renderings of that one plan. The reasoning is the same as the twelve districts', at a different scale. A slides: array in the frontmatter would produce better slides in principle and worse ones in practice: it means every new lesson needs a second authoring pass in two languages, and that is the pass that gets skipped. The consequence for authors is real and worth naming. Because the deck is derived, lessons are written with lists, bold lead-ins, tables and callouts — because that is what a deck is made of. A parameterised system shapes its inputs, and pretending otherwise produces templates that fight their own content.
  25. Slide 25 / 28

    Report it whole — Example

    District reports
    
      Twelve district reports are rendered from one template, report.qmd, by
      run.py. No district-specific text exists outside the data.
    
      Parameters: district, as_of (2025-03-31), data_version (v1). Each report
      prints all three in its colophon.
    
      Cleaning and figure generation run once, before rendering; the template
      reads prepared data and performs no transformation.
    
      Districts with no screening in the period render a stated "no data"
      section rather than an empty table.
    
      Rendering is verified by `--check`: a failed render stops the run rather
      than leaving a missing file.
  26. Slide 26 / 28

    Report it whole

    • The last line is what turns twelve rendered files into twelve reports you can send — A loop that swallows failures…
    Speaker notes
    The last line is what turns twelve rendered files into twelve reports you can send. A loop that swallows failures produces eleven reports and a question nobody asks until the wrong person notices.
  27. Slide 27 / 28

    What comes next

    • A pipeline that renders twelve reports from broken upstream data renders twelve broken reports.
    Speaker notes
    A pipeline that renders twelve reports from broken upstream data renders twelve broken reports. The next lesson makes it stop instead — loudly, at the point of failure, rather than producing a plausible number nobody can check.
  28. Slide 28 / 28

    Where this goes next

    Read the full lesson, with runnable code Back to the lesson