R Markdown 入门:代码、文字与结果写进同一份文档
分析做完的最后一步是把它写出来。手写 Word 的麻烦在于数据和正文是两份东西:数据一改,表格要重贴、正文里的数字要重核,三个月后自己也说不清哪一版是最新的。R Markdown 把 R 代码、代码的运行结果和解释文字放进同一个文件,改完数据重跑一次,图、表、数字全部跟着更新。
和 Jupyter Notebook 的分工
Section titled “和 Jupyter Notebook 的分工”两者形式相近,都是「文字 + 代码 + 输出」的文档,区别在默认假设。Notebook 面向探索:单元格可以乱序执行,状态留在内存里,输出(含图片)写进 .ipynb,版本控制的 diff 里全是 base64 字符串。R Markdown 面向交付:源文件是纯文本,所有输出在渲染时现场生成,适合进 Git,也适合每周重跑一次的例行报告。用 Python 的话思路完全一样,可以对照 /python/basics/jupyter/ 看另一种节奏。
YAML 元数据
Section titled “YAML 元数据”Rmd 文件分两部分:开头被 --- 包住的是 YAML 元数据(YAML 是一种缩进敏感的配置格式),之后是正文。
---title: "ToothGrowth 剂量效应分析"author: "张三"date: "2026-01-06"output: html_document---title、author、date 是元数据,output 决定渲染成什么。三种最常用:html_document 输出单个自包含的 HTML 文件,直接发给别人就能打开;pdf_document 输出 PDF;word_document 输出 .docx,方便交给习惯用 Word 批注的导师。换格式只改这一行。
日期可以自动填:
date: "`r Sys.Date()`"引号不能省。knitr 在生成文档前会先解析 YAML 里的行内代码,所以这段会被替换成当天日期,避免每篇报告都写着去年的日期。
PDF 多一层依赖:pdf_document 走 LaTeX 路线,本机必须装 LaTeX 发行版。推荐轻量的 TinyTeX:
install.packages("tinytex")tinytex::install_tinytex()中文 PDF 还要两步。第一,把引擎换成 XeLaTeX——默认的 pdflatex 处理中文会直接报字体错误;第二,指定一个系统里确实装了的中文字体:
output: pdf_document: latex_engine: xelatexCJKmainfont: "Noto Serif CJK SC"latex_engine 是 pdf_document 的参数,必须缩进在它下面;CJKmainfont 是 Pandoc 层面的变量,写在与 output 同级的位置。字体名要用 fc-list :lang=zh 里能查到的准确名称,写错会得到 fontspec error: font not found。
代码块以三个反引号加 {r} 开头。选项写在花括号里,逗号分隔,这是 R Markdown 最需要记熟的一处语法。常用的几个:
echo = FALSE:执行代码但不显示代码,只留结果。给读者看结论时用。eval = FALSE:显示代码但不执行。讲一个耗时操作或需要交互的步骤时用。warning = FALSE与message = FALSE:分别屏蔽警告和包加载信息,让报告干净。注意它们只屏蔽显示,代码照常执行,结果照常输出。fig.width与fig.height:图形尺寸,单位英寸。fig.cap:图标题。给了它,PDF 里的图会自动编号,也才能在正文里引用。
echo 和 eval 看着对称,作用完全不同,写反了会得到一份只有代码没有结果的报告,或者一段没有代码的孤立数字。
每个块还可以起名字,写在花括号里,比如 {r summary-table}。块名不能重复:两个块叫同一个名字,渲染会直接失败并提示 duplicate chunk label,这是复制粘贴上一个块之后最常见的翻车点。
逐块写选项很啰嗦,可以在文档最前面放一个全局设置块,通常标成 {r setup, include=FALSE},它只做设置、不产生输出:
knitr::opts_chunk$set( echo = TRUE, warning = FALSE, message = FALSE, fig.width = 6, fig.height = 4)正文里的数字不要手打。写成 `r round(mean(ToothGrowth$len), 2)` 这样的行内代码,渲染后是 18.81,数据一更新它自己变,不会出现「正文写 18.8、表格里是 18.9」这种对不上的情况。样本量、点估计、p 值都该这么写。
下面这份文档统计 ToothGrowth 数据(维生素 C 对豚鼠齿生长的影响;supp 是给药方式,dose 是剂量,len 是齿长)在各组下的均值:
data(ToothGrowth)agg <- aggregate(len ~ supp + dose, data = ToothGrowth, FUN = mean)agg$len <- round(agg$len, 2)agg渲染出来是:
supp dose len1 OJ 0.5 13.232 VC 0.5 7.983 OJ 1.0 22.704 VC 1.0 16.775 OJ 2.0 26.066 VC 2.0 26.14读这张表可以下一个初步判断:低剂量下两种给药方式差距明显(13.23 对 7.98),剂量升到 2.0 毫克时几乎拉平(26.06 对 26.14)。可重复报告的价值就在这里——判断和产生它的代码在同一份文件里,谁都能重跑验证。
RStudio 里点 Knit 按钮即可,命令行等价写法:
$ Rscript -e 'rmarkdown::render("report.Rmd")'knitr::knit() 和 rmarkdown::render() 经常被混为一谈。前者只执行代码,产出一个中间 .md;后者在此之上再调用 Pandoc,生成 HTML、PDF 或 Word 成品。日常用 render(),需要检查中间产物时才用 knit()。批量渲染可以写循环:
files <- list.files("reports", pattern = "[.]Rmd$", full.names = TRUE)for (f in files) { rmarkdown::render(f)}几个真实的坑
Section titled “几个真实的坑”工作目录不是项目根目录。 渲染时 knitr 默认把工作目录设成 Rmd 文件所在的目录。如果 Rmd 放在 reports/ 子目录里,而代码里写 read.csv("data/raw.csv"),一定会报找不到文件。两个办法:路径相对 Rmd 文件写,或者在 setup 块里用 knitr::opts_knit$set(root.dir = ...) 显式指定项目根。
cache = TRUE 有代价。 开了缓存,块的结果会存下来复用,重跑时只执行改过的块,能省下大量时间。但它判断不出上游数据文件的变化——数据更新了,缓存里的还是旧结果。缓存目录还会留下成堆文件,记得加进 .gitignore。相比缓存,更稳的做法是把耗时的计算拆成单独的脚本,存成 .rds 中间结果。
中文乱码。 先确认源文件存成 UTF-8。Windows 下从别处拷来的文件可能是 GBK,渲染后中文变成问号,用 RStudio 的 File → Save with Encoding 转一次。
别在报告里 setwd()。 它在命令行下可能有效,换到 RStudio 的 Knit 又把目录改了,行为随环境漂移。路径问题用项目结构解决,不要用 setwd() 打补丁。
概念摸清之后,可以顺着看 /r/reporting/quarto/:Quarto 保留了这套思路的绝大部分,但语法更统一,交叉引用也是原生支持的。