This tutorial shows how to create a reproducible report that combines narrative, R code, computed results, plots, and tables. By the end, you should be able to:
.Rmd file with YAML, Markdown, and
executable code;knitr::kable(); andTerminology: knitr is an R package.
kable() is a function exported by knitr, not a
separate package. The optional kableExtra package adds
advanced styling, but it is not required for the examples in this
guide.
An R Markdown document is a plain-text source file. It keeps the explanation and the analysis together, which makes the report easier to reproduce and update.
Rendering pipeline
.Rmd source →
knitr executes code and creates Markdown/assets →
Pandoc converts them → HTML, Word, PDF, or another
output
Two functions in this pipeline have different jobs:
knitr::knit() executes chunks and normally creates an
intermediate Markdown document.rmarkdown::render() coordinates knitr,
Pandoc, and the requested output format to create the final
document.R Markdown is useful for exploratory analyses, course assignments, laboratory reports, dashboards, model summaries, automated recurring reports, and technical documentation.
Install the required packages from the R console. Do not put package installation in an automatically executed report: rendering should not unexpectedly modify a reader’s R library.
Render from RStudio’s Knit button or from the R console:
rmarkdown::render(
"r_markdown_guide_en.Rmd",
output_file = "r_markdown_guide_en.html",
encoding = "UTF-8"
)Pandoc is also required. RStudio includes it; otherwise install
Pandoc separately and make it available to rmarkdown.
.Rmd fileAn R Markdown file normally has three parts.
The YAML block at the top stores metadata and output settings. Indentation uses spaces, not tabs.
---
title: "Analysis title"
author: "Analyst name"
date: "2026-08-11"
params:
min_mpg: 20
output:
html_document:
toc: true
number_sections: true
code_folding: show
self_contained: true
---Useful HTML options include a table of contents (toc),
numbered sections, syntax highlighting, floating navigation, and
collapsible source code. A self-contained HTML file embeds local CSS,
JavaScript, and generated figures so the result is easy to share as one
file.
Markdown supplies lightweight formatting:
# Level-one heading
## Level-two heading
**bold text**, *italic text*, and `inline code`
- an unordered item
- another item
1. a numbered item
2. another item
[R Markdown documentation](https://pkgs.rstudio.com/rmarkdown/)
> A block quotation
Inline mathematics: $\bar{x} = \frac{1}{n}\sum_{i=1}^{n}x_i$
Displayed mathematics:
$$
s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i-\bar{x})^2
$$Blank lines matter: they separate paragraphs, lists, and block-level output.
A chunk starts with three backticks and {r}, and ends
with three backticks. Give every executed chunk a unique, meaningful
label.
``` r
summary(mtcars$mpg)
```
```
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 10.40 15.43 19.20 20.09 22.80 33.90
```Inline R inserts a short result directly into a sentence. For
example, mtcars has 32 rows, and its mean fuel economy is
20.1 miles per gallon. Inline expressions should be short and should
depend on objects created earlier in the document.
All required objects are created inside this document, so the report does not depend on an interactive Global Environment.
cars_data <- data.frame(
Model = rownames(mtcars),
mtcars[, c("mpg", "cyl", "hp", "wt")],
row.names = NULL,
check.names = FALSE
)
names(cars_data) <- c("Model", "MPG", "Cylinders", "Horsepower", "Weight")
head(cars_data)| Model | MPG | Cylinders | Horsepower | Weight |
|---|---|---|---|---|
| Mazda RX4 | 21.0 | 6 | 110 | 2.620 |
| Mazda RX4 Wag | 21.0 | 6 | 110 | 2.875 |
| Datsun 710 | 22.8 | 4 | 93 | 2.320 |
| Hornet 4 Drive | 21.4 | 6 | 110 | 3.215 |
| Hornet Sportabout | 18.7 | 8 | 175 | 3.440 |
| Valiant | 18.1 | 6 | 105 | 3.460 |
The following plot is generated when the document is knitted.
fig.cap adds a caption; fig.width controls the
graphics device size in inches, whereas out.width controls
its displayed size in the HTML page.
plot(
mtcars$wt,
mtcars$mpg,
xlab = "Weight (1000 lb)",
ylab = "Miles per gallon",
pch = 19,
col = "#2c7fb8"
)
weight_fit <- lm(mpg ~ wt, data = mtcars)
abline(weight_fit, col = "#d95f0e", lwd = 2)Fuel economy generally decreases as vehicle weight increases.
knitr in depthknitr implements the literate-programming layer of R
Markdown: it finds code chunks, evaluates them in order, captures
text/graphics/messages, and writes the results into the intermediate
document.
The hidden setup chunk at the beginning of this file
uses knitr::opts_chunk$set() to define defaults. A local
option in a chunk header overrides the global value only for that
chunk.
## This result is visible even though this chunk's source is hidden.
The source above was executed because eval remained
TRUE; only its display was changed by
echo=FALSE.
| Option | Purpose | Key_point |
|---|---|---|
| echo | Show or hide source code | The code still runs when echo = FALSE |
| eval | Execute or skip the code | eval = FALSE shows code without running it |
| include | Include code and all captured output | include = FALSE still runs the code but hides everything |
| results | Control text output: markup, asis, hide, or hold | results = ‘asis’ treats generated text as document markup |
| message | Show messages produced by code | Messages and warnings are different conditions |
| warning | Show warnings produced by code | Hiding a warning does not fix its cause |
| error | Choose whether knitting may continue after an error | Use error behavior deliberately; do not hide broken analyses |
| fig.width / fig.height | Set graphics-device dimensions, normally in inches | This is not the same as the HTML display size |
| out.width | Set the displayed image width in the output | Percent values such as ‘90%’ are convenient for HTML |
| fig.align | Align a figure: left, center, or right | Applies to the rendered figure |
| fig.cap | Add a figure caption | Useful for accessible, explained output |
| cache | Reuse saved results for an unchanged chunk | External dependencies need special care |
Three commonly confused settings are worth memorizing:
echo=FALSE: run the code, hide the source, keep its
results.eval=FALSE: show the source, do not run it, therefore
produce no result.include=FALSE: run the code, but hide the source and
every captured result.During development, keep messages and warnings visible until you understand them. Suppressing them is a presentation decision, not a repair.
results='asis'Normally, character output is formatted like console output.
results='asis' tells knitr that trusted generated text is
already Markdown or HTML.
Base R, ggplot2, and other graphics systems can be
captured automatically. For an existing image,
knitr::include_graphics() records the file correctly for
the selected output format:
Prefer relative project paths. Avoid setwd() inside a
report because it makes path behavior harder to reason about.
Caching can accelerate slow, deterministic calculations:
```{r fit-expensive-model, cache=TRUE, cache.extra=tools::md5sum("data/input.csv")}
model <- expensive_model(read.csv("data/input.csv"))
```
Do not enable caching everywhere. A code or option change invalidates
the normal cache, but changes to an external file may not.
cache.extra can add a file hash or another version marker
to the cache key. Delete stale caches when in doubt.
knitr toolsknitr::knit() creates the intermediate knitted
document.knitr::purl() extracts R code from an .Rmd
file into an .R script.knitr::include_graphics() inserts external graphics
safely.knitr::knit_child() composes a report from reusable
child documents.knitr::opts_knit controls the knitting process itself;
it is distinct from knitr::opts_chunk, which controls
chunks.knitr::knit_hooks and knitr::opts_hooks
support advanced customization.knitr can also dispatch chunks to other language
engines, but those engines and their runtimes must be installed
separately.
knitr::kable() in depthkable() converts rectangular data such as a data frame
or matrix into a simple table suited to the current output format. Its
deliberately small feature set is an advantage for ordinary reporting:
tables remain readable and portable.
cars_table <- cars_data[1:8, ]
knitr::kable(
cars_table,
digits = 2,
row.names = FALSE,
col.names = c(
"Model", "MPG", "Cylinders", "Horsepower", "Weight (1000 lb)"
),
align = "lrrrr",
caption = "Selected variables for the first eight cars"
)| Model | MPG | Cylinders | Horsepower | Weight (1000 lb) |
|---|---|---|---|---|
| Mazda RX4 | 21.0 | 6 | 110 | 2.62 |
| Mazda RX4 Wag | 21.0 | 6 | 110 | 2.88 |
| Datsun 710 | 22.8 | 4 | 93 | 2.32 |
| Hornet 4 Drive | 21.4 | 6 | 110 | 3.21 |
| Hornet Sportabout | 18.7 | 8 | 175 | 3.44 |
| Valiant | 18.1 | 6 | 105 | 3.46 |
| Duster 360 | 14.3 | 8 | 245 | 3.57 |
| Merc 240D | 24.4 | 4 | 62 | 3.19 |
This example illustrates good defaults for report tables:
| Argument | What it controls |
|---|---|
| x | A rectangular data frame or matrix (and, in supported cases, a list of them) |
| format | Table syntax such as pipe, simple, html, latex, or rst |
| digits | Rounding digits passed to round(); a vector is recycled across table columns |
| row.names | Whether to display row names |
| col.names | Display labels that replace the input column names |
| align | Column alignment: l, c, or r |
| caption | Text shown with the table |
| label | An identifier whose referencing behavior depends on the output framework |
| format.args | A list passed to format(), for example big.mark and scientific |
| escape | Whether HTML or LaTeX special characters are escaped |
| … | Format-specific settings such as table.attr for HTML |
Usually, omit format and let knitr choose it. The
pipe and simple formats are portable Markdown;
html and latex expose output-specific features
but reduce portability.
Create the summary first, then pass a clean rectangular object to
kable().
cylinder_values <- sort(unique(mtcars$cyl))
cylinder_summary <- data.frame(
Cylinders = cylinder_values,
N = vapply(
cylinder_values,
function(value) sum(mtcars$cyl == value),
numeric(1)
),
Mean_MPG = vapply(
cylinder_values,
function(value) mean(mtcars$mpg[mtcars$cyl == value]),
numeric(1)
),
SD_MPG = vapply(
cylinder_values,
function(value) sd(mtcars$mpg[mtcars$cyl == value]),
numeric(1)
)
)
knitr::kable(
cylinder_summary,
digits = c(0, 0, 2, 2),
row.names = FALSE,
col.names = c("Cylinders", "n", "Mean MPG", "SD MPG"),
align = "rrrr",
caption = "Fuel economy summarized by cylinder count"
)| Cylinders | n | Mean MPG | SD MPG |
|---|---|---|---|
| 4 | 11 | 26.66 | 4.51 |
| 6 | 7 | 19.74 | 1.45 |
| 8 | 14 | 15.10 | 2.56 |
digits controls rounding for display but does not by
itself force trailing zeroes. A digits vector is recycled
across all table columns, and numeric columns are passed through
round(). format.args is passed to base R’s
format() and can add thousands separators or avoid
scientific notation. Neither argument changes the values stored in the
input data frame.
numeric_example <- data.frame(
Rate = c(0.12345, 0.98765),
Count = c(1250, 98340),
Amount = c(15230.4, 824500.8)
)
knitr::kable(
numeric_example,
digits = c(3, 0, 2),
format.args = list(big.mark = ",", scientific = FALSE),
row.names = FALSE,
align = "rrr",
caption = "Column-specific rounding and thousands separators"
)| Rate | Count | Amount |
|---|---|---|
| 0.123 | 1,250 | 15,230.4 |
| 0.988 | 98,340 | 824,500.8 |
By default, missing values are printed as NA. The global
option knitr.kable.NA changes their display. Save and
restore the old option so later tables are not changed accidentally.
missing_example <- data.frame(
Student = c("A", "B", "C"),
Score = c(87, NA, 91)
)
old_options <- options(knitr.kable.NA = "—")
knitr::kable(
missing_example,
row.names = FALSE,
caption = "A custom symbol for a missing value"
)| Student | Score |
|---|---|
| A | 87 |
| B | — |
| C | 91 |
table.attrFor a document that targets HTML only, format="html" and
table.attr can attach CSS classes. This is less portable
than a format-neutral table.
knitr::kable(
head(iris, 8),
format = "html",
digits = 1,
row.names = FALSE,
table.attr = 'class="table kable-custom"',
caption = "An HTML table styled with CSS classes"
)| Sepal.Length | Sepal.Width | Petal.Length | Petal.Width | Species |
|---|---|---|---|---|
| 5.1 | 3.5 | 1.4 | 0.2 | setosa |
| 4.9 | 3.0 | 1.4 | 0.2 | setosa |
| 4.7 | 3.2 | 1.3 | 0.2 | setosa |
| 4.6 | 3.1 | 1.5 | 0.2 | setosa |
| 5.0 | 3.6 | 1.4 | 0.2 | setosa |
| 5.4 | 3.9 | 1.7 | 0.4 | setosa |
| 4.6 | 3.4 | 1.4 | 0.3 | setosa |
| 5.0 | 3.4 | 1.5 | 0.2 | setosa |
With the default escape=TRUE, characters meaningful to
HTML or LaTeX are escaped, protecting the document structure. Set
escape=FALSE only for trusted, intentionally generated
markup.
trusted_summary <- data.frame(
Metric = c("Mean MPG", "Maximum MPG"),
Value = c(
sprintf("<strong>%.1f</strong>", mean(mtcars$mpg)),
sprintf("<em>%.1f</em>", max(mtcars$mpg))
),
check.names = FALSE
)
knitr::kable(
trusted_summary,
format = "html",
escape = FALSE,
row.names = FALSE,
caption = "Trusted, author-generated HTML markup"
)| Metric | Value |
|---|---|
| Mean MPG | 20.1 |
| Maximum MPG | 33.9 |
Never use escape=FALSE directly on untrusted or
user-submitted text. It can break the page and can create an
HTML/script-injection vulnerability.
At the top level, a kable() result is normally printed
automatically. Inside a loop or function, print it explicitly. Use
results='asis', and separate tables with blank lines or an
HTML comment so Pandoc recognizes each block.
for (number_of_cylinders in sort(unique(mtcars$cyl))) {
subset_rows <- mtcars[
mtcars$cyl == number_of_cylinders,
c("mpg", "hp", "wt"),
drop = FALSE
]
subset_rows <- head(subset_rows, 4)
subset_rows <- data.frame(
Model = rownames(subset_rows),
subset_rows,
row.names = NULL,
check.names = FALSE
)
print(knitr::kable(
subset_rows,
format = "pipe",
digits = 2,
row.names = FALSE,
col.names = c("Model", "MPG", "Horsepower", "Weight"),
caption = paste("Sample cars with", number_of_cylinders, "cylinders")
))
cat("\n\n<!-- table separator -->\n\n")
}| Model | MPG | Horsepower | Weight |
|---|---|---|---|
| Datsun 710 | 22.8 | 93 | 2.32 |
| Merc 240D | 24.4 | 62 | 3.19 |
| Merc 230 | 22.8 | 95 | 3.15 |
| Fiat 128 | 32.4 | 66 | 2.20 |
| Model | MPG | Horsepower | Weight |
|---|---|---|---|
| Mazda RX4 | 21.0 | 110 | 2.62 |
| Mazda RX4 Wag | 21.0 | 110 | 2.88 |
| Hornet 4 Drive | 21.4 | 110 | 3.21 |
| Valiant | 18.1 | 105 | 3.46 |
| Model | MPG | Horsepower | Weight |
|---|---|---|---|
| Hornet Sportabout | 18.7 | 175 | 3.44 |
| Duster 360 | 14.3 | 245 | 3.57 |
| Merc 450SE | 16.4 | 180 | 4.07 |
| Merc 450SL | 17.3 | 180 | 3.73 |
For several tables supplied together,
knitr::kable(list(table1, table2)) is a compact option. If
each table needs different settings, first create the separate
kable() objects and combine them with
knitr::kables().
kable() is not enoughkable() is intended for simple rectangular tables.
Choose another tool when the presentation requires more:
Captions appear in a normal html_document, but automatic
numbering and robust cross-references require a supporting output
framework such as bookdown. Raw HTML table markup will not translate
directly to Word or PDF.
The YAML header of this guide defines min_mpg under
params. In R code it is available as
params$min_mpg, so one source can produce reports for
different thresholds, regions, dates, or clients.
selected_cars <- subset(cars_data, MPG >= params$min_mpg)
knitr::kable(
selected_cars,
digits = 2,
row.names = FALSE,
caption = paste0(
"Cars with fuel economy of at least ",
params$min_mpg,
" MPG"
)
)| Model | MPG | Cylinders | Horsepower | Weight |
|---|---|---|---|---|
| Mazda RX4 | 21.0 | 6 | 110 | 2.62 |
| Mazda RX4 Wag | 21.0 | 6 | 110 | 2.88 |
| Datsun 710 | 22.8 | 4 | 93 | 2.32 |
| Hornet 4 Drive | 21.4 | 6 | 110 | 3.21 |
| Merc 240D | 24.4 | 4 | 62 | 3.19 |
| Merc 230 | 22.8 | 4 | 95 | 3.15 |
| Fiat 128 | 32.4 | 4 | 66 | 2.20 |
| Honda Civic | 30.4 | 4 | 52 | 1.61 |
| Toyota Corolla | 33.9 | 4 | 65 | 1.83 |
| Toyota Corona | 21.5 | 4 | 97 | 2.46 |
| Fiat X1-9 | 27.3 | 4 | 66 | 1.94 |
| Porsche 914-2 | 26.0 | 4 | 91 | 2.14 |
| Lotus Europa | 30.4 | 4 | 113 | 1.51 |
| Volvo 142E | 21.4 | 4 | 109 | 2.78 |
Override the parameter while rendering:
rmarkdown::render(
"r_markdown_guide_en.Rmd",
output_file = "r_markdown_guide_en_mpg22.html",
params = list(min_mpg = 22),
envir = new.env(parent = globalenv())
)Using a new environment keeps objects created by the report out of
the Global Environment. For the strongest reproducibility check, render
from a fresh R session (for example, with Rscript).
One source can target several formats:
HTML needs Pandoc; PDF normally also needs a LaTeX distribution. Output-specific HTML or LaTeX should be used only when portability is not required.
For citations, add bibliography: references.bib to YAML
and cite an entry with [@citation-key]. Pandoc uses the
bibliography metadata while producing the final document.
warning=TRUE and diagnose it; hiding a
warning is not a fix.setwd().row.names=FALSE explicitly.col.names must have the same length as the displayed data
columns.print(kable(...)) with results='asis'.escape=TRUE unless the markup is trusted and intentional;
then use HTML format deliberately.Render in a clean R session before sharing. A document that succeeds only after manual console work is not yet reproducible.
.Rmd, data, and supporting files in a
project.sessionInfo() available when diagnosing
version-dependent behavior.## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
##
## time zone: America/Edmonton
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 xfun_0.60 cachem_1.1.0
## [6] knitr_1.51 htmltools_0.5.9 rmarkdown_2.31 lifecycle_1.0.5 cli_3.6.6
## [11] sass_0.4.10 jquerylib_0.1.4 compiler_4.6.1 tools_4.6.1 evaluate_1.0.5
## [16] bslib_0.12.0 yaml_2.3.12 rlang_1.3.0 jsonlite_2.0.0