Modern and Efficient Scientific Communication with Quarto

From Script to Document
One source file → HTML, PDF, Word, Slides, ePub…

Yuri Gelsleichter

April 29, 2026

Setup installation: R & RStudio
See link/QR code for instructions
github.com/Gelsleichter/quarto_ws
QR Code

About Me

Yuri Gelsleichter

Who Has Done This? 🙋

  1. Run analysis in R / Python / Excel / SPSS
  2. Copy table → paste into Word
  3. Copy figure → paste into Word
  4. Adjust formatting, captions, numbering…
  5. Co-author says: “Let’s transform the data”
  6. Start over from step 1 😩

xkcd comic about file formats

The Common Workflow

flowchart TB
  A[🗂️ Raw Data] --> B[💻 Software <br> R / Python / Excel / SPSS]
  B --> C[📊 Output <br> Tables & Figures]
  C --> D[📋 Copy-Paste]
  D --> E[📝 Word Document]
  E --> F{Reviewer says: <br> 'Let's change X'}
  F --> B

The copy-paste cycle

This diagram was code too!

```{mermaid}
flowchart TB
  A[Raw Data] --> B[Software]
  B --> C[Output]
  C --> D[Copy-Paste]
  D --> E[Word Document]
  E --> F{Reviewer says: 'Lets change X'}
  F --> B
```

Code → Output. That’s the idea we’re exploring today.

After submit a paper:

“2000 years later…” 📬

Reviewer 2:

“Please normalize variables before modeling.”

“Please add a Tukey post-hoc comparison.”

“Please remove the outlier in Block III and redo all analyses.”

You need to redo every table, every figure, every number in the text…

And your project folder looks like this:

📁 project/
├── data_raw.xlsx
├── data_clean.xlsx
├── data_clean_v2.xlsx
├── data_FINAL.xlsx
├── data_FINAL_really.xlsx
└── data_FINAL_USE_THIS_reviewed_v3.xlsx  🤯

The Invisible Problem

For click button software (Excel, SPSS…):

  • Which buttons did you click? In which order?
  • Which menu options? Which checkboxes?
  • Can you reproduce this analysis in few months (weeks)?
  • Can a colleague reproduce it without you?

Reproducibility is not just about code

It’s about recording decisions and methods — in any software.

What If…

Everything lived in one file?

Change the data → everything updates?

Automatically :)

From Markup to Quarto

Markup language = text with formatting instructions

Markdown examples:

**bold text** → bold text
# Title       → a heading
[link](url)   → a clickable link

The evolution

Year Tool What changed
2004 Markdown Simple text formatting
2012 R Markdown Markdown + R code chunks
2022 Quarto Language-agnostic, multi-format, active development

. . .

You can still render .Rmd files with Quarto — nothing breaks.

Markdown Cheat Sheet

You type You get
# Header 1 Header level 1
## Header 2 Header level 2
**bold** bold
*italic* italic
~~strike~~ strikethrough
[text](url) clickable link
![](img.png) embedded image
`code` inline code
You type You get
- item bullet list
1. item numbered list
> text blockquote
--- horizontal rule
^super^ superscript
~sub~ subscript
[^1] footnote reference

This cover the Markdown rules to compose a Quarto document.

What is Quarto?

An open-source scientific and technical publishing system

  • Created by Posit (formerly RStudio)

  • The formula:

    Code (data analysis) + Text (interpretation) + Output = One file

  • Supports R, Python, Julia, Observable

  • Reference: quarto.org

Quarto logo

One File → Many Outputs

format:
  html: default      # web page
  pdf: default       # PDF document
  docx: default      # Word document
  revealjs: default  # slides (like this one!)
  epub: default      # e-book

Also: websites, books, dashboards, manuscripts

Same content, different formats — from a single .qmd file.

Where to Use Quarto

IDE / Editor Support
RStudio Full (built-in)
VS Code Full (extension)
Positron Full (built-in)
Jupyter Native
Terminal CLI

RStudio with Quarto document

Positron with Quarto document

VS Code with Quarto document

.qmd files are supported by the common code editor

Real-World Examples

Explore the Quarto Gallery:

This presentation is also a .qmd file!

Rendered with format: revealjs — the same system we’ll learn today.

Let’s See How It Works

Building a Quarto file, step by step

Part 3: Anatomy of a .qmd File

What We’ll Build

We’ll construct a complete reproducible document using the Oats dataset:

The data:

  • Split-plot Experiment on Varieties of Oats
  • 3 varieties: Golden Rain, Marvellous, Victory
  • 4 nitrogen levels: 0, 0.2, 0.4, 0.6 cwt/acre
  • 6 blocks, 72 observations

The analysis:

  • Summary tables, figures, inline results
  • A simple linear model

cwt: “hundredweight” (centweight), 1 cwt = 112 lbs ~50.8 kg
Yield: 1/4 lbs per sub-plot (1/80 acre)
Original units from Yates (1935)

Step 1: The YAML Header

Every .qmd file starts with a YAML header between --- marks:

---
title: "Oat Yield Analysis"
author: "Your Name"
date: today
format: html
---
  • title, author, date → document metadata
  • format: html → output type
  • The header controls everything about your document

Change format: html to format: docx → the entire output changes to Word!

YAML (Yet Another Markup Language), is a human-friendly way to specify options and metadata

YAML Options

---
title: "Oat Yield Analysis"
author: "Your Name"
format:
  docx: default
   pdf: default
  html:
    toc: true              # table of contents
    code-fold: show        # collapsible code blocks
    embed-resources: true  # self-contained HTML file
    (many more)
---

embed-resources: true

Produces a single, self-contained HTML file — all images, CSS, and JS bundled inside. Share via email, USB, or upload anywhere. No broken links!

The YAML header is the “brain” of your document — it controls the overall structure, format, and behavior.

More options in the Quarto formats and execution-options.

Step 2: Text in Markdown

You write:

## Introduction

This study examines the effect
of **nitrogen** on *oat yield*.

Key findings:

- Nitrogen increases yield
- Varieties respond similarly
- [Oats dataset, F. Yates, 1935](https://doi.org/10.2307/2983638)

You get:

Introduction

This study examines the effect of nitrogen on oat yield.

Key findings:

Callout Blocks

Note

Useful for supplementary information.

Tip

Share best practices and shortcuts.

Warning

Highlight potential issues or assumptions.

Important

Critical information — don’t skip this!

Step 3: Code Chunks

Code lives inside the document — it runs when you render.

You write:

```{r}
data(Oats)
Oats <- as.data.frame(Oats)
head(Oats)
```

You get:

data("Oats")
Oats <- as.data.frame(Oats)
head(Oats)
  Block     Variety nitro yield
1     I     Victory   0.0   111
2     I     Victory   0.2   130
3     I     Victory   0.4   157
4     I     Victory   0.6   174
5     I Golden Rain   0.0   117
6     I Golden Rain   0.2   114

Chunk Options: What Each One Does

Option What it controls Default
echo Show the code in the output? true
eval Run the code? true
include Include anything (code + output) in the document? true
output Show the results? true

By default, everything is shown and everything runs. You turn options off to control what the reader sees.

Set per chunk with #| comments:

#| echo: false
#| eval: true

Or globally in the YAML execute: block for the entire document.

Chunk Options: Common Combinations

You want… Set this Code Runs Output
Show everything (default) (nothing)
Hide code, show results #| echo: false
Show code, don’t run it #| eval: false
Run silently (setup) #| include: false

For example: echo: true (default)

The reader sees both the code and the output:

```{r}
#| echo: true
mean(Oats$yield)
```
mean(Oats$yield)
[1] 103.9722

Example: echo: false

The reader sees only the output — the code is hidden:

```{r}
#| echo: false
mean(Oats$yield)
```
[1] 103.9722

Example: eval: false

The reader sees only the code — it doesn’t run:

# This code is displayed but NOT executed
very_slow_model <- train_model(data, epochs = 10000)

Useful for showing examples or syntax without running them.

Example: code-fold (click to toggle)

When you set code-fold: true, the reader sees a button to expand:

Click to see the code
aggregate(yield ~ Variety, data = Oats, FUN = mean)
      Variety    yield
1 Golden Rain 104.5000
2  Marvellous 109.7917
3     Victory  97.6250

Focus on results, but code still there. Same document.

Step 4: Tables with knitr::kable

aggregate(yield ~ Variety, data = Oats, FUN = mean) |>
  knitr::kable(digits = 1, col.names = c("Variety", "Mean Yield"))
Table 1: Mean oat yield by variety
Variety Mean Yield
Golden Rain 104.5
Marvellous 109.8
Victory 97.6

Cross-reference: As shown in Table 1, Marvellous has the highest mean yield.

“The table number updates itself if modified.”

Tables with {gt}

Same data, publication-ready in a few extra lines:

Oats |>
  dplyr::group_by(Variety, nitro) |>
  dplyr::summarise(mean_yield = round(mean(yield), 1), .groups = "drop") |>
  tidyr::pivot_wider(names_from = nitro, values_from = mean_yield,
                     names_prefix = "N = ") |>
  gt::gt() |>
  gt::tab_header(
    title    = "Oat Yield by Variety and Nitrogen",
    subtitle = "Mean yield (bushels/acre)"
  )
Table 2: Mean oat yield by variety and nitrogen level
Oat Yield by Variety and Nitrogen
Mean yield (bushels/acre)
Variety N = 0 N = 0.2 N = 0.4 N = 0.6
Golden Rain 80.0 98.5 114.7 124.8
Marvellous 86.7 108.5 117.2 126.8
Victory 71.5 89.7 110.8 118.5

Step 5: Figures + Cross-References

ggplot2::ggplot(Oats, ggplot2::aes(x = factor(nitro), y = yield)) +
  ggplot2::geom_boxplot(alpha = 0.5) +
  ggplot2::geom_jitter(ggplot2::aes(color = Variety), width = 0.1, size = 2) +
  ggplot2::labs(x = "Nitrogen", y = "Yield") +
  ggplot2::theme_minimal(base_size = 16)
Figure 1: Oat yield by nitrogen application rate

Cross-References

Reference figures and tables by their label:

As shown in @fig-yield, nitrogen increases yield.
Model coefficients are in @tbl-model.

Rendered:
As shown in Figure 1, nitrogen increases yield.
Model coefficients are in Table 2

Labels must start with a prefix

fig- for figures, tbl- for tables, eq- for equations, sec- for sections.

Step 6: Inline Code (blend in the text)

You write:

The dataset contains
**`​r nrow(Oats)`** observations.

The mean yield was
**`​r round(mean(Oats$yield), 1)`**
bushels/acre.

Yield ranged from
`​r min(Oats$yield)` to
`​r max(Oats$yield)`.

You get:

The dataset contains 72 observations.

The mean yield was 104 bushels/acre.

Yield ranged from 53 to 174.

The numbers come directly from the data. Change the data, the text updates.

Step 7: Second Figure

ggplot2::ggplot(Oats, ggplot2::aes(x = nitro, y = yield, color = Variety)) +
  ggplot2::geom_point(size = 2.5, alpha = 0.6) +
  ggplot2::geom_smooth(method = "lm", se = TRUE, alpha = 0.2) +
  ggplot2::labs(x = "Nitrogen (cwt/acre)", y = "Yield (bu/acre)") +
  ggplot2::theme_minimal(base_size = 16) +
  ggplot2::theme(legend.position = "top")
Figure 2: Nitrogen-yield response by oat variety

Now we have Figure 1 (Figure 1) and Figure 2 (Figure 2). Add or remove figures — numbering is always correct.

Step 8: Fitting a Model

model <- lm(yield ~ nitro + Variety, data = Oats)
summary(model)

Call:
lm(formula = yield ~ nitro + Variety, data = Oats)

Residuals:
    Min      1Q  Median      3Q     Max 
-33.725 -15.133  -1.392  12.333  54.275 

Coefficients:
                  Estimate Std. Error t value Pr(>|t|)    
(Intercept)         82.400      5.483  15.029  < 2e-16 ***
nitro               73.667     11.192   6.582 7.96e-09 ***
VarietyMarvellous    5.292      6.130   0.863    0.391    
VarietyVictory      -6.875      6.130  -1.122    0.266    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 21.24 on 68 degrees of freedom
Multiple R-squared:  0.4102,    Adjusted R-squared:  0.3841 
F-statistic: 15.76 on 3 and 68 DF,  p-value: 6.965e-08

The results flow into the document.

Tidy Model Table with {broom} + {gt}

broom::tidy(model, conf.int = TRUE) |>
  dplyr::mutate(dplyr::across(dplyr::where(is.numeric), \(x) round(x, 3))) |>
  gt::gt() |>
  gt::tab_header(
    title    = "Linear Model: Yield ~ Nitrogen + Variety",
    subtitle = "Oat yield experiment"
  )
Table 3: Linear model coefficients
Linear Model: Yield ~ Nitrogen + Variety
Oat yield experiment
term estimate std.error statistic p.value conf.low conf.high
(Intercept) 82.400 5.483 15.029 0.000 71.459 93.341
nitro 73.667 11.192 6.582 0.000 51.334 96.000
VarietyMarvellous 5.292 6.130 0.863 0.391 -6.941 17.524
VarietyVictory -6.875 6.130 -1.122 0.266 -19.107 5.357

From raw output to publication table — no manual formatting. See Table 3.

Model Equation with {equatiomatic}

Extract the symbolic equation directly from the model:

equatiomatic::extract_eq(model, wrap = TRUE)

\[ \begin{aligned} \operatorname{yield} &= \alpha + \beta_{1}(\operatorname{nitro}) + \beta_{2}(\operatorname{Variety}_{\operatorname{Marvellous}}) + \beta_{3}(\operatorname{Variety}_{\operatorname{Victory}})\ + \\ &\quad \epsilon \end{aligned} \]

Now with estimated coefficients:

equatiomatic::extract_eq(model, use_coefs = TRUE, wrap = TRUE)

\[ \begin{aligned} \operatorname{\widehat{yield}} &= 82.4 + 73.67(\operatorname{nitro}) + 5.29(\operatorname{Variety}_{\operatorname{Marvellous}}) - 6.87(\operatorname{Variety}_{\operatorname{Victory}}) \end{aligned} \]

The equation writes itself from the model object. Change the model, the equation updates.

Auto-Generated Report with {report}

report::report(model)
We fitted a linear model (estimated using OLS) to predict yield with nitro and
Variety (formula: yield ~ nitro + Variety). The model explains a statistically
significant and substantial proportion of variance (R2 = 0.41, F(3, 68) =
15.76, p < .001, adj. R2 = 0.38). The model's intercept, corresponding to nitro
= 0 and Variety = Golden Rain, is at 82.40 (95% CI [71.46, 93.34], t(68) =
15.03, p < .001). Within this model:

  - The effect of nitro is statistically significant and positive (beta = 73.67,
95% CI [51.33, 96.00], t(68) = 6.58, p < .001; Std. beta = 0.61, 95% CI [0.43,
0.80])
  - The effect of Variety [Marvellous] is statistically non-significant and
positive (beta = 5.29, 95% CI [-6.94, 17.52], t(68) = 0.86, p = 0.391; Std.
beta = 0.20, 95% CI [-0.26, 0.65])
  - The effect of Variety [Victory] is statistically non-significant and negative
(beta = -6.87, 95% CI [-19.11, 5.36], t(68) = -1.12, p = 0.266; Std. beta =
-0.25, 95% CI [-0.71, 0.20])

Standardized parameters were obtained by fitting the model on a standardized
version of the dataset. 95% Confidence Intervals (CIs) and p-values were
computed using a Wald t-distribution approximation.

The package report is handy, and works with several model outputs.

It generates a narrative summary of the model results.

Step 9: One File → Many Formats

flowchart LR
  A[📄 .qmd file] --> B[🌐 HTML]
  A --> C[📑 PDF]
  A --> D[📝 Word]
  A --> E[🎤 Slides]
  A --> F[📚 ePub]

flowchart LR
  A[📄 .qmd file] --> B[🌐 HTML]
  A --> C[📑 PDF]
  A --> D[📝 Word]
  A --> E[🎤 Slides]
  A --> F[📚 ePub]

One .qmd, many outputs

When you render to HTML with multiple formats, Quarto automatically adds download links for PDF, Word, and ePub — readers can choose!

# install.packages("reticulate")
library(reticulate)
# reticulate::install_miniconda()

reticulate::py_config()
python:         /home/ds/.cache/R/reticulate/uv/cache/archive-v0/2jhW8OVLRnKsG4octEWJA/bin/python
libpython:      /home/ds/.cache/R/reticulate/uv/python/cpython-3.12.13-linux-x86_64-gnu/lib/libpython3.12.so
pythonhome:     /home/ds/.cache/R/reticulate/uv/cache/archive-v0/2jhW8OVLRnKsG4octEWJA:/home/ds/.cache/R/reticulate/uv/cache/archive-v0/2jhW8OVLRnKsG4octEWJA
virtualenv:     /home/ds/.cache/R/reticulate/uv/cache/archive-v0/2jhW8OVLRnKsG4octEWJA/bin/activate_this.py
version:        3.12.13 (main, Mar 10 2026, 18:17:25) [Clang 21.1.4 ]
numpy:          /home/ds/.cache/R/reticulate/uv/cache/archive-v0/2jhW8OVLRnKsG4octEWJA/lib/python3.12/site-packages/numpy
numpy_version:  2.4.4

NOTE: Python version was forced by py_require()
reticulate::py_require("pandas")
reticulate::py_require("matplotlib")

# Sys.setenv(RETICULATE_PYTHON = "~/miniforge3/bin/python3.12")
# reticulate::use_python("/home/ds/miniforge3/bin/python3.12")
# reticulate::py_install("pandas", pip = TRUE)
# reticulate::py_install("matplotlib", pip = TRUE)

Tabsets (Tabs)

boxplot(yield ~ nitro, data = Oats)

ggplot2::ggplot(Oats, ggplot2::aes(x = factor(nitro), y = yield)) + 
  ggplot2::geom_boxplot() + 
    ggplot2::theme_minimal()

import pandas as pd
import matplotlib.pyplot as plt
df = r.Oats
df.boxplot(column='yield', by='nitro')
plt.show()

Same analysis, three languages, one document.

Python Integration

Quarto supports Python natively. From RStudio, we use {reticulate}:

import pandas as pd
df = r.oats_py
print(f"Dataset: {df.shape[0]} rows × {df.shape[1]} columns")
Dataset: 72 rows × 4 columns
import pandas as pd
print(df.groupby('Variety')['yield'].mean().round(1))
Variety
Golden Rain    104.5
Marvellous     109.8
Victory         97.6
Name: yield, dtype: float64

Same document, same workflow — R and Python side by side.

YAML Tricks

Dark / Light toggle

theme:
  dark: darkly
  light: cosmo

Readers switch between themes — built-in!

Self-contained HTML

embed-resources: true

A single HTML file with everything inside. The “portable PDF” of the HTML world.

Collapsible code

code-fold: show
code-tools: true

Readers toggle code visibility with a click.

Table of contents

toc: true
toc-depth: 3
toc-location: left

Automatic, clickable navigation.

Facts to consider before use Quarto

  • Heavy pipelines → separate computation (.R) from reporting (.qmd). Load saved results with readRDS().
  • Large projects → split into a Quarto Project with multiple .qmd files (books, websites).
  • Real-time collaboration → render to .docx, share with co-authors, incorporate feedback — or use version control (Git).

Tip

These aren’t reasons to avoid Quarto — they’re strategies for using it well.

Let’s Build One From Scratch

Open RStudio → File → New File → Quarto Document


If you have RStudio (Quarto is built in) — follow along!

If not — All materials in the link/QR code.


github.com/Gelsleichter/quarto_ws

What We Covered

  • ✅ Why reproducibility matters
  • ✅ Anatomy of a .qmd file (YAML + Markdown + Code)
  • ✅ Tables (kable, gt), figures (ggplot2), inline code
  • ✅ Cross-references (@fig-, @tbl-)
  • ✅ Model output: equations (equatiomatic), tidy tables (broom), auto-reports (report)
  • ✅ Python integration (reticulate)
  • ✅ One file → many formats (HTML, Word, PDF, Slides)

Keyboard Shortcuts (RStudio)

Shortcut Action
Ctrl + Shift + K Render document
Ctrl + Alt + I Insert code chunk
Ctrl + Shift + Enter Run current chunk
Ctrl + Enter Run current line
Ctrl + Shift + M Insert pipe |>

Resources & References

Official

Workshops & Tutorials

R Packages Used Today

ggplot2 · gt · equatiomatic · broom · report · knitr · reticulate · qrcode · dplyr · tidyr

Thank You!

Questions?


📧 Gelsleichter.Yuri.Andrei@uni-mate.hu

🔗 github.com/Gelsleichter


github.com/Gelsleichter/quarto_ws