Capítulo 1. Entorno de trabajo y flujo reproducible

▶ Ejecutar este capítulo en Binder

La primera vez que se abre, Binder construye el entorno en la nube (unos 10-20 min); verás una pantalla de progreso. Después queda en caché y abre en segundos. Si parece que no responde, espera a que termine de construirse o vuelve a intentarlo.

Antes de la primera línea de R hay una decisión que condiciona todas las demás: dónde y cómo va a vivir tu trabajo. La tradición docente empieza por la sintaxis y deja «lo del entorno» para un apéndice; aquí invertimos el orden a propósito. Quien analiza datos no entrega un programa que corre una vez y se olvida: entrega una afirmación —«la energía media del rock es 0,85»— que alguien, en otra máquina y en otro mes, tiene que poder poner a prueba. Eso exige que entrada, código y entorno queden atados desde el principio, y atarlos a posteriori es siempre más caro. De ahí que este capítulo, lejos de ser un trámite de instalación, sea el cimiento metodológico del libro entero: lo que aquí se monta sostiene el registro de cifras (cap. 10), la estadística honesta (cap. 11) y la ingeniería del proyecto final (cap. 16).

Qué significa «programar para datos»

Un guion de análisis no se juzga por si funciona, sino por si sus conclusiones resisten que otro las compruebe. Imprimir un acierto de \(0{,}92\) es fácil; lo difícil —y lo valioso— es que ese \(0{,}92\) vuelva a salir cuando el revisor, el compañero de equipo o tu propio yo de dentro de seis meses relance el análisis. Sin esa propiedad, la cifra es una anécdota con decimales. Con ella, es un resultado defendible. Programar para datos es, ante todo, fabricar resultados de la segunda clase.

La crisis de reproducibilidad

No hablamos de un riesgo hipotético. Cuando la revista Nature preguntó a más de mil quinientos investigadores por su experiencia, más del 70 % admitió haber fracasado al intentar reproducir experimentos ajenos —y más de la mitad, al intentar reproducir los propios (Baker 2016)—. Lo llamativo es que el problema alcanza de lleno al cálculo con ordenador, el territorio donde repetir debería ser trivial: Peng (2011) propuso hace más de una década que la reproducibilidad computacional fuera el listón mínimo verificable de una publicación, y el listón sigue sin superarse de forma general. ¿Por qué falla algo que «debería» ser determinista? Casi nunca por mala fe. Falla porque el contexto se evapora: aquel paquete que se actualizó y cambió un valor por defecto, aquella celda que se ejecutó dos veces, aquel CSV retocado a mano una tarde de prisa que nadie apuntó.

Los cuadernos de análisis dan la medida del desastre. Pimentel et al. (2019) descargaron más de un millón de notebooks de repositorios públicos y los relanzaron, tal cual, en un entorno limpio: la inmensa mayoría ni siquiera terminó sin errores, y entre los que terminaron, solo una minoría exigua devolvió los resultados publicados. Moraleja doble. Primera: colgar el código en un repositorio no convierte un análisis en reproducible. Segunda: la reproducibilidad no se añade al final; se practica desde el primer día. Aquel corpus era de cuadernos de Python, pero el mecanismo del fallo —estado oculto, entorno sin fijar— no entiende de lenguajes: un cuaderno de R descuidado se rompe exactamente igual.

Merece la pena ponerles nombre a los culpables, porque se repiten con una regularidad casi cómica. Son cinco. Las versiones a la deriva: el paquete de hoy no se comporta como el de hace un año, y el mismo guion da otro número. La manipulación fantasma: un retoque manual —en una hoja de cálculo, en la consola— que no dejó rastro en ningún fichero. El azar sin semilla: cada ejecución sortea de nuevo y las cifras bailan. Los crudos corrompidos: alguien editó el fichero original y el punto de partida ya no existe. Y el estado oculto: lo que muestra la pantalla lo produjo una versión del código que ya no está en el fichero. Ninguno exige talento para evitarse; exigen disciplina, y cada uno tiene su antídoto en este capítulo (tabla 1.1).

Los cinco modos de fallo de la reproducibilidad. Cada uno se desactiva con uno de los cuatro pilares (§1.1.2); todos reaparecen resueltos a lo largo del libro.
Modo de fallo Síntoma Antídoto
Versiones sin fijar el resultado cambia sin tocar el código lockfile (renv)
Pasos manuales un arreglo «a mano» sin registrar todo en código
Semilla no fijada números distintos cada vez set.seed
Crudos modificados no hay vuelta al punto de partida crudos inmutables
Estado oculto la pantalla no refleja el código sesión limpia

Los cuatro pilares

¿Qué hay que clavar para que un resultado se sostenga? Cuatro cosas, ni una menos, y la figura 1.1 las reúne:

  • El código: bajo control de versiones (§1.7), para poder señalar las instrucciones exactas que produjeron cada cifra.

  • Los datos: con origen documentado y, siempre que se pueda, una copia congelada o un identificador de versión que diga «estos y no otros».

  • El entorno: la versión de R y la de cada paquete, anotadas (§1.4); entre dplyr 1.0 y dplyr 1.2 hay comportamientos que no coinciden.

  • La aleatoriedad: domesticada con una semilla declarada (§1.10), para que particiones y sorteos caigan siempre igual.

Figura 1.1. Los cuatro pilares de la reproducibilidad. Código versionado, datos con procedencia, entorno fijado y semillas declaradas: los cuatro convergen en un resultado que se puede defender. Basta que uno falle para que el número quede en el aire. Este capítulo dedica una sección a cada pilar; el resto del libro los da por sentados.

Nada de esto es una aspiración etérea: hay recetarios publicados que convierten los pilares en gestos verificables. El decálogo de Sandve et al. (2013) (§1.1.6) se puede aplicar tal cual, punto por punto; Wilson et al. (2017) rebajan la exigencia a un conjunto de «prácticas suficientemente buenas» pensadas para científicos sin formación de ingeniería; y The Turing Way (The Turing Way Community 2022), en constante revisión comunitaria, funciona hoy como el manual de referencia de todo este terreno. Aun así, ninguna lista sustituye al hábito, y el hábito arranca con dos prohibiciones caseras: no teclear jamás un número que el código pueda calcular, y no ejecutar nada que no sepas relanzar idéntico mañana.

Reproducible, replicable, robusto

Tres palabras se usan como sinónimos en conversación y no lo son; ordenarlas ahorra discusiones enteras. Reproducible: mismos datos, mismo código, mismo número —la exigencia más baja de las tres y, a la vez, la única que se puede verificar mecánicamente; es la meta de este capítulo—. Replicable: un equipo distinto recoge datos nuevos, monta su propio análisis y desemboca en la misma conclusión; aquí ya no basta con que el código esté bien, tiene que ser verdad el fenómeno. Robusto: la conclusión no se cae al cambiar de método. Nótese la asimetría: reproducir no certifica nada —se puede estar equivocado con precisión perfecta—, pero sin reproducibilidad ni siquiera existe el objeto sobre el que discutir replicación o robustez. En eso consiste el «estándar mínimo» de Peng (2011): no es la meta, es el requisito de entrada. Las herramientas estadísticas para juzgar la robustez llegan en el capítulo 11.

Un ejemplo fija los tres niveles con el caso de este libro. Afirmas que, en el catálogo de música, las pistas más enérgicas suenan más fuerte. El resultado es reproducible si cualquiera, con tu CSV y tu guion, obtiene la misma correlación hasta el último decimal. Es replicable si otro equipo, descargando otro catálogo —otro año, otra plataforma— y escribiendo su propio análisis, concluye también que energía y sonoridad van de la mano. Y es robusto si la conclusión aguanta cambios de método: correlación de Spearman en vez de Pearson, quitar el 1 % de pistas extremas, controlar por género. Cada nivel protege de un fallo distinto: el primero, de no poder ni comprobar; el segundo, de que tu muestra fuera peculiar; el tercero, de que la conclusión fuera un artefacto de la técnica elegida.

Datos que se puedan encontrar y reutilizar: los principios FAIR

Cuidar el código es media batalla; la otra media son los datos, y ahí el marco de referencia son los principios FAIR (Wilkinson et al. 2016). El acrónimo inglés (Findable, Accessible, Interoperable, Reusable) pide cuatro propiedades a un conjunto de datos: que se pueda encontrar mediante un identificador persistente, que se pueda obtener por un protocolo claro, que viva en formatos y vocabularios que cualquier herramienta entienda, y que declare licencia y procedencia para que otro sepa a qué atenerse. Traducido al día a día de este libro: fuente canónica citada, formatos abiertos (cap. 5) y fecha y versión de descarga apuntadas, de modo que «los datos» sean siempre unos datos concretos. De poco sirve un análisis impecablemente reproducible si el fichero sobre el que corría ya no aparece.

Conviene desglosar las cuatro letras, porque cada una se traduce en un gesto concreto. Localizable significa que el dato tiene un identificador estable —un DOI, una URL canónica— y no un «me lo pasó un compañero». Accesible, que se puede recuperar con un protocolo claro y abierto, no tras un formulario que un día desaparece. Interoperable, que vive en un formato estándar (CSV, Parquet, JSON) legible por cualquier herramienta, no en el binario propietario de un programa concreto. Y reutilizable, que lleva su licencia y su procedencia, de modo que quien lo encuentre sepa qué puede hacer con él. FAIR no es un ideal abstracto: es la diferencia entre un dato que sobrevive a su autor y uno que muere con el portátil donde se guardó.

Las tres clases de dato (anticipo)

Hay una disciplina que el capítulo 10 formalizará y que conviene saborear desde ya: cada número de un informe debe declarar su estirpe, y las estirpes posibles son tres. O el número lo midió tu código (y entonces es reproducible), o lo fabricaste tú a propósito como ilustración (sintético, y se dice), o lo tomaste de otro sitio (citado, con su referencia). El pecado capital del oficio es cruzar las etiquetas: colar una cita como si fuera una medición propia, o vender como hecho general lo que es una peculiaridad de tu muestra. Y el papel de la reproducibilidad en este esquema es concreto: es lo que te permite demostrar que un número pertenece a la primera estirpe.

Conviene fijar ya un matiz sobre la primera clase. Dentro de lo medido hay dos linajes: las cifras deterministas —una media, una correlación—, que cualquiera reproduce sobre los mismos datos con cualquier herramienta, y las que nacen de un sorteo —una muestra, una partición, un intervalo por bootstrap—, que solo se reproducen si además se declara el generador y su semilla (§1.10). En este libro las primeras acompañan al catálogo de música allá donde vaya, y las segundas llevan siempre su receta al lado: semilla, generador y código.

Diez reglas para no engañarse

El decálogo de Sandve et al. (2013) baja los pilares al suelo: diez reglas concretas, de la más elemental a la más ambiciosa, que funcionan como auditoría exprés de cualquier análisis:

  1. Deja rastro de cada resultado. Que cada número y cada figura tengan localizable el guion que los produjo.

  2. Destierra la manipulación a mano. Si un dato se tocó en una hoja de cálculo, ese paso no existe para nadie más; conviértelo en código.

  3. Congela las versiones del software del que dependes —el pilar del entorno (§1.4)—.

  4. Todo tu código, bajo control de versiones (§1.7); sin excepciones «porque era un script pequeño».

  5. Materializa los pasos intermedios en formatos abiertos: poder retomar el análisis a mitad vale horas.

  6. Declara la semilla de todo lo que sortee algo (§1.10).

  7. Guarda el dato que hay detrás de cada gráfica, no solo el PNG resultante.

  8. Ofrece niveles de detalle: del resumen al desglose, que se pueda bajar la escalera.

  9. Ata cada afirmación a su evidencia —«medir, no proclamar», el lema del capítulo 10—.

  10. Publica guiones, ejecuciones y resultados cuando el contexto lo permita.

Vistas de una en una, ninguna intimida; el reto es no saltarse ninguna nunca. El libro que tienes delante juega con esas reglas: el código de cada capítulo vive en src/, las cifras del texto salen de ahí, y las figuras se regeneran con un comando.

El intérprete de R y su ecosistema

Un lenguaje interpretado

En R no hay un paso de compilación que produzca un ejecutable: escribes una expresión y un programa —el intérprete— la lee, la analiza y la evalúa ahí mismo, una tras otra. Esa inmediatez es la que hace posible la conversación con los datos que define al lenguaje, y tiene su precio: cada operación interpretada cuesta, y por eso un bucle sobre millones de números pierde por goleada contra la operación vectorizada equivalente, que despacha el trabajo en C o Fortran compilado (cap. 7; la Parte III explota esta idea a fondo). La implementación que usa prácticamente todo el mundo es GNU R, a cargo del R Core Team. Y una costumbre de primer día: pregunta a tu sesión quién es, porque entre versiones cambian comportamientos:

R.version.string            # "R version 4.6.1 (2026-06-24)"
R.version$platform          # "x86_64-pc-linux-gnu"
RNGkind()[1]                # "Mersenne-Twister"  (el generador por defecto)

Del código fuente a la evaluación

Una mirada rápida bajo el capó rinde durante todo el libro. Al evaluar una expresión, R hace dos cosas: la analiza (parse), construyendo un árbol de expresiones que —detalle muy de R— el propio lenguaje puede examinar y reescribir (lo explotaremos en el cap. 6), y después la evalúa. Entre medias hay una optimización silenciosa: desde R 3.4, las funciones se compilan a bytecode sobre la marcha (compilación JIT), y los paquetes base llegan ya compilados; ejecutar ese bytecode es sensiblemente más rápido que caminar el árbol. La figura 1.2 dibuja el camino.

Figura 1.2. Modelo de evaluación de R. El código se analiza en un árbol de expresiones —que el propio lenguaje puede inspeccionar (cap. 6)—, se compila a bytecode y se evalúa. El coste por operación de este ciclo es la razón de fondo para preferir las operaciones vectorizadas al bucle (cap. 7).

Dos consecuencias prácticas se desprenden de este diseño. La primera, el coste por operación ya comentado, que la vectorización esquiva empujando los bucles hacia código compilado. Dos, la portabilidad: al no haber ejecutables nativos, el mismo .R corre sin cambios en Linux, macOS y Windows mientras los paquetes existan para la plataforma —una comodidad enorme cuando el análisis cambia de manos o de máquina—.

Por qué R en ciencia de datos

A diferencia de casi todos los lenguajes de programación, R nació para el análisis de datos: es la reencarnación libre del lenguaje S, creado por John Chambers en los Laboratorios Bell para «convertir ideas en software, con rapidez y fidelidad» (Chambers 2008). Esa herencia se nota en todo: el vector es el tipo básico (no hay escalares, cap. 2), los valores ausentes son ciudadanos de primera clase, la interfaz de fórmula (y ~ x) y la gramática de gráficos (cap. 12) son nativas, y los contrastes estadísticos vienen en la instalación base (cap. 11). La capa numérica y la estadística no se añaden: vienen de fábrica. Su ecosistema —CRAN, el archivo con más de veinte mil paquetes, y el tidyverse, la colección coherente de Hadley Wickham y colaboradores (Wickham et al. 2019)— crece sobre esa base común. Aprender R no es aprender paquetes sueltos, sino un lenguaje diseñado, desde el primer día, para pensar con datos.

Breve historia del R científico

La forma actual del ecosistema se entiende mejor con su biografía delante. El antepasado es S, el lenguaje de análisis estadístico interactivo que John Chambers y su grupo idearon en Bell Labs en los setenta. Dos décadas después, en la Universidad de Auckland, Ross Ihaka y Robert Gentleman construyeron desde cero una implementación libre que cruzaba las ideas de S con el lenguaje funcional Scheme; el artículo fundacional es de 1996 (Ihaka y Gentleman 1996), y el nombre —R— juega a la vez con sus iniciales y con la letra anterior. El andamiaje institucional llegó enseguida (1997: R Core Team y CRAN, el Comprehensive R Archive Network; 2000: R 1.0.0), y sobre él quedó empaquetada la estadística de medio siglo. El último gran giro es de 2016: el tidyverse (Wickham et al. 2019) unificó manipulación (dplyr, cap. 8), gráficos (ggplot2, cap. 12) y modelado (tidymodels, cap. 14) bajo una gramática común cuyo pegamento es el data frame: todas las piezas hablan la misma estructura de datos, y por eso encajan.

Dos episodios de esa historia explican rasgos que aún se notan. El primero: durante los noventa el S «oficial» era un producto comercial (S-PLUS), y R, su reimplementación libre, empezó como proyecto académico marginal; que hoy R sea el estándar y S-PLUS una pieza de museo es uno de los primeros triunfos claros del software libre en la ciencia, y la razón de que casi toda la estadística académica publique sus métodos como paquetes de CRAN. El segundo: en 2001 nació Bioconductor, el repositorio hermano especializado en bioinformática, con una política pionera de revisión y de integración continua que anticipó lo que hoy llamamos ingeniería de software científico. La tabla 1.2 ordena los hitos.

Hitos del ecosistema R. Fechas redondeadas al año del anuncio o de la primera versión estable.
Año Hito Qué aportó
1976 S (Bell Labs) el lenguaje estadístico interactivo original
1993 R (Auckland) reimplementación libre de S, anunciada en listas
1995 licencia GPL R se vuelve software libre formalmente
1997 CRAN + R Core archivo central de paquetes y equipo estable
2000 R 1.0.0 primera versión considerada estable
2001 Bioconductor repositorio bioinformático con revisión e IC
2011 RStudio IDE el entorno que popularizó R fuera de la academia
2012 knitr, Shiny informes reproducibles y aplicaciones web en R
2016 tidyverse gramática coherente de manipulación y gráficos
2020 R 4.0 stringsAsFactors=FALSE: rompe con un legado
2022 Posit, Quarto la empresa se reorienta y el cuaderno se hace políglota
2023 S7 sistema de objetos que unifica S3 y S4 (cap. 6)

Un último rasgo del ecosistema no es técnico sino humano: la comunidad. CRAN mantiene Task Views, páginas curadas que catalogan los paquetes de un dominio (series temporales, ecología, farmacocinética) y ahorran semanas de búsqueda; rOpenSci revisa por pares paquetes de ciencia abierta; R-Ladies y las conferencias useR! y posit::conf sostienen una red de usuarios inusualmente dispuesta a ayudar. Para quien empieza, esto tiene una consecuencia práctica: casi cualquier problema estadístico que encuentres ya tiene un paquete escrito por la persona que publicó el método —una inmediatez entre literatura y software que pocas comunidades igualan—.

Paquetes: instalar, cargar y el operador ::

El valor de R está en sus paquetes, y conviene distinguir dos gestos que los principiantes confunden. install.packages("dplyr") instala el paquete en el disco: se hace una vez (o se delega en renv). library(dplyr) lo carga en la sesión actual: se hace en cada guion que lo use, y pone sus funciones a mano. Instalar es comprar la herramienta; cargar es sacarla del cajón.

install.packages("dplyr")   # una vez: descarga e instala en el disco
library(dplyr)              # en cada sesion: carga sus funciones
dplyr::filter               # :: usa una funcion SIN cargar el paquete

El operador :: merece un apunte: paquete::funcion llama a una función indicando explícitamente de qué paquete es, sin necesidad de library(). Tiene dos virtudes: evita la ambigüedad cuando dos paquetes definen una función con el mismo nombre (dplyr::filter frente a stats::filter), y documenta en el propio código de dónde sale cada cosa. En guiones de producción, ser explícito con :: en las llamadas menos habituales es una cortesía con quien lea el código después.

Versiones de R y su cadencia

El calendario de R es previsible: una versión mayor cada primavera y parches de mantenimiento entre medias. En la tabla 1.3 están las que importan a mediados de 2026, y la elección no es cosmética: de la versión depende qué sintaxis tienes disponible.

Versiones de R relevantes en 2026. Novedades destacadas de cada versión para ciencia de datos. Las fechas son de publicación de la versión estable.
Versión Año Novedad destacada
4.0 2020 stringsAsFactors=FALSE por defecto; cadenas crudas r"()"
4.1 2021 tubería nativa |>; función anónima corta \(x)
4.2 2022 |> admite marcador de posición; mejoras de rendimiento
4.3 2023 \(x) consolidada; mensajes de error más claros
4.4 2024 operador %||% en base; ALTREP más extendido
4.5 2025 conjunto penguins en la base; mejoras de rendimiento
4.6 2026 maduración del compilador y del sistema de objetos S7

Las dos incorporaciones que más cambian el estilo cotidiano son de R 4.1: la tubería nativa |>, que encadena operaciones de izquierda a derecha —datos |> filter(...) |> summarise(...)— y sustituye al anidamiento de paréntesis, y la función anónima corta \(x) x + 1, más ligera que function(x) x + 1. Ambas atraviesan todo el libro.

La consola y el primer programa

Teclear R a secas en un terminal arranca la consola: un ciclo de lectura–evaluación–impresión (REPL) en el que cada expresión que escribes recibe respuesta inmediata. Como banco de pruebas para ideas sueltas no tiene rival. Repara en el [1] con que empiezan las salidas: no es adorno, sino el índice del primer elemento —la primera pista de que en R todo es un vector (cap. 2)—.

> 2 + 2
[1] 4
> strrep("dato", 3)
[1] "datodatodato"
> sqrt(2)
[1] 1.414214

Ahora bien, la consola es un laboratorio, no un archivo: al cerrar la sesión, lo tecleado se esfuma. El trabajo que quieras conservar pertenece a ficheros .R (guiones) o a documentos Quarto (§1.5). Este es hola.R, el primero, que se lanza con Rscript hola.R:

# hola.R -- el primer programa, con una intencion minima de utilidad.

#' Devuelve un saludo. Separar el "que" del "como" desde el principio.
#' @param nombre cadena con el nombre a saludar.
#' @return una cadena de saludo.
saludar <- function(nombre) {
  sprintf("Hola, %s. Empezamos a programar para datos.", nombre)
}

# solo al ejecutar el fichero con Rscript, no al cargarlo con source():
if (sys.nframe() == 0L) {
  cat(saludar("cientifica"), "\n")
}

Detente en la guarda if (sys.nframe() == 0L), porque encierra una idea de diseño. Un mismo .R juega dos papeles: programa, cuando lo lanza Rscript, y biblioteca, cuando otro fichero lo incorpora con source(). ¿Cómo distinguirlos? sys.nframe() mide la profundidad de la pila de llamadas: cero si el fichero corre directamente, positiva si alguien lo está cargando. El bloque bajo la guarda, por tanto, solo se ejecuta en el primer caso; en el segundo, el fichero se limita a ofrecer sus funciones sin disparar nada. Esa separación entre lo que un fichero hace y lo que pone a disposición de otros es la semilla del código reutilizable, y la Parte II la desarrollará sin descanso. Los comentarios #’ que ves sobre la función son roxygen2 (§1.11.2): documentación de la que se genera ayuda automáticamente.

La línea de comandos para datos

Hay una herramienta que casi ningún plan de estudios enseña ya y que este libro se niega a dar por sabida: la línea de comandos (la shell). Puede parecer una reliquia; para quien trabaja con datos es lo contrario. Te llega un fichero de diez gigabytes y quieres saber cuántas filas trae, cómo se llaman sus columnas o cuántas líneas mencionan cierto género musical: la shell contesta en segundos, leyendo en flujo, cuando abrir ese fichero en un editor —o cargarlo entero en R— sería lento o directamente inviable.

Tuberías: componer herramientas pequeñas

El golpe de genio de Unix no está en ningún comando concreto, sino en cómo se combinan: cada programa hace una única cosa, y la tubería (pipe, el símbolo |) empalma la salida de uno con la entrada del siguiente. Pregunta: ¿cuántos géneros distintos hay en la tercera columna del catálogo, y cuál domina? Respuesta, sin escribir un programa:

# extrae la columna 3, ordena, cuenta duplicados y ordena por frecuencia
cut -d, -f3 musica.csv | sort | uniq -c | sort -rn | head

Ninguno de los cuatro eslabones sabe nada de los otros, y sin embargo juntos responden una pregunta analítica completa. Guárdate la imagen, porque R la importó tal cual: su tubería nativa |> (§1.2.6) copia símbolo e idea, y ese encadenar pasos de izquierda a derecha reaparecerá como columna vertebral de dplyr (cap. 8) y del estilo funcional (cap. 3). Falta un detalle: la redirección, donde > vuelca la salida a un fichero y >> la añade al final del existente.

grep "reggaeton" musica.csv > reggaeton.csv   # filtra un genero a un fichero

Dos piezas más completan el vocabulario de composición. La primera, el código de salida: todo comando termina con un número (0 = bien, distinto de 0 = error) que los operadores && y || consultan; a && b ejecuta b solo si a salió bien, y a || b solo si falló. Es el mecanismo con el que un flujo se detiene ante el primer error en vez de seguir procesando datos a medias —la misma disciplina que stop() dentro de R (§1.6)—. La segunda, tee, que duplica la corriente: deja pasar la salida hacia el siguiente eslabón y la guarda en un fichero, útil para conservar un registro de lo que atravesó la tubería.

Rscript generar.R && Rscript resumir.R   # resumir SOLO si generar salio bien
cut -d, -f3 ventas.csv | sort -u | tee categorias.txt | wc -l
#                                  ^ guarda la lista Y sigue contando

Variables, bucles y automatización

Sería un error quedarse en los comandos sueltos: la shell es un lenguaje de programación completo, con variables (f=ventas.csv), bucles (for f in *.csv; do ...; done) y ficheros .sh donde una secuencia de comandos queda lista para relanzarse cuantas veces haga falta. Ahí está su vocación de automatizadora: el barrido en lote de una carpeta de ficheros, la cadena nocturna de descargas, la secuencia de pasos que nadie quiere teclear dos veces. Por ejemplo, aplicar el mismo comando a todos los CSV de una carpeta sin repetirse:

for f in datos/crudos/*.csv; do
  echo "$f: $(wc -l < "$f") filas"   # filas de cada CSV de la carpeta
done

Completan el lenguaje los condicionales y la búsqueda recursiva. if comprueba —típicamente con [ -f fichero ], «existe el fichero»— antes de actuar, lo que vuelve un guion idempotente: se puede relanzar sin miedo porque no repite lo ya hecho. Y find recorre un árbol de carpetas entero aplicando un criterio (nombre, tamaño, fecha), donde el comodín *.csv solo mira la carpeta actual:

if [ ! -f data/crudos/musica.csv ]; then      # descarga SOLO si falta
  curl -L -o data/crudos/musica.csv "$URL_DATOS"
fi
find data/ -name "*.csv" -size +100M          # CSV grandes, a cualquier nivel

Cuando estos automatismos sueltos empiezan a multiplicarse, llega el momento de consolidarlos en un gestor de flujos como targets (§1.9), que orquesta el análisis entero con un comando. ¿Y dónde acaba la shell y empieza R? No hay muro, hay reparto de papeles: la shell conecta programas y automatiza; R razona sobre los datos. Reconocer ese reparto —y no empeñarse en hacer agregaciones con awk ni en mover ficheros desde R— es una de esas señales pequeñas que delatan experiencia.

NotaAvanzado

Un reparto de papeles orientativo, para quien dude en la frontera: mirar, contar y filtrar deprisa un fichero, o encadenar las etapas de un análisis, es terreno de la línea de comandos; en cuanto entran tipos, uniones entre tablas o agregaciones con lógica, el trabajo pertenece a R y sus paquetes. Con el tiempo, cambiar de un mundo a otro se vuelve tan natural como cambiar de ventana.

Entornos aislados y gestión de dependencias

Este es el punto en que el trabajo casero se vuelve trabajo serio. Quien empieza teclea install.packages("dplyr") sin más y deja los paquetes «en el sistema»; el apaño dura hasta que un proyecto pide dplyr 1.0 y otro reclama dplyr 1.2, porque una biblioteca global única no puede albergar dos versiones del mismo paquete a la vez, y algo acaba rompiéndose. Ese atolladero se conoce como «infierno de dependencias», y su salida es de sobra conocida: una biblioteca por proyecto, una carpeta que reúne los paquetes de un proyecto —con sus versiones exactas— al margen de las demás.

El aislamiento en la práctica

La figura 1.3 pone el problema delante de los ojos. A la izquierda, el mundo sin aislamiento: una sola biblioteca global (.libPaths()) que todos los proyectos comparten, donde actualizar un paquete para el proyecto de hoy sabotea el de la semana pasada. A la derecha, cada proyecto con su carpeta de paquetes propia: dplyr 1.0 aquí, dplyr 1.2 al lado, y ninguno se entera del otro. Piénsalo como pasar de un armario común donde todos cuelgan su ropa a un cajón con llave por proyecto. El encargado de gestionar esos cajones en R es renv (Ushey y Wickham 2025).

Figura 1.3. Biblioteca global frente a bibliotecas por proyecto. A la izquierda, todos los proyectos compiten por una única biblioteca y las versiones chocan; a la derecha, cada proyecto encierra las suyas en su carpeta renv/ y los conflictos desaparecen.

El flujo con renv

Poner un proyecto bajo renv son tres verbos. renv::init() crea la biblioteca aislada y detecta los paquetes que el proyecto ya usa; renv::snapshot() escribe el lockfile (renv.lock) con las versiones exactas; y renv::restore(), en otra máquina, reinstala esas versiones a partir del lockfile. Ese renv.lock es el pilar del entorno de la figura 1.1.

renv::init()                 # crea la biblioteca aislada del proyecto
install.packages("dplyr")    # se instala DENTRO del proyecto, no en el sistema
renv::snapshot()             # anota versiones exactas en renv.lock
# en otra maquina, para reproducir el entorno bit a bit:
renv::restore()              # reinstala EXACTAMENTE lo que dice renv.lock

La figura 1.4 resume el ciclo: en tu máquina, init y snapshot destilan el entorno a un renv.lock de texto que sí se versiona; en cualquier otra, restore reconstruye a partir de él un entorno idéntico. El lockfile es el puente: pequeño, legible y suficiente para que dos máquinas, separadas por el espacio o por el tiempo, ejecuten sobre los mismos paquetes.

Figura 1.4. El ciclo de renv. snapshot destila el entorno a un lockfile de texto (que se versiona); restore lo reconstruye idéntico en otra máquina. El lockfile es el puente reproducible entre las dos.

La fortaleza de renv es que el renv.lock no congela un momento cualquiera: registra el árbol de dependencias completo y su origen (CRAN, Bioconductor, GitHub), de modo que restore() reconstruye el entorno de forma reproducible en otra máquina y en otra fecha.

Conviene mirar por dentro ese fichero, porque desmitifica el mecanismo. El renv.lock es un JSON legible que anota, para cada paquete, su versión exacta, el repositorio de origen y una suma de verificación (hash) que garantiza que el contenido descargado es idéntico bit a bit:

{
  "R": { "Version": "4.6.1", "Repositories": [ ... ] },
  "Packages": {
    "dplyr": {
      "Package": "dplyr",
      "Version": "1.2.1",
      "Source": "Repository",
      "Repository": "CRAN",
      "Hash": "6a2c...e0f1"          // identidad exacta del paquete
    }
    // ... una entrada por cada dependencia, directa o transitiva
  }
}

Con ese hash, renv::restore() no reinstala «una versión parecida de dplyr», sino exactamente este dplyr. El día a día con renv se reduce a un par de verbos más: cuando añades o actualizas un paquete, renv::status() te dice si el lockfile y la biblioteca han dejado de coincidir, y renv::snapshot() vuelve a sincronizarlos. Esa disciplina —instalar, comprobar el estado, fotografiar— es la que mantiene el entorno reproducible sin esfuerzo consciente.

La declaración: el fichero DESCRIPTION

Conviene distinguir dos ficheros con papeles distintos. El renv.lock dice qué versión exacta hay instalada (lo resuelto); el DESCRIPTION dice qué necesita el proyecto, con rangos si se quiere (lo declarado). El DESCRIPTION es el fichero estándar de metadatos de todo paquete de R, y sirve igual para un proyecto de análisis:

Package: analisis.ventas
Title: Analisis de ventas reproducible
Version: 0.1.0
Depends: R (>= 4.4)
Imports:
    dplyr (>= 1.1),
    readr,
    ggplot2

Estado del arte 2026: renv y pak

El flujo moderno combina renv para el aislamiento y el lockfile con pak para instalar: pak resuelve el árbol de dependencias de una vez y descarga en paralelo, de modo que instalar decenas de paquetes deja de ser una espera de minutos. Para gestionar varias versiones de R en la misma máquina existe rig. La combinación renv + pak + rig cubre, en 2026, los tres frentes del entorno: aislamiento, instalación rápida y versión del intérprete.

Declarado frente a resuelto: versiones y semver

La distinción entre lo declarado (DESCRIPTION: «necesito dplyr \(\geq\) 1.1») y lo resuelto (renv.lock: «hay instalado dplyr 1.2.1») es la clave de la reproducibilidad del entorno. Un rango declara compatibilidad; solo el lockfile garantiza identidad. El versionado semántico (Preston-Werner 2013)MAYOR.MENOR.PARCHE, donde un cambio de MAYOR puede romper la compatibilidad— explica por qué el rango no basta: entre dplyr 1.1 y una hipotética 2.0 podría cambiar un comportamiento por defecto, y solo fijando la versión exacta te aseguras de reproducir el resultado.

Editores, cuadernos y ejecución determinista

El guion y el editor

El análisis vive en ficheros de texto, y un buen editor los convierte en un entorno de trabajo. Los dos de referencia en R son RStudio, el IDE clásico de Posit, y Positron (Posit 2025b), su sucesor políglota —construido sobre la base de VS Code—, que trae de serie el formateador Air y el resto del instrumental moderno. Cualquiera de los dos ofrece lo que un editor de datos debe ofrecer: ejecución interactiva línea a línea, inspección de objetos, autocompletado, ayuda integrada y control de versiones.

Merece la pena nombrar los «superpoderes» que distinguen a un editor moderno de un simple bloc de notas, porque quien no los conoce trabaja al doble de esfuerzo. El panel de entorno muestra, en todo momento, qué objetos hay en memoria y su forma —cuántas filas tiene un data frame, qué tipo es una columna—, de modo que no hay que imprimir para saber qué se tiene entre manos. El autocompletado conoce las columnas de tus datos y los argumentos de cada función, y reduce los errores de tecleo a casi cero. La ayuda integrada pone la documentación de cualquier función a un atajo de distancia. Y la integración con Git y con el depurador convierte el editor en el centro del trabajo, no en un mero campo de texto.

Quarto: exploración y narración

Junto al guion existe otra forma de trabajar: el cuaderno, que entrelaza prosa, código y resultados en un mismo documento. En R el formato de referencia es Quarto (.qmd), sucesor de R Markdown: un bloque de código ‘‘‘{r} se evalúa y su salida —una tabla, una figura— se incrusta en el documento, que luego se renderiza a HTML, PDF o Word. Es el instrumento natural de la programación literaria (§1.11.2): el informe y el código que lo sostiene viven juntos.

La anatomía de un documento Quarto es sencilla: una cabecera YAML entre --- que fija el título, el formato y las opciones, seguida de prosa en Markdown y de bloques de código encabezados por ‘‘‘{r}:

---
title: "Resumen del catálogo"
format: html
execute:
  echo: true        # muestra el codigo
  warning: false    # oculta los avisos en el informe final
---

## Energía media por género


::: {.cell}

```{.r .cell-code}
library(dplyr)
musica |> group_by(genero) |> summarise(energia = mean(energy))
```
:::

Cada bloque admite opciones —escritas dentro del bloque con el prefijo #|— que controlan qué se ejecuta y qué se muestra; la tabla 1.5 recoge las más útiles. La clave de la reproducibilidad es que quarto render ejecuta el documento en una sesión limpia, de arriba abajo, de modo que la cifra publicada es, por construcción, la que produce el código actual —no un residuo de una ejecución anterior—.

Opciones de bloque en Quarto. Las opciones se escriben dentro del bloque con el prefijo #|. Controlan qué se ejecuta y qué aparece en el informe.
Opción Efecto
echo: false ejecuta el código pero no lo muestra
eval: false muestra el código pero no lo ejecuta
warning: false oculta los avisos en la salida
cache: true guarda el resultado y no reejecuta si no cambia
fig-cap pie de la figura que produzca el bloque

La trampa del estado oculto

El cuaderno tiene una trampa que causó buena parte de los fallos de Pimentel et al. (2019): el estado oculto. Como puedes ejecutar los bloques en cualquier orden y volver a ejecutar uno tras cambiarlo, lo que ves en pantalla puede depender de una historia de ejecución que ya no se corresponde con el código actual. Un objeto sigue en memoria aunque hayas borrado la línea que lo creó; un resultado en pantalla lo produjo una versión anterior de un bloque. La regla que lo desactiva es una sola: antes de creer una cifra, reinicia R y ejecuta el documento entero de arriba abajo —con quarto render, que parte siempre de una sesión limpia—. Si el resultado no sobrevive a esa ejecución limpia, no es reproducible.

Cuadernos reactivos y publicación

Hay un escalón más. Cuando el informe debe reaccionar a la interacción del lector —mover un control y ver cómo cambia una gráfica—, R ofrece Shiny, que convierte un análisis en una aplicación web, y Quarto integra esos elementos interactivos en el documento publicado. Es la diferencia entre un informe que se lee y uno con el que se juega; ambos nacen del mismo código.

Entre el documento fijo y la aplicación hay un punto intermedio muy rentable: el informe parametrizado. Un documento Quarto puede declarar parámetros en su cabecera —un género musical, un rango de fechas— y renderizarse una vez por valor:

---
params:
  genero: "rock"
---

con quarto render informe.qmd -P genero:jazz se obtiene el mismo informe para otro género sin tocar el fuente. Un solo documento bien hecho sustituye así a veinte copias que habrían divergido a la primera corrección; es el principio de «una fuente» (§1.11.2) aplicado a los informes.

Cuándo el guion, cuándo el cuaderno

La regla es de propósito. El cuaderno es para explorar y narrar: probar ideas, mirar los datos, contar una historia con figuras. El guion (.R, en src/) es para producir: código reutilizable, probado y determinista, que otro guion o un gestor de flujos ejecuta. La exploración vive en el cuaderno; la producción, en el guion. Confundirlos —meter en producción un cuaderno con estado oculto, o narrar un informe a golpe de source()— es una fuente típica de irreproducibilidad. Este libro mantiene la separación: el texto explica, y el código ejecutable y determinista vive en src/, con un criterio de nombres uniforme: capNN_tema.R para las cifras del capítulo y sus soluciones, capNN_figuras.R para las figuras —salvo el capítulo de visualización, cuyo guion temático las emite— y capNN_generar_*.R para los datos derivados. La tabla 1.6 resume cuándo elegir cada uno.

Guion o cuaderno: cuándo cada uno. No compiten: cubren fases distintas del trabajo. La exploración vive en el cuaderno; la producción, en el guion.
Cuaderno (.qmd) Guion (.R)
Propósito explorar y narrar producir código reutilizable
Ejecución bloque a bloque, interactiva de principio a fin, determinista
Salida un informe con figuras un artefacto (dato, modelo)
Riesgo estado oculto ninguno si es idempotente

Depuración, registro y calidad de código

Depurar: mirar dentro del programa

Ante un programa que no se comporta como debería, el instinto novato es salpicar el código de llamadas a print(). Existe una herramienta muy superior: el depurador. La función browser() detiene la ejecución en el punto donde se escribe y abre una consola dentro del programa, con todas sus variables vivas para inspeccionarlas; debug(f) hace lo mismo cada vez que se llama a f; y traceback(), tras un error, muestra la cadena de llamadas que llevó hasta él. Aprender a detener un programa y mirar dentro —en vez de adivinar desde fuera— es una de las destrezas que más tiempo ahorran.

El flujo típico es sembrar un browser() en la función sospechosa y, una vez detenidos dentro, teclear las variables para ver su estado, avanzar línea a línea con n (next) o continuar con c:

media_segura <- function(x) {
  browser()                 # la ejecucion se detiene AQUI, con x a la vista
  suma <- sum(x)
  suma / length(x)
}
# Browse[1]> x          # se inspecciona el argumento
# Browse[1]> n          # se avanza una linea
# Browse[1]> suma       # se comprueba el valor recien calculado

Ese diálogo con el programa detenido —mirar, avanzar, comprobar— convierte la depuración de una adivinanza en una observación, y es incomparablemente más rápido que salpicar el código de print() y volver a ejecutarlo entero.

Registrar en vez de imprimir

Para seguir lo que hace un proceso largo, print() se queda corto: no distingue niveles de importancia, ensucia la salida y hay que borrarlo después. La alternativa es registrar (logging) con mensajes que tienen nivel —información, aviso, error— y se pueden filtrar o silenciar sin tocar el código. En R, message() y warning() escriben al canal de mensajes (no a la salida de datos), y el paquete cli da mensajes con formato, y barras de progreso para las tareas pesadas (entrenamiento, validación), como pide la guía de estilo de este libro.

Validar: stop(), no un comentario

Un programa robusto comprueba sus supuestos y falla pronto y claro cuando no se cumplen, en lugar de seguir con datos corruptos y producir un número sin sentido. En R se distingue entre la comprobación barata de una condición interna —stopifnot(nrow(x) > 0), el análogo de una aserción— y el error de verdad dirigido al usuario, que se lanza con stop() o, mejor, con rlang::abort(), que permite clasificar el error para poder capturarlo con precisión (cap. 3):

leer_precios <- function(ruta) {
  if (!file.exists(ruta)) {
    rlang::abort("no encuentro el fichero de precios", class = "error_io")
  }
  # ... resto de la funcion
}

La diferencia con un comentario que dice lo que se espera es que la comprobación lo impone: el programa no puede continuar con un supuesto roto.

El otro lado de fallar es recuperarse. R tiene un sistema de condiciones más expresivo que las excepciones de otros lenguajes: tryCatch() captura un error (o un aviso) y decide qué hacer con él —registrarlo, dar un valor por defecto, reintentar— en lugar de dejar que tumbe todo el análisis. Es la base del patrón «tolerar con auditoría» que el capítulo 3 desarrolla:

leer_o_avisar <- function(ruta) {
  tryCatch(
    readr::read_csv(ruta, show_col_types = FALSE),
    error = function(e) {                      # si falla, no aborta:
      cli::cli_warn("no pude leer {ruta}: {conditionMessage(e)}")
      NULL                                     # devuelve NULL y sigue
    }
  )
}

Validar pronto y capturar con criterio son las dos caras de un código robusto: el primero impide entrar en un estado imposible; el segundo evita que un fallo aislado —un fichero de un millón que llegó corrupto— eche por tierra el trabajo entero.

Formato, estilo y aserciones: Air y lintr

Dos herramientas mantienen el código legible sin esfuerzo. Air (Posit 2025a) es un formateador —reescribe el código a un estilo canónico: sangrado, espacios, saltos de línea— escrito en Rust y endiabladamente rápido; viene de serie en Positron. lintr es un linter: no cambia el código, sino que señala olores y posibles errores (variables sin usar, nombres inconsistentes). Formatear no es cosmética: un estilo uniforme elimina discusiones estériles y hace que los cambios en el control de versiones reflejen ideas, no reformateos.

Estilo: la guía del tidyverse

Air aplica la guía de estilo del tidyverse (Wickham 2025), el consenso de la comunidad: nombres en minúscula con guion bajo (precio_medio), asignación con <-, líneas de hasta 80 columnas —que coincide con la preferencia de brevedad de este libro— y espacios alrededor de los operadores. Como el Zen de otros lenguajes, su valor no está en cada regla por separado, sino en que haya una convención compartida: el código deja de ser una firma personal y pasa a ser legible por cualquiera.

La tabla 1.7 resume el instrumental de calidad de código en R y su papel. Ninguna de estas herramientas es imprescindible para que el código funcione; lo son para que el código se sostenga cuando lo lee otra persona —o tú, medio año después—.

Instrumental de calidad de código en R. Cada herramienta cubre un frente.
Frente Herramienta
Formateo Air
Linting lintr
Depuración browser, debug
Registro cli, message
Aserciones stopifnot, checkmate
Pruebas testthat

Control de versiones con Git

Nada de lo anterior se sostiene sin control de versiones. Git registra la historia de un proyecto como una sucesión de instantáneas (commits), cada una con su autor, su fecha y su mensaje, de modo que siempre se puede saber qué cambió, cuándo y por qué, y volver a cualquier punto anterior. Es, a la vez, una máquina del tiempo, un cuaderno de bitácora y la base de la colaboración. Los comandos del día a día son pocos:

git init                 # inicia el repositorio en la carpeta del proyecto
git add R/ DESCRIPTION    # selecciona que entra en la proxima instantanea
git commit -m "carga de datos: lectura y validacion del CSV"
git log --oneline        # la historia, un commit por linea

El ciclo básico es siempre el mismo —trabajar, seleccionar lo que entra en la instantánea (add), fotografiarla (commit)— y unos pocos comandos más cubren casi todo (tabla 1.8). No hay que memorizarlos: se aprenden usándolos, y un editor como Positron o RStudio los expone con botones para las operaciones frecuentes.

Comandos de Git del día a día. El ciclo trabajar–addcommit cubre el uso cotidiano; los demás resuelven la mayoría de las situaciones restantes.
Comando Qué hace
git status qué ha cambiado desde la última instantánea
git diff el detalle línea a línea de esos cambios
git add <f> selecciona <f> para la próxima instantánea
git commit -m crea la instantánea con su mensaje
git log --oneline la historia, un commit por línea
git switch -c <r> crea una rama y se cambia a ella
git restore <f> descarta los cambios no confirmados de <f>

Buenos mensajes y qué no versionar

Un buen mensaje de commit explica el porqué, no el qué (que ya se ve en el cambio): «corrige el sesgo de la muestra al estratificar por género» dice más que «cambios varios». Y hay cosas que nunca se versionan: los datos pesados (van aparte, con su procedencia), los artefactos regenerables (figuras, PDF, la carpeta renv/library) y —sobre todo— los secretos (claves de API). Un fichero .gitignore desde el primer día los excluye:

# .gitignore
renv/library/          # los paquetes se reinstalan desde renv.lock
data/procesados/       # regenerable desde los datos crudos y el codigo
.Rhistory              # historia de la sesion, ruido
.Renviron              # variables de entorno y secretos: JAMAS al repo

Trabajo en equipo y revisión de código

Sobre Git se monta, además, el trabajo a varias manos. En un servicio como GitHub, cada cual desarrolla en su rama (branch) y propone incorporar sus cambios mediante una pull request, que un compañero lee y aprueba —o discute— antes de que entren. Esa lectura mutua es de lo más valioso del proceso: atrapa errores que su autor ya no ve, reparte el conocimiento del código por el equipo y fija un listón común de calidad. Y en el mismo tablero vive la integración continua (§1.14.2), que relanza las pruebas con cada cambio propuesto.

La figura 1.5 muestra la coreografía. La regla que la hace funcionar es mantener main siempre funcional: nadie escribe directamente en ella; todo cambio —una limpieza nueva, un modelo, un arreglo— nace en una rama corta, con un propósito y un nombre descriptivo, y vuelve por una pull request pequeña. Pequeña es la palabra importante: una PR de cien líneas se revisa con atención; una de tres mil se aprueba sin leer. Y aun trabajando en solitario el patrón compensa, porque la rama es un experimento sin riesgo —si la idea no cuaja, se borra y main nunca se enteró— y la PR, aunque la apruebes tú, deja escrito qué cambió y por qué.

Figura 1.5. Ramas y pull request. El trabajo nace en una rama corta con nombre descriptivo (naranja) mientras main (azul) permanece siempre funcional; los cambios vuelven mediante una pull request revisada, que además dispara la integración continua antes de integrarse.

Contenedores: reproducibilidad a largo plazo

El lockfile de renv clava las versiones de los paquetes, pero deja fuera el sistema operativo, la propia versión de R y las bibliotecas del sistema de las que dependen algunos paquetes (una biblioteca de álgebra, un compilador). Para una reproducibilidad a prueba de años, un contenedor (Boettiger 2015) empaqueta todo eso: un sistema completo, congelado, que corre igual en cualquier máquina. El proyecto Rocker ofrece imágenes de R listas —rocker/r-ver fija una versión de R, rocker/tidyverse la trae ya con el tidyverse—, sobre las que un Dockerfile añade el proyecto y restaura el entorno:

FROM rocker/r-ver:4.6.1        # fija R y el sistema base
COPY renv.lock renv.lock
RUN R -e "renv::restore()"     # reinstala EXACTAMENTE lo del lockfile
COPY . .
CMD ["Rscript", "scripts/analisis.R"]

Es la capa más externa de la reproducibilidad: el lockfile garantiza los paquetes; el contenedor, todo lo demás. El proyecto Rocker mantiene una familia de imágenes para distintos usos (tabla 1.9), y para quien quiera esa garantía sin escribir Dockerfiles a mano, el ecosistema de R ofrece rix, que describe el entorno completo de forma declarativa sobre Nix.

Imágenes de Rocker más habituales. Cada una parte de la anterior y añade una capa; se elige la mínima que cubra las necesidades del proyecto.
Imagen Qué trae
rocker/r-ver una versión fija de R sobre un sistema base
rocker/tidyverse lo anterior más el tidyverse y RStudio
rocker/verse lo anterior más LaTeX y publicación
rocker/ml lo anterior más el instrumental de aprendizaje automático

Anatomía de un proyecto reproducible

Un análisis serio nunca cabe en un fichero suelto: necesita una geografía donde los datos no se mezclen con el código, lo exploratorio no contamine lo estable y las entradas no se confundan con las salidas. R parte aquí con ventaja: su unidad natural de código reutilizable, el paquete, trae esa organización de serie, y usethis la levanta con un comando. La figura 1.6 muestra la disposición mínima que este libro emplea.

Figura 1.6. Estructura de un proyecto de datos. Organización mínima de un proyecto reproducible: datos crudos intocables, funciones reutilizables en R/ (como un paquete) y programas ejecutables aparte en scripts/, que usan las funciones, nunca al revés.

A cada carpeta, un oficio, y solo uno (tabla 1.10). Respetar ese reparto —que los datos no aniden entre el código, que lo experimental no se cuele en producción— es lo que consigue que el proyecto siga siendo navegable cuando pase de diez ficheros a cien.

Carpetas de un proyecto y su papel. Un oficio por directorio; en cuanto dos oficios comparten carpeta, el desorden tiene la puerta abierta.
Carpeta Papel
data/crudos/ el original intacto: se descarga y no se vuelve a tocar
data/procesados/ lo que produce la limpieza; se puede regenerar
R/ funciones con vocación de reutilizarse (formato paquete)
scripts/ ejecutables de principio a fin, clientes de R/
tests/testthat/ la red de pruebas automatizadas

Bajo la estructura laten dos reglas. La primera: al dato crudo no lo toca nadie. Entra una vez, se archiva, y cualquier limpieza escribe ficheros nuevos en procesados/ —así el camino desde el origen siempre se puede volver a andar—. La segunda: lo reutilizable se escribe como función (R/) y lo ejecutable como script (scripts/), con la dependencia en un solo sentido: los scripts llaman a las funciones, jamás al contrario. Parece papeleo hasta el día en que quieres probar una función sin relanzar el análisis entero —exactamente lo que el capítulo 16 hará—. Encadenar las etapas —descarga, limpieza, entrenamiento, evaluación— es tarea de targets, el gestor de flujos de R que solo rehace lo que ha cambiado, de modo que reconstruir el análisis entero quepa en una sola orden (targets::tar_make()).

Orquestar con targets

Merece la pena ver targets de cerca, porque encarna la reproducibilidad en forma de herramienta. Un fichero _targets.R declara el análisis como un grafo de dependencias: cada objetivo (target) es un paso —descargar, limpiar, modelar— que depende de los anteriores. targets calcula ese grafo, ejecuta solo lo que hace falta y guarda cada resultado; si cambias una función, tar_make() rehace ese paso y los que dependen de él, y nada más.

# _targets.R
library(targets)
tar_source("R/")                       # carga las funciones del proyecto
list(
  tar_target(fichero, "data/crudos/musica.csv", format = "file"),
  tar_target(datos, cargar(fichero)),          # depende de 'fichero'
  tar_target(limpio, limpiar(datos)),          # depende de 'datos'
  tar_target(modelo, ajustar(limpio))          # depende de 'limpio'
)

Con eso, tar_make() ejecuta el flujo entero la primera vez y, en adelante, solo lo que cambió; tar_visnetwork() dibuja el grafo con los pasos al día en verde y los obsoletos en otro color. Es la diferencia entre «creo que reejecuté todo lo necesario» y «la herramienta garantiza que el resultado está al día». El capítulo 16 retoma targets como columna del proyecto desplegable.

La figura 1.7 muestra la mecánica que hace valioso el grafo: la invalidación selectiva. targets guarda el hash del código de cada objetivo y el de su resultado; cuando cambias la función limpiar(), el objetivo limpio deja de estar al día, y con él todo lo que depende de élmodelo—, pero fichero y datos, que no dependen de lo tocado, conservan su resultado almacenado y no se recalculan. En un análisis donde la carga tarda minutos y el modelo horas, esa contabilidad automática es la diferencia entre iterar con agilidad y temer cada cambio. tar_outdated() lista, sin ejecutar nada, qué objetivos están obsoletos: la pregunta «¿qué tendría que rehacer?» tiene respuesta exacta antes de pagar el coste de rehacerlo.

Figura 1.7. Invalidación selectiva en el grafo de targets. Arriba, el flujo con todos los objetivos al día. Abajo, tras cambiar la función limpiar(): quedan obsoletos limpio y todo lo que depende de él (modelo), y tar_make() rehace solo esos dos; fichero y datos conservan su resultado almacenado.

Pruebas: la red de seguridad

La última pieza de un proyecto que se sostiene son las pruebas automatizadas: un código que comprueba que otro código hace lo que debe, y que se ejecuta en cada cambio (§1.14.2). En R, el paquete de referencia es testthat, con una sintaxis que se lee casi como una frase —«espero que esto sea igual a aquello»—:

test_that("normalizar lleva al rango [0, 1]", {
  expect_equal(normalizar(c(2, 4, 6)), c(0, 0.5, 1))
  expect_true(all(normalizar(runif(100)) >= 0))
})

Una prueba no demuestra que el código sea correcto, pero sí atrapa la regresión —el error que se cuela al cambiar otra cosa— antes de que llegue a un resultado publicado. El capítulo 16 las desarrolla, incluidas las pruebas de propiedad, que generan cientos de casos automáticamente.

Determinismo y aleatoriedad reproducible

Queda el pilar traicionero (figura 1.1): la aleatoriedad, el que nadie olvida en la teoría y casi todos en la práctica. Partir en entrenamiento y test, inicializar un modelo, remuestrear con bootstrap (cap. 11): todo eso consulta un generador de números pseudoaleatorios, y un generador sin semilla fijada entrega números nuevos en cada ejecución —o sea, cifras que no se pueden volver a obtener—. El gesto que lo desactiva en R es set.seed(), reforzado con RNGkind() si se quiere dejar el tipo de generador a salvo de futuros cambios del valor por defecto:

set.seed(2026)                 # semilla fija -> reproducible
x1 <- rnorm(1000)              # los MISMOS 1000 valores en cada corrida
set.seed(2026)
x2 <- rnorm(1000)
identical(x1, x2)              # TRUE: la semilla garantiza el mismo flujo

Hay una alternativa que evita un error sutil —olvidar que set.seed() altera el estado global del generador y afecta a todo lo que venga después—: withr::with_seed(2026, rnorm(1000)) fija la semilla solo para esa expresión y restaura el estado al terminar. Es la forma disciplinada de sembrar sin efectos colaterales.

NotaAvanzado

La aleatoriedad no es portable entre lenguajes. Cada lenguaje elige su generador por defecto —el de R es Mersenne-Twister; otros entornos eligen otros, como PCG64— y, con la misma semilla, generadores distintos producen flujos distintos. Una semilla declarada garantiza la reproducción dentro del mismo lenguaje y versión, no a través de implementaciones: quien replique tu análisis con otras herramientas obtendrá tu misma estructura (esquema, número de filas) pero otros números del sorteo. La consecuencia práctica: documenta lenguaje, generador y semilla como parte de la receta. Y dentro de R hay un matiz más: el trabajo en paralelo necesita el generador L’Ecuyer-CMRG (RNGkind("L’Ecuyer-CMRG")) para que cada hilo reciba un flujo independiente y reproducible.

Ese trabajo en paralelo reproducible se declara con dos líneas: se fija el generador apropiado y una semilla, y el marco de paralelismo se encarga de dar a cada tarea un flujo propio que no se solapa con los demás:

library(furrr)
plan(multisession)                    # varias sesiones en paralelo
options(future.seed = TRUE)           # un flujo L'Ecuyer por tarea
resultados <- future_map(1:100, ~ mean(rnorm(1000)))   # reproducible

Sin esa precaución, dos ejecuciones en paralelo darían números distintos —o, peor, dos hilos compartirían el mismo flujo y sus resultados estarían correlacionados sin que nadie lo notara—. La reproducibilidad del azar sobrevive al paralelismo solo si se pide explícitamente.

Y con esto, el andamiaje queda montado; a partir de aquí el libro lo usa sin volver a discutirlo. El contrato en adelante: toda cifra de la prosa llegará medida por código relanzable, o sintética y declarada como tal, o citada con su fuente; y detrás de cada figura habrá un guion determinista. El acompañante ejecutable del capítulo es src/cap01_entorno.R: imprime la identidad del entorno y demuestra el determinismo repitiendo una generación con semilla fija y comparando.

Documentación y comunicación del código

Piensa en el lector más frecuente de tu código: tú mismo, dentro de unos meses, sin recordar nada del contexto. Para ese lector se documenta. Y documentar no es la guinda que se pone al final si sobra tiempo: es parte del acto de programar bien, porque redactar la intención obliga a tenerla clara. La brújula viene de la programación literaria de Knuth (1984): el destinatario de un programa es una persona; que además lo ejecute una máquina es secundario.

El README como puerta de entrada

La puerta de entrada de cualquier proyecto es su README.md, y su cometido cabe en tres preguntas: ¿qué es esto?, ¿cómo lo instalo?, ¿cómo lo ejecuto? Cuando esas respuestas están, alguien que no te conoce reproduce el análisis en lo que dura un café; cuando faltan, el proyecto muere de olvido a la primera distracción de su autor. El formato es Markdown —marcado ligero, legible en crudo, renderizado a HTML por cualquier repositorio— y la versión mínima ocupa un puñado de líneas:

# Análisis del catálogo de música

Clasifica el género de una pista por su perfil de sonido.

## Instalación
    R -e 'renv::restore()'          # reconstruye el entorno del lockfile

## Ejecución
    Rscript scripts/entrenar.R      # ajusta y evalua el modelo

## Datos
Spotify Tracks Dataset (Hugging Face), licencia BSD. Ver `data/README.md`.

Cuatro respuestas —qué, instalación, ejecución, origen de los datos— y quien llegue de fuera ya no necesita escribirte para reproducir el trabajo.

Igual de importante es lo que no va en el README: no es el manual de referencia (eso es roxygen2 y el sitio de pkgdown), ni el diario del proyecto, ni el lugar de los resultados —un README que hay que desplazar tres pantallas ya no es una puerta de entrada—. Dos complementos sí merecen fichero propio. Un data/README.md que documente cada dataset: fuente canónica con enlace, fecha y versión de la descarga, licencia, y una línea por columna con su significado y unidades; es la traducción doméstica de los principios FAIR (§1.1.4), y donde primero mira quien duda de un dato. Y, si el proyecto es citable, un fichero CITATION.cff —un formato YAML estándar que los repositorios entienden— con autores, título y versión, para que citarte bien no exija adivinar.

roxygen2: el contrato de cada función

La documentación no tiene por qué guardarse aparte: con la convención roxygen2 se redacta pegada a cada función, en comentarios especiales que abren con #’. A diferencia de un comentario suelto, roxygen2 se procesa: genera la página de ayuda del paquete (la que se ve con ?funcion), de modo que la documentación y el código no se separan nunca:

#' Escala un vector al rango [0, 1].
#'
#' @param x vector numerico no vacio.
#' @return vector escalado; ceros si todos los valores son iguales.
#' @examples
#' normalizar(c(2, 4, 6))   # 0.0 0.5 1.0
normalizar <- function(x) {
  lo <- min(x); hi <- max(x)
  if (hi == lo) rep(0, length(x)) else (x - lo) / (hi - lo)
}

Las etiquetas @param y @return documentan de forma estructurada qué recibe y qué devuelve la función; @examples añade código que, además, se comprueba al revisar el paquete (cap. 16), de modo que un ejemplo roto se detecta solo. La tabla 1.11 resume las etiquetas más usadas. De estos comentarios, roxygen2::roxygenise() genera las páginas de ayuda, y pkgdown un sitio web de documentación navegable, sin escribir nada dos veces. El principio, otra vez, es «una fuente»: la documentación nace del código y no puede quedar desactualizada en silencio.

Etiquetas de roxygen2 más usadas. Se escriben en comentarios #’ sobre la función; de ellas se genera la ayuda (?funcion) y el sitio de documentación.
Etiqueta Documenta
@param un argumento: su nombre y qué espera
@return qué devuelve la función
@examples código de ejemplo (se comprueba al revisar)
@export que la función sea visible fuera del paquete
@seealso referencias cruzadas a otras funciones

Procedencia, licencias y caché de datos

De dónde vienen los datos

Un análisis vale lo que valen sus datos, y los datos valen lo que vale su procedencia. De cada conjunto conviene registrar tres cosas: de dónde salió (la URL o la fuente canónica), cuándo se descargó (la fecha o la versión) y con qué derecho se usa (la licencia). Sin esa ficha, un número deja de ser defendible en cuanto alguien pregunta «¿de dónde sale esto?». El caso transversal de este libro —el catálogo de música— entra en el capítulo 5 con esa ficha completa.

Licencias: tener el dato no es tener el derecho

Descargar un fichero no otorga el derecho a usarlo, redistribuirlo o publicar resultados derivados: eso lo fija su licencia. Las hay abiertas y permisivas (CC0, CC-BY, que solo pide atribución) y las hay restrictivas (que prohíben el uso comercial o la redistribución). Comprobar la licencia antes de construir sobre un dato no es burocracia: es lo que separa un trabajo publicable de un problema legal. La disciplina de procedencia y la de seguridad —de dónde viene el dato, quién lo tocó, con qué derecho— son una seña de identidad de este volumen.

Caché idempotente: descargar una vez

Un análisis reproducible no vuelve a descargar los datos en cada ejecución: los descarga una vez, los guarda como crudos inmutables y, en adelante, los lee de disco. El patrón se llama caché idempotente —comprobar si el fichero ya existe antes de pedirlo a la red— y ahorra tiempo, ancho de banda y sorpresas si la fuente cambia:

descargar_una_vez <- function(url, destino) {
  if (!file.exists(destino)) {            # solo si no esta ya en disco
    dir.create(dirname(destino), recursive = TRUE, showWarnings = FALSE)
    download.file(url, destino, mode = "wb")
  }
  destino
}

A esa base se le añade la verificación de integridad: una suma de verificación (hash) del fichero descargado, que se compara con la esperada para detectar una descarga corrupta o —lo que la disciplina de seguridad del autor no olvida— manipulada. Guardar junto al dato su hash, su URL de origen y la fecha de descarga es lo que convierte un fichero anónimo en un dato con procedencia:

library(digest)
huella <- digest(file = "data/crudos/musica.csv", algo = "sha256")
# se compara con la huella publicada por la fuente;
# si no coincide, el dato no es fiable

El capítulo 5 lleva esta idea a su forma completa —con la fecha de descarga, la suma de verificación y los metadatos— para el catálogo de música.

Medir sin engañarse: rendimiento y tiempos

Antes de optimizar, hay que medir: la intuición sobre qué es lento acierta poco. R trae system.time() para un cronometraje rápido, y el paquete bench —o microbenchmark— para comparar alternativas con rigor, repitiendo cada una muchas veces y descartando el ruido. Hay un principio, heredado de la política de datos de este libro, que conviene grabar: un tiempo de reloj se comunica como cociente o como orden de magnitud, jamás en cifra absoluta, pues esta depende de la máquina, de la carga y de la versión. «El camino vectorizado es unas cien veces más rápido» es una afirmación defendible y portable; «tardó 3,2 milisegundos» no lo es.

Un ejemplo mínimo mide lo que la Parte III desarrollará: sumar diez millones de números con un bucle explícito frente a la función vectorizada sum().

x <- runif(1e7)
system.time({ s <- 0; for (v in x) s <- s + v })   # ~0.16 s
system.time(sum(x))                                 # ~0.016 s
# la version vectorizada: del orden de 10 veces mas rapida aqui

Damos el resultado como cociente —«unas diez veces»— y no como los segundos absolutos, que dependen de la máquina. profvis añade el detalle que a bench se le escapa: señala, dentro de un programa, en qué líneas se va el tiempo. Medir antes de optimizar evita reescribir lo que ya era rápido. La Parte III vuelve sobre esto al comparar el bucle con la vectorización (cap. 7).

El entorno en la nube y la automatización

Entornos en la nube

No siempre se trabaja en la propia máquina. Posit Cloud ofrece RStudio en el navegador sin instalar nada, útil para docencia y para compartir un entorno idéntico; y un servicio como Binder puede arrancar, a partir de un repositorio con su DESCRIPTION o su install.R, una sesión ejecutable en la nube, de modo que cualquiera pruebe el código sin configurar su ordenador. En esa idea se apoya la edición web de este libro: el texto se lee, y cada capítulo podrá ejecutarse en un entorno en la nube con un clic.

Integración continua: automatizar las comprobaciones

¿Quién vigila que el proyecto sigue sano cuando nadie mira? La integración continua (CI): un servicio que, con cada cambio subido al repositorio, relanza por su cuenta las comprobaciones —pruebas (cap. 16), estilo, instalación desde cero en una máquina limpia—. Para R, la colección r-lib/actions trae los flujos de GitHub Actions ya cocinados, R CMD check incluido, y el flujo mínimo son unas líneas de YAML: a cada push, instalar R, restaurar el entorno del lockfile y correr las pruebas.

# .github/workflows/comprobar.yaml
on: [push]
jobs:
  comprobar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: r-lib/actions/setup-r@v2        # instala R
      - uses: r-lib/actions/setup-renv@v2     # restaura el entorno (renv.lock)
      - run: Rscript -e 'testthat::test_dir("tests")'   # ejecuta las pruebas

Su valor es que la reproducibilidad deja de depender de la buena voluntad: la máquina comprueba, en cada cambio, que el proyecto sigue instalándose y pasando las pruebas desde cero, en un entorno limpio que no es el del autor.

Configuración, secretos y determinismo del entorno

Configuración fuera del código

Un principio sano: lo que cambia entre máquinas o entre ejecuciones —una ruta, una clave de API, un parámetro— no se escribe en el código, sino en la configuración. En R, las variables de entorno viven en el fichero .Renviron (que nunca se versiona) y se leen con Sys.getenv("CLAVE_API"); el paquete config gestiona parámetros por entorno (desarrollo, producción). La mecánica es sencilla: la clave vive en un fichero que Git ignora, y el código la lee del entorno sin conocer nunca su valor literal.

# .Renviron  (en la raiz del proyecto, EXCLUIDO del control de versiones)
SPOTIFY_TOKEN=abc123...

# en el codigo, jamas la clave literal:
token <- Sys.getenv("SPOTIFY_TOKEN")   # se lee del entorno, no del codigo

Empotrar una clave de API en el código y empujarla a un repositorio público figura entre los descuidos de seguridad más frecuentes —y más caros—: una vez publicada, hay que darla por comprometida —los robots que rastrean GitHub la encuentran en minutos—, y no basta con borrar el commit, porque queda en la historia. La única cura es rotar la clave. Por eso la disciplina es preventiva: el secreto nunca toca el código.

Determinismo del entorno: el orden y el hash

La reproducibilidad del entorno tiene un último matiz. Dos instalaciones con «los mismos paquetes» pueden diferir si se resolvieron en momentos distintos: por eso el lockfile no lista solo nombres, sino versiones exactas y, a menudo, una suma de verificación (hash) de cada paquete, que garantiza que el contenido es idéntico bit a bit. Ese hash es lo que convierte «tengo dplyr» en «tengo exactamente este dplyr», y es la razón de que renv::restore() reconstruya un entorno idéntico y no solo parecido.

Errores frecuentes al empezar

Todos los principiantes tropiezan en las mismas baldosas, así que señalarlas tiene un rendimiento altísimo: cada uno de estos tropiezos, reconocido a tiempo, son semanas de frustración que no ocurren.

  • Instalar en el sistema en vez de en el proyecto. Cada install.packages lanzado fuera de renv engorda la biblioteca global y prepara el próximo conflicto de versiones. Solución: renv::init() y un cajón de paquetes por proyecto (§1.4).

  • Confundir <- y =, o = y ==. En R se asigna con <-, y comparar con = en lugar de == es el desliz de manual. Solución: seguir el estilo del tidyverse y dejar que el linter chille.

  • Versionar datos y artefactos. Meter en Git ficheros pesados o salidas que el código regenera (la carpeta renv/library incluida) infla el repositorio hasta hacerlo inservible. Solución: escribir el .gitignore antes que el primer commit (§1.7.1).

  • Editar los datos crudos. Ese valor «corregido» directamente en el fichero original acaba de destruir tu punto de partida. Solución: crudos intocables; las correcciones viven en código y producen ficheros nuevos.

  • No fijar la semilla. Si el número cambia con cada ejecución, no hay número que defender. Solución: set.seed() y comprobarlo relanzando (§1.10).

  • Fiarse del estado de la sesión. Esa cifra en pantalla puede deberse a una versión del código que ya borraste. Solución: antes de creerla, sesión limpia y ejecución de arriba abajo (Rscript o quarto render).

  • Teclear números a mano en el informe. En el momento de copiar un resultado de la consola al texto, el número se divorcia del código que lo produjo. Solución: toda cifra viaja del código al informe por vía programática (cap. 10).

  • Cablear secretos en el código. Escribir una clave de API en un fichero que acaba en el repositorio. Solución: .Renviron excluido del control de versiones (§1.15.1).

  • Olvidar cargar el paquete. Usar una función de dplyr sin library(dplyr) da un «could not find function». Solución: cargar los paquetes al principio del guion, o llamar con dplyr::.

  • Reejecutar sobre datos ya transformados. Correr dos veces una celda que modifica un objeto deja un resultado que depende de cuántas veces se ejecutó. Solución: funciones sin efectos colaterales y sesión limpia.

  • Confiar en la ruta de trabajo. Un guion con rutas relativas a la carpeta «desde donde lo lancé» falla en otra máquina. Solución: el paquete here ancla las rutas a la raíz del proyecto.

  • Cablear setwd() con una ruta absoluta. La primera línea setwd("/home/yo/analisis") garantiza que el guion solo funcione en esa máquina y esa cuenta. Solución: no fijar el directorio en el código; abrir el proyecto (que ya define la raíz) y rutas con here.

  • Creer que rm(list = ls()) limpia la sesión. Borra los objetos, pero no descarga paquetes, ni restaura opciones, ni reinicia el generador aleatorio: la sesión sigue contaminada. Solución: reiniciar R de verdad; es la única sesión limpia que existe.

  • Ignorar los avisos. Un warning como «NAs introduced by coercion» no es ruido: anuncia que una conversión acaba de estropear datos (cap. 2). Solución: leer cada aviso la primera vez que aparece y decidir; en producción, elevarlos con options(warn = 2) para que ninguno pase de largo.

  • Usar attach(). Poner un data frame en la ruta de búsqueda para escribir energia en vez de datos$energia crea ambigüedad sobre de dónde sale cada nombre y produce errores fantasma. Solución: no usarlo; dplyr (cap. 8) da la misma comodidad dentro de un ámbito controlado.

Tomados de uno en uno, son tropiezos menores. El daño está en la acumulación: tres o cuatro de estos hábitos juntos producen ese análisis que «solo funciona en mi máquina, y solo hoy». Todo el capítulo ha sido, en el fondo, la vacuna.

Un primer flujo completo, de principio a fin

Para terminar, montamos las piezas en un flujo real en miniatura —la maqueta de lo que el resto del libro hará en grande—: preparar el entorno, un guion determinista que fabrica un artefacto, otro que lo consume, y la comprobación final de que todo ello es reproducible de verdad. Los datos que usaremos son sintéticos declarados (clase 2, §1.1.5): existen para ilustrar el flujo, no miden nada del mundo.

El plano está en la figura 1.8: un guion produce, un artefacto conecta, otro guion consume; la orquestación encadena los pasos y la verificación cierra el círculo.

Figura 1.8. El flujo de dos guiones. generar.R fabrica, con semilla fija, el artefacto pistas.csv; resumir.R lo lee y resume; y la rama de verificación relanza la generación para comprobar, hash contra hash, que el artefacto es idéntico. El libro entero repite este patrón a mayor escala.

Preparar el entorno

mkdir mini-flujo && cd mini-flujo
R -e 'renv::init()'                    # biblioteca aislada del proyecto
R -e 'install.packages(c("readr","dplyr")); renv::snapshot()'
mkdir -p data/procesados scripts
git init                               # nace bajo control de versiones

Generar un artefacto de forma determinista

Abre el baile scripts/generar.R: fabrica una tabla sintética bajo semilla fija —los mismos 500 registros en cada ejecución, sin excepción— y la deposita en data/procesados/. Antes de despedirse imprime cinco filas al azar: nunca guardes nada sin echarle un vistazo.

# scripts/generar.R -- tabla sintetica de pistas (semilla fija -> reproducible).
library(dplyr)
library(readr)

generar <- function(n = 500, semilla = 2026) {
  set.seed(semilla)                                   # semilla fija
  genero <- sample(c("pop", "rock", "classical"), n, replace = TRUE)
  base <- case_when(genero == "pop" ~ 0.72,           # energia tipica
                    genero == "rock" ~ 0.85,
                    TRUE ~ 0.30)
  energy <- pmin(pmax(base + rnorm(n, 0, 0.08), 0), 1)
  tibble(genero = genero, energy = round(energy, 3))
}

if (sys.nframe() == 0L) {
  pistas <- generar()
  dir.create("data/procesados", recursive = TRUE, showWarnings = FALSE)
  write_csv(pistas, "data/procesados/pistas.csv")
  cat("guardadas", nrow(pistas), "filas\n")
  set.seed(0); print(slice_sample(pistas, n = 5))    # inspeccion: 5 muestras
}
# guardadas 500 filas
# # A tibble: 5 x 2
#   genero    energy
#   <chr>      <dbl>
# 1 pop        0.708
# 2 classical  0.235
# 3 classical  0.215
# 4 rock       0.936
# 5 pop        0.756

Consumir el artefacto

Al otro lado, scripts/resumir.R hace deliberadamente poco: lee el CSV que dejó el generador —no lo recalcula— y agrega. Que producir y consumir sean guiones separados no es manía organizativa: es lo que permitirá, cuando algo cambie, rehacer solo el tramo afectado, y es la semilla de la orquestación con targets (§1.9).

# scripts/resumir.R -- lee el artefacto y calcula un resumen (no lo regenera).
library(dplyr)
library(readr)

resumir <- function(pistas) {
  pistas |>
    group_by(genero) |>
    summarise(media = round(mean(energy), 2),
              mediana = round(median(energy), 2),
              n = n())
}

if (sys.nframe() == 0L) {
  pistas <- read_csv("data/procesados/pistas.csv", show_col_types = FALSE)
  print(resumir(pistas))
}
# # A tibble: 3 x 4
#   genero    media mediana     n
#   <chr>     <dbl>   <dbl> <int>
# 1 classical  0.3     0.3    140
# 2 pop        0.73    0.73   179
# 3 rock       0.85    0.86   181

Fíjate en las cifras: son sintéticas declaradas, nacidas del generador de R con la semilla 2026. Cualquiera que ejecute este guion en R obtiene estos mismos conteos y medias hasta el último decimal; quien reimplemente la receta con otro lenguaje u otro generador obtendrá la misma forma —tres géneros, la energía típica de cada uno— pero otros números (§1.10). Es la distinción de §1.1.5 en acción: el número exacto pertenece al código que lo produjo.

Orquestar y versionar

Para no encadenar los guiones a mano, un _targets.R declara la dependencia entre ambos pasos como grafo, y targets::tar_make() pasa a ser el único comando necesario —rehaciendo, en adelante, solo lo que cambie—:

# _targets.R
library(targets)
tar_source(c("scripts/generar.R", "scripts/resumir.R"))   # carga las funciones
list(
  tar_target(pistas, generar()),                        # produce el artefacto
  tar_target(resumen, resumir(pistas))                  # lo consume
)

Y en cuanto el flujo respira, primer commit. Entra el código y la configuración; los datos generados se quedan fuera vía .gitignore, precisamente porque el código puede regenerarlos cuando haga falta:

echo "data/procesados/" > .gitignore
git add DESCRIPTION renv.lock scripts/ _targets.R .gitignore
git commit -m "flujo minimo: generar y resumir pistas (determinista)"

Verificar la reproducibilidad

Falta el gesto final, el que separa suponer de saber: demostrar que el flujo reproduce. La prueba es artesanal y contundente —generar dos veces, comparar los hashes del artefacto—:

Rscript scripts/generar.R && md5sum data/procesados/pistas.csv
# 14d1ea58084017421b03d132d81b2f9e  data/procesados/pistas.csv
Rscript scripts/generar.R && md5sum data/procesados/pistas.csv
# 14d1ea58084017421b03d132d81b2f9e  data/procesados/pistas.csv  <- COINCIDE

Dos hashes iguales significan que la semilla gobierna de verdad todo el azar del guion, y el resultado admite defensa pública. Dos hashes distintos delatan una fuente de aleatoriedad huérfana que habría envenenado cualquier cifra construida encima. En el vocabulario de la política de datos del libro (cap. 10), esto es «verificar contra el artefacto, no contra el parte»: no te creas lo que el programa dice que hizo; comprueba lo que dejó escrito en disco. Producir con código determinista y verificar el producto: sobre ese doble gesto se construye todo lo que viene.

La figura 1.9 lo condensa en un bucle. El caso «difieren» no es un fracaso: es el diagnóstico funcionando. Las causas típicas, por orden de frecuencia: una llamada aleatoria antes del set.seed() (o fuera de su alcance), una marca de tiempo o una ruta absoluta escrita dentro del artefacto, un orden de filas que depende del sistema de ficheros, o paralelismo sin el generador L’Ecuyer (§1.10). Se localiza la fuente, se fija, y se vuelve a ejecutar el bucle hasta que los hashes cuadren; solo entonces el resultado pasa a Git.

Figura 1.9. El bucle de verificación de la reproducibilidad. Dos ejecuciones, dos hashes: si coinciden, el artefacto es defendible y se registra; si difieren, hay una fuente de aleatoriedad sin fijar —semilla fuera de alcance, marca de tiempo, orden dependiente del sistema— que se repara antes de volver a intentarlo.

Como cierre, la tabla 1.12 condensa el capítulo en una lista de control: diez preguntas que se le hacen a cualquier proyecto —propio o ajeno— para saber si es reproducible, con la herramienta que responde a cada una. Vale como auditoría rápida (¿cuántas responde tu último análisis?) y como mapa de lo que queda por montar.

Lista de control del proyecto reproducible. Diez preguntas, su herramienta en R y la sección donde se desarrollan.
Pregunta Herramienta Dónde
¿El entorno está fijado y es restaurable? renv.lock §1.4
¿Todo el código está versionado? Git §1.7
¿Los datos crudos son inmutables y con origen? caché + data/README §1.12
¿La aleatoriedad está sembrada? set.seed §1.10
¿El flujo se rehace con un comando? targets §1.9.1
¿Hay pruebas que atrapen regresiones? testthat §1.9.2
¿Las comprobaciones corren solas? GitHub Actions §1.14.2
¿Una persona ajena sabe empezar? README §1.11.1
¿Los secretos viven fuera del código? .Renviron §1.15.1
¿El resultado se verificó, no se supuso? md5sum \(\times 2\) §1.17.5

Lecturas recomendadas

  • The Turing Way Community (2022): The Turing Way, la guía comunitaria de ciencia reproducible que más se actualiza; si solo vas a abrir una referencia de este capítulo, que sea esta.

  • Sandve et al. (2013) y Wilson et al. (2017): el decálogo y las «prácticas suficientemente buenas»; se leen en una tarde y se aplican al día siguiente.

  • Pimentel et al. (2019): el millón de cuadernos que no volvió a ejecutarse; conviene tenerlo presente antes de compartir el tuyo.

  • Ushey y Wickham (2025) y Posit (2025a): las documentaciones oficiales de renv y de Air, las dos herramientas que definen el flujo de trabajo de 2026 en R.

  • Wickham (2019): Advanced R, la puerta al interior del lenguaje —objetos, evaluación, estilo— cuando este capítulo sepa a poco.

  • Ihaka y Gentleman (1996) y Chambers (2008): el acta de nacimiento de R y el ideario de S; pocas páginas que explican por qué el lenguaje es como es.

Referencias

Baker, Monya. 2016. «1,500 scientists lift the lid on reproducibility». Nature 533 (7604): 452-54. https://doi.org/10.1038/533452a.
Boettiger, Carl. 2015. «An introduction to Docker for reproducible research». ACM SIGOPS Operating Systems Review 49 (1): 71-79. https://doi.org/10.1145/2723872.2723882.
Chambers, John M. 2008. Software for Data Analysis: Programming with R. Springer. https://doi.org/10.1007/978-0-387-75936-4.
Ihaka, Ross, y Robert Gentleman. 1996. «R: A Language for Data Analysis and Graphics». Journal of Computational and Graphical Statistics 5 (3): 299-314. https://doi.org/10.1080/10618600.1996.10474713.
Knuth, Donald E. 1984. «Literate Programming». The Computer Journal 27 (2): 97-111. https://doi.org/10.1093/comjnl/27.2.97.
Peng, Roger D. 2011. «Reproducible Research in Computational Science». Science 334 (6060): 1226-27. https://doi.org/10.1126/science.1213847.
Pimentel, João Felipe, Leonardo Murta, Vanessa Braganholo, y Juliana Freire. 2019. «A Large-Scale Study About Quality and Reproducibility of Jupyter Notebooks». 2019 IEEE/ACM 16th International Conference on Mining Software Repositories (MSR), 507-17. https://doi.org/10.1109/MSR.2019.00077.
Posit. 2025a. «Air: An Extremely Fast R Formatter». https://posit-dev.github.io/air/.
Posit. 2025b. «Positron: The Data Science IDE». https://positron.posit.co/.
Preston-Werner, Tom. 2013. «Semantic Versioning 2.0.0». https://semver.org/.
Sandve, Geir Kjetil, Anton Nekrutenko, James Taylor, y Eivind Hovig. 2013. «Ten Simple Rules for Reproducible Computational Research». PLoS Computational Biology 9 (10): e1003285. https://doi.org/10.1371/journal.pcbi.1003285.
The Turing Way Community. 2022. The Turing Way: A handbook for reproducible, ethical and collaborative research. https://doi.org/10.5281/zenodo.3233853.
Ushey, Kevin, y Hadley Wickham. 2025. «renv: Project Environments». https://rstudio.github.io/renv/.
Wickham, Hadley. 2019. Advanced R. 2.ª ed. Chapman; Hall/CRC. https://adv-r.hadley.nz/.
Wickham, Hadley. 2025. «The tidyverse Style Guide». https://style.tidyverse.org/.
Wickham, Hadley, Mara Averick, Jennifer Bryan, et al. 2019. «Welcome to the tidyverse». Journal of Open Source Software 4 (43): 1686. https://doi.org/10.21105/joss.01686.
Wilkinson, Mark D., Michel Dumontier, IJsbrand Jan Aalbersberg, et al. 2016. «The FAIR Guiding Principles for scientific data management and stewardship». Scientific Data 3: 160018. https://doi.org/10.1038/sdata.2016.18.
Wilson, Greg, Jennifer Bryan, Karen Cranston, Justin Kitzes, Lex Nederbragt, y Tracy K. Teal. 2017. «Good enough practices in scientific computing». PLoS Computational Biology 13 (6): e1005510. https://doi.org/10.1371/journal.pcbi.1005510.