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

NoteA small exception: display-only chunks and inline code

“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
```
TipUse #| 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).

![Kōura shelter preference by size class. Stone configurations
were preferred across all size classes, with the strongest
preference in adult animals.](
outputs/figures/fig-preference.png){#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.

## References
TipGoing further: rich scholarly metadata

The 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.qmd is open → generates HTML with all your code and outputs
  • Click Render when index.qmd is 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.

TipWhy does index.qmd load tidyverse and re-read the CSV again?

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.

NotePlaceholder data example

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.