2. Quarto Flow
Two files, one workflow: analysis.qmd and index.qmd
4 min read
The heart of this workflow is a two-file Quarto structure. Most tutorials use a single .qmd file for everything, but separating your analysis from your manuscript gives you the best of both worlds: a clean, readable paper and a fully transparent, reproducible analysis.
Why two files?
analysis.qmd |
index.qmd |
|
|---|---|---|
| Purpose | Reproducible analytical record | Clean journal manuscript |
| Contains | All R code, models, figures, tables | Prose, plus small hidden chunks that display precomputed figures/tables/stats |
| Who reads it | Reviewers, other researchers | Journal editors, readers |
| Renders to | HTML (best for code display) | PDF or Word for submission, HTML for a quick preview |
| Tone | Code-heavy, annotated | Academic prose |
Both files are version controlled together on GitHub. Together they give the complete picture of your research.
How they connect
analysis.qmd runs all your R code and saves figures and derived data to the outputs/ folder. index.qmd references those saved files.
analysis.qmd → runs R code → saves to outputs/figures/
↓
index.qmd → references figure → manuscript PDF
This means: - Changing your analysis in analysis.qmd automatically updates the figures, tables, and any inline numbers in index.qmd when you re-render - Your manuscript stays clean — no modelling or cleaning code in the middle of your methods section - A reviewer can open analysis.qmd and see every modelling decision you made
“Text only” doesn’t mean zero code in index.qmd. A tiny echo: false chunk that reads a saved table and prints it, or inline code like `r round(report_stats$mean_stone, 1)` in a sentence, is normal and encouraged — it’s not analysis, just display. This is what keeps the numbers in your prose in sync with your actual results instead of a hardcoded value someone forgets to update after a revision.
The rule of thumb: any filtering, subsetting, or computation happens in analysis.qmd and gets saved to outputs/. index.qmd only ever reads — it never filters a data frame or picks out a row itself. If index.qmd needs mean_stone, analysis.qmd computes and saves mean_stone; index.qmd just loads it.
Structure of analysis.qmd
---
title: "Analysis: Kōura habitat preference study"
format: html
execute:
echo: true # show all code
warning: false
message: false
---
## Setup
```{r}
#| label: setup
# Packages are installed once via renv::restore() (see chapter 1) —
# no install.packages() here, so everyone gets the exact locked versions.
library(tidyverse)
library(ggplot2)
# Load raw data — never modify data/raw/ itself, only read from it
data <- read_csv("data/raw/koura_data.csv")
```
## Data cleaning
```{r}
#| label: data-cleaning
# Remove incomplete observations
data_clean <- data |>
filter(!is.na(shelter_type)) |>
mutate(size_class = cut(carapace_length_mm,
breaks = c(0, 20, 35, Inf),
labels = c("juvenile", "sub-adult", "adult")))
# Save derived data
write_csv(data_clean, "data/derived/koura_clean.csv")
```
## Summary table
```{r}
#| label: tbl-summary
#| tbl-cap: "Mean carapace length by shelter type"
summary_table <- data_clean |>
summarise(
n = n(),
mean_length = mean(carapace_length_mm),
sd_length = sd(carapace_length_mm),
.by = shelter_type
)
write_csv(summary_table, "outputs/tables/summary_table.csv")
summary_table
```
## Statistical models
```{r}
#| label: model-preference
#| cache: true
model_preference <- lm(carapace_length_mm ~ shelter_type, data = data_clean)
summary(model_preference)
saveRDS(model_preference, "outputs/model_preference.rds")
# Save exactly the values index.qmd needs to report inline. Computed and
# filtered here, in the reproducible record — index.qmd only ever reads
# this back, it never filters or subsets anything itself.
report_stats <- list(
mean_stone = summary_table$mean_length[summary_table$shelter_type == "stone"],
pvalue = summary(model_preference)$coefficients[2, "Pr(>|t|)"]
)
saveRDS(report_stats, "outputs/report_stats.rds")
```
## Figures
```{r}
#| label: fig-preference
#| fig-cap: "Kōura shelter preference by size class"
fig_preference <- ggplot(data_clean,
aes(x = shelter_type,
fill = size_class)) +
geom_bar(position = "dodge") +
scale_fill_manual(values = c("#2E7D6B", "#4CAF93", "#A8D5C8")) +
labs(x = "Shelter type",
y = "Count",
fill = "Size class") +
theme_minimal()
# Save for use in manuscript
ggsave("outputs/figures/fig-preference.png",
fig_preference,
width = 7, height = 5, dpi = 300)
fig_preference
```#| cache: true for slow models
Some models take a while to run, especially with large datasets. Adding #| cache: true to those chunks means Quarto saves the result and skips re-running on the next render unless the code changes. Combined with saveRDS(), this keeps your workflow fast even as your analysis grows.
Structure of index.qmd
---
title: "Shelter structure drives refuge selection in kōura"
author: "Olivier V. Raven, A. N. Other"
date: today
format:
pdf:
documentclass: article
linestretch: 2
docx:
reference-doc: journal-template.docx
html: # handy for a quick preview, not for submission
toc: true
bibliography: references/references.bib
csl: references/aquatic-conservation.csl
---
## Abstract
...
## Introduction
Freshwater crayfish depend on physical refuges to avoid
predation [@Parkyn2001]. Here we tested...
## Methods
### Study species
Kōura (*Paranephrops planifrons*) were collected from...
### Statistical analysis
We fitted linear models in R [@RCoreTeam2024]...
## Results
```{r}
#| label: setup-results
#| echo: false
library(tidyverse)
summary_table <- read_csv("outputs/tables/summary_table.csv")
report_stats <- readRDS("outputs/report_stats.rds")
```
Kōura showed a strong preference for stone configurations
over wood (see @fig-preference). Carapace length also differed
significantly by shelter type (mean stone = `r round(report_stats$mean_stone, 1)` mm,
p = `r round(report_stats$pvalue, 3)`; @tbl-summary).
{#fig-preference}
```{r}
#| label: tbl-summary
#| tbl-cap: "Mean (± SD) carapace length by shelter type"
#| echo: false
knitr::kable(summary_table)
```
## Discussion
...
## Data availability
Data and code supporting this study are available on
Zenodo at https://doi.org/10.5281/zenodo.XXXXXXX and
on GitHub at https://github.com/olivierraven/
koura-habitat-mesocosm.
## ReferencesThe author: field can hold far more than a name — structured entries with ORCID iDs, affiliations, a corresponding-author flag, funding statements, and keywords, all of which flow through into your rendered PDF/HTML and are readable by indexers like Crossref. Handy for a thesis chapter or anything with multiple co-authors:
author:
- name:
given: A. N.
family: Other
orcid: 0000-0000-0000-0000
affiliations:
- name: Your University
department: Your Department
corresponding: true
- name:
given: Second
family: Author
orcid: 0000-0000-0000-0001
affiliations:
- name: Collaborating Institution
keywords:
- your keyword
- another keyword
funding: "This research was supported by [funder], grant [number]."See Quarto’s author/affiliations docs for the full schema.
Rendering both files
In Positron, you can render each file independently:
- Click Render in the top bar when
analysis.qmdis open → generates HTML with all your code and outputs - Click Render when
index.qmdis open → generates your PDF manuscript
Always render analysis.qmd first if you have changed any code, so the figures in outputs/figures/ are up to date before the manuscript renders.
Each .qmd file renders in its own independent R session — index.qmd never sees the objects analysis.qmd created in memory, only the files it saved (outputs/tables/summary_table.csv, outputs/report_stats.rds, outputs/figures/fig-preference.png). That’s the whole point of the two-file split: index.qmd’s hidden setup chunk just re-loads those saved results, it doesn’t repeat any analysis.
The code examples on this page use realistic but placeholder kōura data. The actual data structure used in published kōura mesocosm studies includes columns for shelter_type, carapace_length_mm, tank_id, round, and sex. If you are adapting this for your own species, adjust the variable names accordingly.
Session info and reproducibility
At the end of your analysis.qmd, always include:
#| label: session-info
sessionInfo()This prints all package versions in your rendered HTML — a second line of defence alongside renv.lock for anyone trying to reproduce your work.