flowchart TD
A{"Is the document<br/>finished and published?"}
A -->|"Yes"| B["Leave it<br/><i>it renders; migration<br/>buys nothing</i>"]
A -->|"No"| C{"Do you need a<br/>Quarto-only capability?"}
C -->|"Yes"| D["Convert<br/><i>multi-language, freeze,<br/>cross-format fidelity</i>"]
C -->|"No"| E{"Will it grow, or<br/>change hands?"}
E -->|"Yes"| D
E -->|"No"| B
16 Rmd Workflow: Conversions and Tables
Programs must be written for people to read, and only incidentally for machines to execute.
Harold Abelson and Gerald Jay Sussman, Structure and Interpretation of Computer Programs (1985)
Blog posts 17-rapidconversionRtoRmd, 38-tableplacementrmarkdown, 07-multilanguagequartodemo.
16.1 Prerequisites
Answer the following questions to see if you can bypass this chapter. You can find the answers at the end of the chapter in Section 16.17.
- What is the quickest way to convert an existing
.Rscript into an.Rmdor.qmd, preserving comments as prose? - Why do tables and figures in rendered PDFs often appear in unexpected places, and what do
fig.posandtbl-posdo? - How do you ensure that a figure or table label defined in an Rmd is clickable (hyperlinked) in both the HTML and the PDF outputs?
16.2 Learning objectives
By the end of this chapter you should be able to:
- Convert between
.R,.Rmd, and.qmdquickly and losslessly. - Decide whether an existing R Markdown project should be migrated at all, and carry out the migration of a
bookdownproject when it should. - Detect the cross-reference breakage that a conversion introduces without raising an error.
- Control LaTeX float placement with
fig.pos/tbl-posand inline anchoring ([H]). - Produce cross-referenced figures and tables that hyperlink correctly in HTML, PDF, and Word outputs.
- Switch a project’s engine between
knitrand Quarto without rewriting chunks. - Run a multi-language document (R, Python, Julia) in a single render via
reticulateorJuliaCall. - Debug common Rmd/Quarto rendering failures.
16.3 Orientation
The preceding chapter argued for the literate document. This one is about living in it, which is a different problem and a less elevated one. A document is rendered once when it is written and perhaps forty times before it is submitted, and the frictions that are negligible on the first render are the dominant cost by the fortieth: the figure that lands three pages from its citation, the cross-reference that works in HTML and breaks in PDF, the expensive chunk that re-runs every time you fix a typo.
None of this is intellectually interesting, and all of it is worth knowing, because the alternative to knowing it is doing the cleanup by hand every time. We collect here the practical measures that pay off on the fifth render rather than the first.
The chapter applies to both R Markdown (.Rmd) and Quarto (.qmd); Quarto is the modern default but R Markdown remains widely used. Most concepts and syntax transfer between them.
16.4 The statistician’s contribution
Conversions and float placement are lookups. The judgments that save time on the fifth render rather than the first are about consistency:
Decide on a primary format and stick to it. A project that mixes .R files, .Rmd reports, and .qmd papers is hard to maintain. Pick a primary format for the project (.qmd for new work) and convert anything else to match.
LaTeX float discipline. Floats are LaTeX’s way of making documents look polished, but they can place a figure pages away from where you cited it. For a paper, let LaTeX do its job; readers expect floating figures. For a report meant to be read top-to-bottom, force inline placement with fig.pos = 'H'. The right default depends on the audience.
Multi-language honesty. Mixing R, Python, and Julia in one document is technically possible. Whether it is helpful depends on whether the languages do genuinely different jobs (R for statistics, Python for deep learning, Julia for performance) or whether the mix is showmanship. Single-language is usually the right answer.
Convert before the document is large. Converting a 1000-line .Rmd to .qmd is harder than converting a 100-line one. If you suspect the document will grow, convert early.
These judgments are what separate a workflow that saves time from one that creates new categories of problem.
16.5 Converting between formats
The conversion tools:
.R → .Rmd / .qmd with knitr::spin():
# script.R has special comments:
#' # Title
#' Some prose explaining what this does.
1 + 1
#' Another paragraph of prose.
2 + 2knitr::spin("script.R", knit = FALSE)
# produces script.Rmd with prose and chunksThe #' prefix marks a comment as prose; everything else becomes code. For converting an existing heavily-commented script to a literate document, this is the fast path.
.Rmd → .R with knitr::purl():
knitr::purl("paper.Rmd", documentation = 0)
# extracts code chunks into paper.Rdocumentation = 1 keeps prose as comments; 2 keeps prose as roxygen-style comments.
.Rmd → .qmd with knitr::convert_chunk_header() or quarto convert:
quarto convert paper.Rmd -o paper.qmdMost chunk options translate directly. The differences to handle by hand:
YAML differences (Quarto uses slightly different format keys:
format: htmlvs.output: html_document).Chunk-option syntax: knitr uses
```{r chunk-name, echo = FALSE}; Quarto’s preferred syntax uses#|directives:Cross-reference syntax: knitr uses
\@ref(fig:scatter); Quarto uses@fig-scatter(different prefix).
.ipynb ↔︎ .qmd:
quarto convert notebook.ipynb -o notebook.qmd
quarto convert notebook.qmd -o notebook.ipynbFor collaborators who prefer Jupyter, this conversion is lossless: text becomes Markdown, code becomes chunks, and metadata round-trips.
16.6 Migrating an R Markdown project
The conversion above is a file operation. Migrating a project is a different exercise, and the first question is whether to do it at all.
16.6.1 Whether to convert
The reflex is to say yes, on the grounds that Quarto is the successor and current tools are better than superseded ones. That reflex is wrong often enough to be worth resisting, because it mistakes currency for value. A finished analysis that renders correctly under a pinned environment already does everything this book asks of it. Migrating it produces the same output from newer machinery, and in exchange you accept a window in which the document does not render and the numbers have not yet been checked against the old ones. There is no reproducibility argument for the change, and there is a small reproducibility argument against it.
Figure 16.1 is the decision.
Anything tagged at submission (Chapter 7) belongs in the left branch without further thought. The tagged commit is the record of what was reported, and it should render with the toolchain that produced it.
16.6.2 What quarto convert leaves for you
The command handles chunk headers and the obvious YAML keys. It does not touch cross-references, does not know that a project is a bookdown project, and does not audit what your packages assumed about knitr. Budget for those three by hand.
Cross-references fail silently, which makes them the dangerous item. A \@ref(fig:scatter) left unconverted does not stop the render. It emits a broken reference into the output, and a document with thirty figures can acquire a dozen of them without a single warning. Two habits contain this. After converting, grep the source for \@ref( and confirm the count is zero. Then grep the rendered output for ?fig- and ?tbl-, which is what Quarto emits where a reference does not resolve, and confirm that count is zero as well. The second check matters because it catches the converse error, a reference whose syntax you updated but whose target label you did not.
Quarto is also stricter about labels than bookdown was. A figure is only referenceable if its label carries the fig- prefix, a table if it carries tbl-. A chunk named scatter that was addressable as fig:scatter under bookdown becomes addressable as nothing at all until it is renamed fig-scatter.
Package assumptions are the quiet cost. Code that reached around knitr rather than through it does not convert, because there is nothing to convert: it simply behaves differently. The recurring cases are kableExtra’s LaTeX-specific options, any output format in the bookdown:: namespace, and custom knit_hooks. Each has to be replaced rather than translated.
16.6.3 bookdown projects
A single .Rmd is a file. A bookdown project is a build system, and it is the case most readers who want to migrate actually have.
The mapping is mostly mechanical. _bookdown.yml and _output.yml collapse into the single _quarto.yml of Chapter 15, chapter files are listed under chapters: rather than inferred from filenames, and index.Rmd becomes an ordinary index.qmd whose YAML carries the book metadata. Part divisions, which bookdown expressed as a specially formatted heading, become part: entries.
What is not mechanical is the numbering. Cross-file references, chapter numbers appearing in prose, and any figure numbered by hand will all move, and they will move silently for the same reason described above. The discipline that makes this tractable is to convert one chapter at a time and keep the bookdown build working until the Quarto build matches it. Render both, and compare the outputs rather than trusting that a clean render means a correct one. A clean render means the document compiled.
16.6.4 The boundary you may not control
Migration is rarely unilateral. A co-author may still be editing .Rmd in RStudio, a journal template you depend on may exist only as an rticles output format, and a continuous-integration workflow (Chapter 27) may invoke rmarkdown::render on a path that no longer exists. Each of these is a reason the sensible unit of migration is a project rather than a file: converting half of something leaves two toolchains to maintain, which is worse than either one alone.
16.7 Float placement in PDF
The frustration: you place \@ref(fig:demographics) in your prose and the figure appears two pages later. LaTeX is exercising its default ‘avoid widow lines’ heuristic.
The fix in knitr Rmd:
```r
plot(...)
```
Or globally:
knitr::opts_chunk$set(fig.pos = "H")In Quarto:
execute:
fig-pos: "H"H is from the float LaTeX package, meaning ‘place exactly here, do not float’. Add to the YAML:
header-includes:
- \usepackage{float}For tables, the equivalent is tbl-pos: 'H' in Quarto or kable_styling(latex_options = "HOLD_position") in knitr+kableExtra.
The LaTeX float-position options:
h(here, if possible)t(top of page)b(bottom of page)p(separate float page)H(exactly here, requiresfloatpackage)!prefix overrides LaTeX’s preferences
The default htbp lets LaTeX decide; H forces inline.
For HTML output, LaTeX float positioning is irrelevant; figures appear in source order.
16.8 Cross-references
Quarto’s cross-reference system is consistent across formats:
See @fig-demographics for the cohort breakdown
(@tbl-baseline summarizes baseline characteristics).
```r
#| label: fig-demographics
#| fig-cap: "Demographics by treatment arm"
ggplot(...) + ...
```
```r
#| label: tbl-baseline
#| tbl-cap: "Baseline characteristics"
gtsummary::tbl_summary(...)
```
Labels follow the prefix convention: fig-, tbl-, eq-, sec-, lst-, tip-. References use the @ prefix to match.
In knitr Rmd, the equivalent syntax is more verbose:
```r
ggplot(...) + ...
```
See \@ref(fig:demographics).
Both produce hyperlinks in HTML and PDF.
For sections:
## Methods {#sec-methods}
See @sec-methods for details.
The numeric value of the cross-reference is set by the order of the labeled object in the document; this is why renumbering happens automatically when you reorder.
16.9 Engine choice: knitr vs. Quarto
Both are options for .qmd files. Default is knitr; engine: jupyter switches to Jupyter for Python-heavy documents.
knitr chunks support all the chunk options you know from Rmd. Quarto’s #| syntax is preferred but the old {r, opt = val} syntax still works. For most R work, the engine choice is invisible.
For mixing R and Python heavily in one document, Jupyter engine may produce cleaner output. For pure R or R-with-occasional-Python, knitr is fine.
16.10 Multi-language documents
Quarto natively supports R, Python, and Julia. With reticulate (R-Python) and JuliaCall (R-Julia), chunks can pass values across languages.
---
format: html
---
```r
library(reticulate)
x <- 1:10
```
```python
import numpy as np
arr = np.array(r.x) # access R variable
print(arr.mean())
```
```r
print(py$arr) # access Python variable
```
The r.x syntax in Python accesses R’s x; the py$arr syntax in R accesses Python’s arr. Coercion is automatic for common types (numeric vectors, data frames, lists).
For a typical biostatistical workflow, this is overkill: stick to R unless the project genuinely needs another language (deep learning in Python, a specialized Julia package). Multi-language is real infrastructure cost.
16.11 Common rendering failures
‘LaTeX Error: File tinytex.sty not found’: install TinyTeX (tinytex::install_tinytex()) or full LaTeX. Quarto’s PDF output requires LaTeX.
‘pandoc: … unknown writer’: out-of-date Quarto or pandoc. Update Quarto.
‘undefined cross-reference’: a @fig-foo reference to a label fig-foo that does not exist (typo, or the chunk producing the figure was not labeled). Render with --verbose to find which reference is broken.
Figures appearing in wrong places: float- positioning issue (above).
Slow renders: cache with freeze: auto (Quarto) or cache = TRUE (knitr) for expensive chunks.
‘Object not found’: a chunk depends on a variable defined in an earlier chunk that did not run (or was eval = FALSE). Check chunk order and eval/include options.
16.12 Worked example: converting a script to a paper
Starting from analysis.R:
# Load and clean data
d <- read.csv("data.csv")
d <- na.omit(d)
# Fit model
fit <- lm(y ~ x1 + x2, data = d)
summary(fit)
# Plot
plot(d$x1, d$y, xlab = "x1", ylab = "y")
abline(fit)Step 1: convert with spin:
knitr::spin("analysis.R", knit = FALSE, format = "Rmd")
# → analysis.RmdStep 2: convert to .qmd:
quarto convert analysis.Rmd -o analysis.qmdStep 3: add YAML, prose, cross-references:
---
title: "Effect of x1 on y"
format:
html: default
pdf: default
execute:
echo: false
warning: false
---
# Methods
We analyzed `{r} nrow(d)` complete cases.
```r
#| label: fig-scatter
#| fig-cap: "Scatterplot of y on x1"
plot(d$x1, d$y, xlab = "x1", ylab = "y")
abline(fit)
```
@fig-scatter shows the relationship; the fitted slope
is `{r} round(coef(fit)["x1"], 2)`.
Step 4: render. Half an hour from script to first-draft paper.
16.13 Collaborating with an LLM on Rmd workflow
LLMs handle conversions well; the cross-format subtleties need verification.
Prompt 1: converting .R to .qmd. Paste the script and ask the LLM to produce a .qmd with appropriate chunk options and prose.
What to watch for. The LLM may invent prose where the original had none. Sticking close to the original’s intent is the right move; rewriting is for later.
Verification. Render both and compare outputs. Check that no comments or code were lost.
Prompt 2: diagnosing rendering failures. Paste the error message and ask: ‘what’s wrong and how to fix?’
What to watch for. Common errors (LaTeX missing, cross-reference broken, package not installed) are easy. Less common (Quarto version mismatch, pandoc filter issues) are harder; verify against the official documentation.
Verification. Apply the fix and re-render. If it works, done; if not, the LLM may have misdiagnosed.
Prompt 3: cross-format cross-references. Describe the document and ask: ‘set up cross-references that work in HTML, PDF, and Word.’
What to watch for. Quarto’s @fig-foo syntax works universally. The LLM should use it. If it suggests \ref{fig:foo}, push back: that is LaTeX-only.
Verification. Render to all three formats and inspect the cross-references in each.
16.14 Principle in use
Three habits keep the fifth render as cheap as the first:
- Convert to a single primary format.
.qmdfor new work; convert legacy.Rmdif active. A project that mixes formats is harder to maintain than the conversion cost. - Use Quarto cross-references universally. They work across HTML, PDF, and Word; LaTeX-specific labeling does not.
- Cache expensive computations.
freeze: autoon the Quarto side,cache = TRUEon the knitr side. Don’t re-run a 5-minute chunk on every typo fix.
16.15 Exercises
- Take a recent
.Ranalysis and convert it to a.qmdviaknitr::spin. Re-render. Note whatspinhandles well and what needs manual cleanup. - In a Quarto PDF, force every table to appear exactly where declared in the source (no LaTeX float rearrangement). Verify with a three-table document.
- Build a minimal reproducible example (MWE) of a cross-reference that works in HTML but not in PDF. File the bug upstream or post it on the Quarto discussion forum.
- Convert a
.Rmdpaper of yours to.qmd. Render both versions to PDF and diff the output; explain any differences. - Build a
.qmdthat calls Python via reticulate to compute one number used in the prose. Render and verify the Python output is faithful to the R value. - Take a
bookdownproject, your own or any public one, and convert a single chapter of it to Quarto while leaving thebookdownbuild working. Render both and diff the chapter’s output. Report how many cross-references broke, and how many of those breaks produced no message during either render. - Find an R Markdown analysis of yours that is finished and published. Write a short argument for not migrating it, in terms of what migration would and would not change about its reproducibility.
16.16 Further reading
- (Xie et al., 2020), R Markdown Cookbook, the recipe book.
- (Xie, 2016), bookdown: Authoring Books and Technical Documents with R Markdown, for long-form documents.
- Quarto’s
quarto convertdocumentation for format-conversion details.
16.17 Prerequisites answers
knitr::spin('script.R')is the canonical fast path: it turns a commented.Rfile into a rendered.Rmdby treating any comment starting with#'as prose and everything else as code chunks. For.qmd, the same convention works underquarto convert. Half an hour from script to literate document.- LaTeX treats figures and tables as floats and rearranges them by default to avoid bad page breaks.
fig.pos = 'H'(requires thefloatpackage) andtbl-pos: 'H'in Quarto YAML tell LaTeX ‘place this exactly here, no reflow’. In chunks,fig.pos = 'htbp'means ‘try here, then top, then bottom, then a float page’ in order. - Use Quarto’s cross-reference system:
{#fig-scatter}on the figure plus@fig-scatterin the prose. Quarto handles HTML and PDF (and Word) anchoring with the same source syntax. Hand-rolled\label{}/\ref{}works only in PDF. The Quarto syntax is the durable choice.