11 trucos para mejorar tus documentos de rmarkdown
In English: 11 tricks to level up your rmarkdown documentsHace tiempo que quería escribir un post con una lista de algunos de los trucos de rmarkdown que fui aprendiendo a lo largo de los años. También quería oír consejos de otras personas, así que pregunté en Mastodon. En fin, acá les presento los 11 trucos que decidí incluir sin ningún orden definido.
Hacer obligatorias algunas opciones de bloque
A veces uso este truco para obligarme a escribir epígrafes en todas las figuras.
knit_plot <- knitr::knit_hooks$get("plot")
knitr::knit_hooks$set(plot = function(x, options) {
if (is.null(options$fig.cap)) {
stop("¡Escribí un epígrafe para todas las figuras!")
}
knit_plot(x, options)
})
Este “hook” corre una vez por cada bloque que produce un gráfico y salta con un error si la opción fig.cap
es NULL
(o sea, que no existe).
Si esto no te obliga a remangarte y escribir epígrafes en todas tus figuras, nada lo hará.
(Gracias a Zhian N Kamvar por encontrar un problema en la versión anterior de este código).
También inspirado por Zhian N Kamvar, ahora uso esto para obligarme a ponerle nombre a todos mis bloques:
knitr::opts_hooks$set(label = function(options) {
# Fijarse si la etiqueta viene de la etiqueta por defecto
default_label <- knitr::opts_knit$get("unnamed.chunk.label")
has_default_label <- grepl(default_label, options$label)
if (has_default_label) {
stop("¡Ponele nombre a los bloques!")
}
return(options)
})
Este código no puede fijarse si is.NULL(options$label)
porque los bloques sin nombre son nombrados con una etiqueta por defecto.
Entonces se fija cuál es esta etiqueta por defecto con knitr::opts_knit$get("unnamed.chunk.label")
y luego se fija si el nombre del bloque fue generado automáticamente.
Esto no sirve en el caso totalmente ridículo de un bloque con un nombre que, de casualidad, es igual que la etiqueta por defecto.
Estos principios se pueden extender a cualquier otra opción. El único problema es que el error salta a penas knitr encuentra un caso problemático. Sería mejor registrar todos esos casos y luego tirar el error al final.
Epígrafes usando referencias textuales
Un buen epígrafe suele ser largo e incluso tener cosas complicadas como código de \(\LaTeX\) o referencias a figuras anteriores (tipo “Igual que la Figura 2 pero para payasos con narices rojas”). Estos epígrafes son difíciles de entender cuando están apretados en las opciones del bloque y necesitan muchos caracteres de escape. La solución de Bookdown es usar referencias textuales (text references).
Si una línea de texto empieza con (ref:etiqueta)
, luego (ref:etiqueta)
va a ser reemplazado por ese texto en otro lado.
Entonces se puede escribir
(ref:nariz-roja-cap) Igual que la Figura \@ref(fig:nariz-azul) pero para payasos con narices *rojas*. Y acá tenés una fórmula matemática para mostrar que funciona: $\pi=3$.
Y luego escribir el bloque de código así:
```{r, fig.cap = "(ref:nariz-roja-cap)"}
plot(nariz_roja)
```
Epígrafe por defecto
Al usar referencias textuales, une termina teniendo que poner fig.cap = "(ref:etiqueta")
en todos los bloques, y es bastante molesto.
En vez de eso, lo que suelo hacer es hacer que la opción fig.cap
tenga un valor por defecto de la forma (ref:etiqueta-cap)
:
knitr::opts_hooks$set(label = function(options) {
if (is.null(options$fig.cap)) {
options$fig.cap <- paste0("(ref:", options$label, "-cap)")
}
})
Así, todos los bloques tienen un epígrafe asociada a una referencia con un nombre predecible.
Guardar figuras en múltiples formatos
¿Sabías que la opción dev
puede ser un vector?
Esto permite guardar figuras en más de un formato al mismo tiempo.
knitr::opts_chunk$set(dev = c('png', 'svg'))
Esta funcionalidad (sugerida por Robert Flight) es simple, posiblemente desconocida pero también muy útil, ya que te permite usar formatos vectoriales en el documento pero tener versiones raster para compartirlas más fácil.
Terminar de knitear antes del final
A medida que un documento se hace más largo y complicado, crece la certeza de que van a aparecer errores estrafalarios y difíciles de debugear.
También puede que quieras pulir alguna parte inicial del documento aún cuando hay código roto más adelantes.
Mickaël CANOUIL y superboreen comentaron que se puede usar knitr::knit_exit()
para que knitr deje de renderizar un documento “antes de que llegue al horripilante código que todavía no arreglé”.
knitr::knit_exit()
Obtener el formato de salida
Si bien la promesa de rmardown es que el mismo código se puede renderizar a cualquier formato de salida, pero las abstracciones son coladores y esta promesa no siempre se cumple. Por ejemplo, no encontré ningún paquete para generar tablas que genere buenas tablas de LaTeX, HTML y Word sin tener que cambiar el código. En cualquier caso, a veces el código tiene que saber el formato de salida del documento.
La función knitr::pandoc_to()
de vuelve el “destino final” del documento, que puede ser “latex”, “html” or “docx”.
knitr::pandoc_to()
También puede devolver un valor lógico indicando si el formato de salida es alguno de los especificados como argumento. Esto permite crear código que corre sólo en algunos formatos:
if (knitr::pandoc_to("docx")) {
# Hacer algo sólo si el formato es docx.
}
Ojo que knitr::pandoc_to()
devuelve NULL
cuando se lo corre en una sesión de R interactiva, así que deberías considerar ese caso.
Otras funciones relacionadas son is_latex_output()
y is_html_output()
.
Configurar la ubicación de la caché
Mis documentos muchas veces tienen código que tarda bastante en correr, así que uso mucho la caché y a veces necesito controlar dónde se guarda.
Esto se puede lograr con knitr::opts_chunk$set(cache.path = direccion)
.
Esta es una posible solución cuando une renderiza más de un formato. Me di cuenta que cambiar el formato invalida la caché, por lo que termina no sirviendo. Entonces lo que hago es configurar una ubicación para cada formato:
formato <- knitr::pandoc_to()
knitr::opts_chunk$set(
cache.path = file.path("cache", formato, "") # El último "" es necesario
)
Obtener el archivo actual
La función knitr::current_input()
devuelve el archivo de entrada que está siendo renderizado.
Hay un montón de casos donde esto puede servir pero yo lo uso, nuevamente, para controlar la ubicación de la caché y las figuras.
En un documento de bookdown, me gusta que cada capítulo tenga su propia carpeta con caché y figuras, así que pongo esto en mi bloque de setup:
formato <- knitr::pandoc_to()
capitulo <- tools::file_path_sans_ext(knitr::current_input())
knitr::opts_chunk$set(
fig.path = file.path("figures", capitulo, ""),
cache.path = file.path("cache", capitulo, formato, "")
)
Invalidar la caché rápidamente
Siguiendo con el tema de caché, a veces es necesario renderizar todo el documento de cero sin usarla. Ya sea para una última corrida que se asegure de que todo el código anda bien o para resolver problemas que une sospecha que pueden estar relacionados con la caché.
Casi siempre uso la opción cache.extra
, la cual invalida la caché cada vez que cambia.
knitr::opts_chunk$set(cache.extra = 42) # Cambiá el nombre para invalidar la caché
Ejecutar código luego de knitear
Knitr ejecuta un hook llamado “document” al terminar de knitear. Este hook se puede personalizar para hacer lo que une queria:
# Primero guardar el hook por defecto
knit_doc <- knitr::knit_hooks$get("document")
knitr::knit_hooks$set(document = function(x) {
# Hacer cualquier cosa
knit_doc(x) # Luego hacer lo qeu knitr iba a hacer
})
Por ejemplo, a veces agrego esto para obtener una notidifación de escritorio cuando mi computadora termina de knitear:
# Esto necesita la utilidad notify-send.
# https://manpages.ubuntu.com/manpages/bionic/man1/notify-send.1.html
notify <- function(title = "title", text = NULL, time = 2) {
time <- time*1000
system(paste0('notify-send "', title, '" "', text, '" -t ', time, ' -a rstudio'))
}
start_time <- unclass(Sys.time())
min_time <- 5*3600 # Sólo notificar el código que toma más de 5 minutos
knit_doc <- knitr::knit_hooks$get("document")
knitr::knit_hooks$set(document = function(x) {
took <- unclass(Sys.time()) - start_time
if (took >= min_time) {
notify("¡Terminé de knitear!",
paste0("Tardó ", round(took), " segundos"),
time = 5)
}
knit_doc(x)
})
Esto mismo se puede usar para enviar notificaciones en el teléfono con RPushbullet o mandar mails con emayili. También está bueno el paquete notifier.
Convertir scripts a y desde RMarkdown
Por último, Katie resaltó la función knitr::spin()
, la cual convierte un script de R con un formato especial a un archivo de RMarkdown.
El flujo opuesto se consigue con la función knitr::purl()
, usada por Ken Butler.