5  El paquete en el interior de su código

Esta parte del libro termina de la misma manera que comenzó, con el desarrollo de un pequeño paquete de juguete. Capítulo 1 estableció la mecánica básica, el flujo de trabajo y las herramientas para el desarrollo de paquetes, pero no dijo prácticamente nada sobre el código R dentro del paquete. Este capítulo se centra principalmente en el código R del paquete y en qué se diferencia del código R en un script.

A partir de un script de análisis de datos, aprenderá cómo encontrar el paquete que se esconde en su interior. Aislará y luego extraerá datos y lógica reutilizables del script, los colocará en un paquete R y luego usará ese paquete en un script mucho más simplificado. Hemos incluido algunos errores de novato a lo largo del camino para resaltar consideraciones especiales para el código R dentro de un paquete.

Tenga en cuenta que los encabezados de las secciones incorporan el alfabeto fonético de la OTAN (alfa, bravo, etc.) y no tienen un significado específico. Son simplemente una manera conveniente de marcar nuestro progreso hacia un paquete de trabajo. Está bien seguirlo simplemente leyendo y este capítulo es completamente autónomo, es decir, no es un requisito previo para el material posterior en el libro. Pero si desea ver el estado de archivos específicos a lo largo del camino, puede encontrarlos en archivos fuente del libro.

5.1 Alfa: un código que funciona

Consideremos data-cleaning.R, un script de análisis de datos ficticio para un grupo que recopila informes de personas que fueron a nadar:

¿Dónde nadaste y qué calor hacía afuera?

Sus datos generalmente vienen como un archivo CSV, como swim.csv:

name,where,temp
Adam,beach,95
Bess,coast,91
Cora,seashore,28
Dale,beach,85
Evan,seaside,31

data-cleaning.R comienza leyendo swim.csv en un data frame o marco de datos:

infile <- "swim.csv"
(dat <- read.csv(infile))
#>   name    where temp
#> 1 Adam    beach   95
#> 2 Bess    coast   91
#> 3 Cora seashore   28
#> 4 Dale    beach   85
#> 5 Evan  seaside   31

Luego clasifican cada observación en inglés americano (“EE.UU.”) o británico (“Reino Unido”), según la palabra elegida para describir el lugar arenoso donde se unen el océano y la tierra. La columna where se utiliza para construir la nueva columna english.

dat$english[dat$where == "beach"] <- "US"
dat$english[dat$where == "coast"] <- "US"
dat$english[dat$where == "seashore"] <- "UK"
dat$english[dat$where == "seaside"] <- "UK"

Lamentablemente, las temperaturas a menudo se informan en una combinación de grados Fahrenheit y Celsius. A falta de mejor información, suponen que los estadounidenses informan las temperaturas en grados Fahrenheit y, por lo tanto, esas observaciones se convierten a grados Celsius.

dat$temp[dat$english == "US"] <- (dat$temp[dat$english == "US"] - 32) * 5/9
dat
#>   name    where temp english
#> 1 Adam    beach 35.0      US
#> 2 Bess    coast 32.8      US
#> 3 Cora seashore 28.0      UK
#> 4 Dale    beach 29.4      US
#> 5 Evan  seaside 31.0      UK

Finalmente, estos datos limpios (¿más limpios?) se vuelven a escribir en un archivo CSV. Les gusta capturar una marca de tiempo en el nombre del archivo cuando hacen esto.1.

now <- Sys.time()
timestamp <- format(now, "%Y-%B-%d_%H-%M-%S")
(outfile <- paste0(timestamp, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile)))
#> [1] "2024-August-18_11-42-14_swim_clean.csv"
write.csv(dat, file = outfile, quote = FALSE, row.names = FALSE)

Aquí está data-cleaning.R en su totalidad:

infile <- "swim.csv"
(dat <- read.csv(infile))

dat$english[dat$where == "beach"] <- "US"
dat$english[dat$where == "coast"] <- "US"
dat$english[dat$where == "seashore"] <- "UK"
dat$english[dat$where == "seaside"] <- "UK"

dat$temp[dat$english == "US"] <- (dat$temp[dat$english == "US"] - 32) * 5/9
dat

now <- Sys.time()
timestamp <- format(now, "%Y-%B-%d_%H-%M-%S")
(outfile <- paste0(timestamp, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile)))
write.csv(dat, file = outfile, quote = FALSE, row.names = FALSE)

Incluso si sus tareas analíticas típicas son bastante diferentes, es de esperar que vea algunos patrones familiares aquí. Es fácil imaginar que este grupo realiza un preprocesamiento muy similar de muchos archivos de datos similares a lo largo del tiempo. Sus análisis pueden ser más eficientes y consistentes si ponen a su disposición estas maniobras de datos estándar como funciones en un paquete, en lugar de incorporar los mismos datos y lógica en docenas o cientos de scripts de ingesta de datos.

5.2 Bravo: un mejor código que funciona

¡El paquete que se esconde dentro del código original es bastante difícil de ver! Está oscurecido por algunas prácticas de codificación subóptimas, como el uso de código repetitivo estilo copiar/pegar y la mezcla de código y datos. Por lo tanto, un buen primer paso es refactorizar este código, aislando la mayor cantidad de datos y lógica posible en objetos y funciones adecuados, respectivamente.

Este también es un buen momento para introducir el uso de algunos paquetes complementarios, por varias razones. En primer lugar, utilizaríamos el tidyverse para este tipo de manipulación de datos. En segundo lugar, muchas personas utilizan paquetes complementarios en sus scripts, por lo que es bueno ver cómo se manejan los paquetes complementarios dentro de un paquete.

Aquí está la versión nueva y mejorada del script.

library(tidyverse)

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

lookup_table <- tribble(
      ~where, ~english,
     "beach",     "US",
     "coast",     "US",
  "seashore",     "UK",
   "seaside",     "UK"
)

dat <- dat %>% 
  left_join(lookup_table)

f_to_c <- function(x) (x - 32) * 5/9

dat <- dat %>% 
  mutate(temp = if_else(english == "US", f_to_c(temp), temp))
dat

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}
write_csv(dat, outfile_path(infile))

Los cambios clave a tener en cuenta son:

  • Estamos usando funciones de los paquetes tidyverse (específicamente de readr y dplyr) y las ponemos a disposición con library(tidyverse).
  • El mapa entre diferentes palabras de playa y si se consideran inglés de EE. UU. o Reino Unido ahora está aislado en una tabla de búsqueda, lo que nos permite crear la columna english de una sola vez con left_join(). Esta tabla de búsqueda hace que el mapeo sea más fácil de comprender y sería mucho más fácil ampliarlo en el futuro con nuevas palabras playa.
  • f_to_c(), timestamp(), y outfile_path() son nuevas funciones auxiliares que mantienen la lógica para convertir temperaturas y formar el nombre del archivo de salida con marca de tiempo.

Cada vez es más fácil reconocer los bits reutilizables de este script, es decir, los bits que no tienen nada que ver con un archivo de entrada específico, como swim.csv. Este tipo de refactorización a menudo ocurre naturalmente en el camino hacia la creación de su propio paquete, pero si no es así, es una buena idea hacerlo intencionalmente.

5.3 Charlie: un archivo separado para funciones auxiliares

Un siguiente paso típico es mover los datos y la lógica reutilizables del script de análisis a uno o más archivos separados. Este es un movimiento de apertura convencional, si desea utilizar estos mismos archivos auxiliares en múltiples análisis.

Aquí está el contenido de beach-lookup-table.csv:

where,english
beach,US
coast,US
seashore,UK
seaside,UK

Aquí está el contenido de cleaning-helpers.R:

library(tidyverse)

localize_beach <- function(dat) {
  lookup_table <- read_csv(
    "beach-lookup-table.csv",
    col_types = cols(where = "c", english = "c")
  )
  left_join(dat, lookup_table)
}

f_to_c <- function(x) (x - 32) * 5/9

celsify_temp <- function(dat) {
  mutate(dat, temp = if_else(english == "US", f_to_c(temp), temp))
}

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

Hemos agregado algunas funciones auxiliares de alto nivel, localize_beach() y celsify_temp(), A los ayudantes preexistentes (f_to_c(), timestamp(), y outfile_path()).

Aquí está la próxima versión del script de limpieza de datos, ahora que hemos eliminado las funciones auxiliares (y la tabla de búsqueda).

library(tidyverse)
source("cleaning-helpers.R")

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

(dat <- dat %>% 
    localize_beach() %>% 
    celsify_temp())

write_csv(dat, outfile_path(infile))

Observe que el código es cada vez más corto y, con suerte, más fácil de leer y modificar, porque el desorden repetitivo y complicado se ha eliminado de la vista. Si es realmente más fácil trabajar con el código es subjetivo y depende de qué tan natural se sienta la “interfaz” para las personas que realmente preprocesan los datos de natación. Este tipo de decisiones de diseño son objeto de un proyecto separado: design.tidyverse.org.

Supongamos que el grupo está de acuerdo en que nuestras decisiones de diseño son prometedoras, es decir, parece que estamos mejorando las cosas, no empeorando. Claro, el código existente no es perfecto, pero esta es una etapa de desarrollo típica en la que intentas descubrir cuáles deberían ser las funciones auxiliares y cómo deberían funcionar.

5.4 Delta: un intento fallido de hacer un paquete

Si bien este primer intento de crear un paquete terminará en un fracaso, sigue siendo útil analizar algunos errores comunes para iluminar lo que sucede detrás de escena.

Estos son los pasos más simples que puede seguir en un intento de convertir cleaning-helpers.R en un paquete en condiciones:

  • Utilice usethis::create_package("ruta/a/delta") para crear un scaffolding de un nuevo paquete R, con el nombre “delta”.
    • ¡Este es un buen primer paso!
  • Copie cleaning-helpers.R en el nuevo paquete, específicamente, para R/cleaning-helpers.R.
    • Esto es moralmente correcto, pero mecánicamente incorrecto en varios sentidos, como veremos pronto.
  • Copie beach-lookup-table.csv en el nuevo paquete. ¿Pero donde? Probemos el nivel superior del paquete fuente.
    • Esto no va a terminar bien. Los archivos de datos de envío en un paquete es un tema especial, que se trata en Capítulo 7.
  • Instale este paquete, quizás usando devtools::install() o mediante Ctrl + Shift + B (Windows y Linux) o Cmd + Shift + B en RStudio.
    • A pesar de todos los problemas identificados anteriormente, ¡esto realmente funciona! Lo cual es interesante, porque podemos (intentar) usarlo y ver qué sucede.

Aquí está la próxima versión del script de limpieza de datos que espera que se ejecute después de instalar exitosamente este paquete (al que llamamos “delta”).

library(tidyverse)
library(delta)

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

dat <- dat %>% 
  localize_beach() %>% 
  celsify_temp()

write_csv(dat, outfile_path(infile))

El único cambio con respecto a nuestro código anterior es que

source("cleaning-helpers.R")

ha sido reemplazado por

library(delta)

Esto es lo que realmente sucede si instala el paquete delta e intenta ejecutar el script de limpieza de datos:

library(tidyverse)
library(delta)

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

dat <- dat %>% 
  localize_beach() %>% 
  celsify_temp()
#> Error in localize_beach(.) : could not find function "localize_beach"

write_csv(dat, outfile_path(infile))
#> Error in outfile_path(infile) : could not find function "outfile_path"

¡Ninguna de las funciones auxiliares está realmente disponible para su uso, aunque llame a library(delta)! A diferencia de source(), al enviar un archivo de funciones auxiliares, adjuntar un paquete no volca sus funciones en el espacio de trabajo global. De forma predeterminada, las funciones de un paquete son sólo para uso interno. Necesita exportar localize_beach(), celsify_temp() y outfile_path() para que sus usuarios puedan llamarlos. En el flujo de trabajo de devtools, logramos esto poniendo @export en el comentario especial de roxygen encima de cada función (la administración del espacio de nombres se trata en Sección 11.3), así:

#' @export
celsify_temp <- function(dat) {
  mutate(dat, temp = if_else(english == "US", f_to_c(temp), temp))
}

Después de agregar la etiqueta @export a localize_beach(), celsify_temp() y outfile_path(), ejecuta devtools::document() para (re)generar el archivo NAMESPACE, y reinstale el paquete delta. Ahora, cuando vuelves a ejecutar el script de limpieza de datos, ¡funciona!

Corrección: más o menos funciona a veces. Específicamente, funciona si y sólo si el directorio de trabajo está configurado en el nivel superior del paquete fuente. Desde cualquier otro directorio de trabajo, sigue apareciendo un error:

dat <- dat %>% 
  localize_beach() %>% 
  celsify_temp()
#> Error: 'beach-lookup-table.csv' does not exist in current working directory ('/Users/jenny/tmp').

No se puede encontrar la tabla de búsqueda consultada dentro de localize_beach(). Uno no simplemente volca archivos CSV en el código fuente de un paquete R y espera que las cosas “simplemente funcionen”. Arreglaremos esto en nuestra próxima versión del paquete (Capítulo 7 tiene una cobertura completa sobre cómo incluir datos en un paquete).

Antes de abandonar este experimento inicial, maravillémonos también del hecho de que haya podido instalar, adjuntar y, hasta cierto punto, utilizar un paquete fundamentalmente roto. ¡devtools::load_all() también funciona bien! Este es un recordatorio aleccionador de que debería ejecutar R CMD check, probablemente a través de devtools::check(), muy a menudo durante el desarrollo. Esto le alertará rápidamente sobre muchos problemas que la simple instalación y uso no revelan.

De hecho, check() falla para este paquete y ves esto:

 * installing *source* package ‘delta’ ...
 ** using staged installation
 ** R
 ** byte-compile and prepare package for lazy loading
 Error in library(tidyverse) : there is no package called ‘tidyverse’
 Error: unable to load R code in package ‘delta’
 Execution halted
 ERROR: lazy loading failed for package ‘delta’
 * removing ‘/Users/jenny/rrr/delta.Rcheck/delta’

¿¡¿Qué quieres decir con “no hay ningún paquete llamado ‘tidyverse’”?!? ¡Lo estamos usando, sin problemas, en nuestro script principal! Además, ya hemos instalado y utilizado este paquete, ¿por qué R CMD check no puede encontrarlo?

Este error es lo que sucede cuando el rigor de “R CMD check” cumple con la primera línea de R/cleaning-helpers.R:

No es así como declaras que tu paquete depende de otro paquete (el tidyverse, en este caso). Así tampoco es cómo haces que las funciones de otro paquete estén disponibles para usar en el tuyo. Las dependencias deben declararse en DESCRIPTION (y eso no es todo). Como no declaramos dependencias, R CMD check nos toma la palabra e intenta instalar nuestro paquete solo con los paquetes base disponibles, lo que significa que esta llamada a library(tidyverse) falla. Una instalación “normal” tiene éxito, simplemente porque tidyverse está disponible en su biblioteca habitual, lo que oculta este error en particular.

Para revisar, copiar cleaning-helpers.R a R/cleaning-helpers.R, sin modificaciones adicionales, fue problemático en (al menos) las siguientes maneras:

  • No tiene en cuenta las funciones exportadas y no exportadas.
  • El archivo CSV que contiene nuestra tabla de búsqueda no se puede encontrar en el paquete instalado.
  • No declara adecuadamente nuestra dependencia de otros paquetes complementarios.

5.5 Echo: un paquete que funciona

Estamos listos para crear la versión mínima de este paquete que realmente funcione.

Aquí está la nueva versión de R/cleaning-helpers.R2:

lookup_table <- dplyr::tribble(
      ~where, ~english,
     "beach",     "US",
     "coast",     "US",
  "seashore",     "UK",
   "seaside",     "UK"
)

#' @export
localize_beach <- function(dat) {
  dplyr::left_join(dat, lookup_table)
}

f_to_c <- function(x) (x - 32) * 5/9

#' @export
celsify_temp <- function(dat) {
  dplyr::mutate(dat, temp = dplyr::if_else(english == "US", f_to_c(temp), temp))
}

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")

#' @export
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

Hemos vuelto a definir lookup_table con código R, ya que el intento inicial de leerlo desde CSV creó algún tipo de error en la ruta del archivo. Esto está bien para datos pequeños, internos y estáticos, pero recuerde consultar Capítulo 7 para conocer técnicas más generales para almacenar datos en un paquete.

Todas las llamadas a funciones de tidyverse ahora se han calificado con el nombre del paquete específico que realmente proporciona la función, por ejemplo. dplyr::mutate(). Hay otras formas de acceder a funciones en otro paquete, explicadas en Sección 11.4, pero esta es la opción predeterminada que recomendamos. También es nuestra fuerte recomendación que nadie dependa del metapaquete tidyverse en un paquete3. En cambio, es mejor identificar los paquetes específicos que realmente utiliza. En este caso, el paquete sólo utiliza dplyr.

La llamada library(tidyverse) desapareció y en su lugar declaramos el uso de dplyr en el campo Imports de DESCRIPTION:

Package: echo
(... other lines omitted ...)
Imports: 
    dplyr

Esto, junto con el uso de llamadas calificadas para espacios de nombres, como dplyr::left_join(), constituye una forma válida de utilizar otro paquete dentro del suyo. Los metadatos transmitidos a través de DESCRIPTION están cubiertos en Capítulo 9.

Todas las funciones orientadas al usuario tienen una etiqueta @export en su comentario de roxygen, lo que significa que devtools::document() las agrega correctamente al archivo NAMESPACE. Tenga en cuenta que f_to_c() actualmente solo se usa internamente, dentro de celsify_temp(), por lo que no se exporta (lo mismo ocurre con timestamp()).

Esta versión del paquete se puede instalar, usar y técnicamente pasa la verificación R CMD check, aunque con 1 advertencia y 1 nota.

* checking for missing documentation entries ... WARNING
Undocumented code objects:
  ‘celsify_temp’ ‘localize_beach’ ‘outfile_path’
All user-level objects in a package should have documentation entries.
See chapter ‘Writing R documentation files’ in the ‘Writing R
Extensions’ manual.

* checking R code for possible problems ... NOTE
celsify_temp: no visible binding for global variable ‘english’
celsify_temp: no visible binding for global variable ‘temp’
Undefined global functions or variables:
  english temp

La nota “sin enlace visible” es una peculiaridad del uso de dplyr y nombres de variables sin comillas dentro de un paquete, donde el uso de nombres de variables simples (english y temp) parece sospechoso. Puede agregar cualquiera de estas líneas a cualquier archivo debajo de R/ para eliminar esta nota (como el archivo de documentación a nivel de paquete descrito en Sección 16.7):

# opción 1 (entonces también deberías poner utilidades en Importaciones)
utils::globalVariables(c("english", "temp"))

# opción 2
english <- temp <- NULL

Estamos viendo que puede resultar complicado programar en torno a un paquete como dplyr, que hace un uso intensivo de evaluación no estándar. Detrás de escena, esa es la técnica que permite a los usuarios finales de dplyr usar nombres de variables simples (sin comillas). Paquetes como dplyr priorizan la experiencia del usuario final típico, a costa de hacer que sea más difícil depender de ellos. Las dos opciones que se muestran arriba para suprimir la nota “sin enlace visible” representan soluciones de nivel básico. Para un tratamiento más sofisticado de estos temas, ver vignette("in-packages", package = "dplyr") y vignette("programming", package = "dplyr").

La advertencia sobre la documentación faltante se debe a que las funciones exportadas no se han documentado adecuadamente. Esta es una preocupación válida y algo que absolutamente debes abordar en un paquete real. Ya has visto cómo crear archivos de ayuda con comentarios de roxygen en Sección 1.12 y cubrimos este tema a fondo en Capítulo 16.

5.6 Foxtrot: tiempo de construcción versus tiempo de ejecución

El paquete echo funciona, lo cual es fantástico, pero los miembros del grupo notan algo extraño en las marcas de tiempo:

Sys.time()
#> [1] "2023-03-26 22:48:48 PDT"

outfile_path("INFILE.csv")
#> [1] "2020-September-03_11-06-33_INFILE_clean.csv"

La fecha y hora en el nombre del archivo con marca de tiempo no refleja la hora informada por el sistema. De hecho, los usuarios afirman que la marca de tiempo nunca parece cambiar en absoluto. ¿Por qué es esto?

Recuerde cómo formamos la ruta del archivo para los archivos de salida:

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

El hecho de que capturemos now <- Sys.time() fuera de la definición de outfile_path() probablemente ha estado molestando a algunos lectores por un tiempo. now refleja el instante en el tiempo en el que ejecutamos now <- Sys.time(). En el enfoque inicial, se asignó now cuando source()d cleaning-helpers.R. Eso no es ideal, pero probablemente fue un error bastante inofensivo, porque el archivo auxiliar sería source()d poco antes de que escribiéramos el archivo de salida.

Pero este enfoque es bastante devastador en el contexto de un paquete. now <- Sys.time() es ejecutado cuando se construye el paquete4. Y nunca más. Es muy fácil asumir que el código de su paquete se vuelve a evaluar cuando se adjunta o utiliza el paquete. Pero no lo es. Sí, el código dentro de sus funciones se ejecuta absolutamente cada vez que se llaman. Pero sus funciones, y cualquier otro objeto creado en el código de nivel superior debajo de R/, se definen exactamente una vez, en el momento de la compilación.

Al definir now con el código de nivel superior debajo de R/, hemos condenado a nuestro paquete a marcar la hora de todos sus archivos de salida con la misma hora (incorrecta). La solución es asegurarse de que la llamada Sys.time() se realice en tiempo de ejecución.

Veamos nuevamente partes de R/cleaning-helpers.R:

lookup_table <- dplyr::tribble(
      ~where, ~english,
     "beach",     "US",
     "coast",     "US",
  "seashore",     "UK",
   "seaside",     "UK"
)

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

Hay cuatro asignaciones <- de nivel superior en este extracto. Las definiciones de nivel superior del marco de datos lookup_table y las funciones timestamp() y outfile_path() son correctas. Es apropiado que se definan exactamente una vez, en el momento de la construcción. La definición de nivel superior de now, que luego se usa dentro de outfile_path(), es lamentable.

Aquí hay mejores versiones de outfile_path():

# siempre marca de tiempo como "now"
outfile_path <- function(infile) {
  ts <- timestamp(Sys.time())
  paste0(ts, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

# permitir al usuario proporcionar una hora, pero de forma predeterminada "now"
outfile_path <- function(infile, time = Sys.time()) {
  ts <- timestamp(time)
  paste0(ts, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

Esto ilustra que es necesario tener una mentalidad diferente al definir objetos dentro de un paquete. La gran mayoría de esos objetos deberían ser funciones y estas funciones generalmente solo deberían usar datos que crean o que se pasan a través de un argumento. Hay algunos tipos de descuido que son bastante inofensivos cuando una función se define inmediatamente antes de su uso, pero que pueden ser más costosos para funciones distribuidas como un paquete.

5.7 Golf: efectos secundarios

Las marcas de tiempo ahora reflejan la hora actual, pero el grupo plantea una nueva preocupación. Tal como están las cosas, las marcas de tiempo reflejan quién ha realizado la limpieza de datos y en qué parte del mundo se encuentran. El corazón de la estrategia de marca de tiempo es esta cadena de formato5:

format(Sys.time(), "%Y-%B-%d_%H-%M-%S")
#> [1] "2024-August-18_11-42-15"

Esto formatea Sys.time() de tal manera que incluya el nombre del mes (no el número) y la hora local.6.

Tabla 5.1 muestra lo que sucede cuando varios colegas hipotéticos producen una marca de tiempo de este tipo limpiando algunos datos exactamente en el mismo instante.

Tabla 5.1: La marca de tiempo varía según la ubicación y la zona horaria.
location timestamp LC_TIME tz
Rome, Italy 2020-September-05_00-30-00 it_IT.UTF-8 Europe/Rome
Warsaw, Poland 2020-September-05_00-30-00 pl_PL.UTF-8 Europe/Warsaw
Sao Paulo, Brazil 2020-September-04_19-30-00 pt_BR.UTF-8 America/Sao_Paulo
Greenwich, England 2020-September-04_23-30-00 en_GB.UTF-8 Europe/London
“Computer World!” 2020-September-04_22-30-00 C UTC

Tenga en cuenta que los nombres de los meses varían, al igual que la hora e incluso la fecha. La opción más segura es formar marcas de tiempo con respecto a una ubicación y zona horaria fijas (presumiblemente las opciones no geográficas representadas por “Computer World!” arriba).

Investiga un poco y descubre que puede forzar una determinada configuración regional a través de Sys.setlocale() y forzar una determinada zona horaria configurando la variable de entorno TZ. Específicamente, configuramos el componente LC_TIME de la configuración regional en “C” y la zona horaria en “UTC” (Tiempo universal coordinado). Aquí está tu primer intento de mejorar. timestamp():

timestamp <- function(time = Sys.time()) {
  Sys.setlocale("LC_TIME", "C")
  Sys.setenv(TZ = "UTC")
  format(time, "%Y-%B-%d_%H-%M-%S")
}

Pero su colega brasileña nota que las fechas y horas se imprimen de manera diferente, antes y después de usar outfile_path() de su paquete:

Antes:

format(Sys.time(), "%Y-%B-%d_%H-%M-%S")
#> Warning in (function (category = "LC_ALL", locale = "") : OS
#> reports request to set locale to "pt_BR.UTF-8" cannot be honored
#> [1] "2024-August-18_08-42-15"

Después:

outfile_path("INFILE.csv")
#> [1] "2024-August-18_11-42-15_INFILE_clean.csv"

format(Sys.time(), "%Y-%B-%d_%H-%M-%S")
#> [1] "2024-August-18_11-42-15"

Observe que el nombre de su mes cambió de portugués a inglés y que la hora claramente se informa en una zona horaria diferente. Las llamadas a Sys.setlocale() y Sys.setenv() dentro de timestamp() han realizado cambios persistentes (y muy sorprendentes) en su sesión de R. Este tipo de efecto secundario es muy indeseable y extremadamente difícil de rastrear y depurar, especialmente en entornos más complicados.

Aquí hay mejores versiones de timestamp():

# use las funciones withr::local_*() para mantener los cambios locales en timestamp()
timestamp <- function(time = Sys.time()) {
  withr::local_locale(c("LC_TIME" = "C"))
  withr::local_timezone("UTC")
  format(time, "%Y-%B-%d_%H-%M-%S")
}

# utilizar el argumento tz para format.POSIXct()
timestamp <- function(time = Sys.time()) {
  withr::local_locale(c("LC_TIME" = "C"))
  format(time, "%Y-%B-%d_%H-%M-%S", tz = "UTC")
}

# poner la llamada format() dentro withr::with_*()
timestamp <- function(time = Sys.time()) {
  withr::with_locale(
    c("LC_TIME" = "C"),
    format(time, "%Y-%B-%d_%H-%M-%S", tz = "UTC")
  )
}

Estos muestran varios métodos para limitar el alcance de nuestros cambios a LC_TIME y la zona horaria. Una buena regla general es hacer que el alcance de dichos cambios sea lo más limitado y práctico posible. El argumento tz de format() es la forma más quirúrgica de tratar con la zona horaria, pero no existe nada similar para LC_TIME. Realizamos la modificación local temporal usando el paquete withr, que proporciona un conjunto de herramientas muy flexible para cambios de estado temporales. Esto (y base::on.exit()) se analizan con más detalle en Sección 6.5. Tenga en cuenta que si usa withr como lo hacemos arriba, deberá incluirlo en DESCRIPTION en Imports (Capítulo 11, Sección 10.1.3).

Esto subraya un punto de la sección anterior: es necesario adoptar una mentalidad diferente al definir funciones dentro de un paquete. Intente evitar realizar cambios en el estado general del usuario. Si dichos cambios son inevitables, asegúrese de revertirlos (si es posible) o documentarlos explícitamente (si están relacionados con el propósito principal de la función).

5.8 Pensamientos concluyentes

Finalmente, después de varias iteraciones, extrajimos con éxito el código de limpieza de datos repetitivos para la encuesta de natación en un paquete R. Este ejemplo concluye la primera parte del libro y marca la transición a material de referencia más detallado sobre componentes de paquetes específicos. Antes de continuar, repasemos las lecciones aprendidas en este capítulo.

5.8.1 Script versus paquete

Cuando escuche por primera vez que los usuarios expertos de R suelen poner su código en paquetes, es posible que se pregunte qué significa eso exactamente. Específicamente, ¿qué sucede con sus scripts R existentes, informes R Markdown y aplicaciones Shiny? ¿Todo ese código de alguna manera se coloca en un paquete? La respuesta es “no”, en la mayoría de los contextos.

Normalmente, identifica ciertas operaciones recurrentes que ocurren en múltiples proyectos y esto es lo que extrae en un paquete R. Seguirá teniendo scripts R, informes R Markdown y aplicaciones Shiny, pero al mover fragmentos de código específicos a un paquete formal, sus productos de datos tienden a volverse más concisos y más fáciles de mantener.

5.8.2 Encontrar el paquete dentro

Aunque el ejemplo de este capítulo es bastante simple, aún captura el proceso típico de desarrollo de un paquete R para uso personal u organizacional. Normalmente se comienza con una colección de scripts R idiosincrásicos y relacionados, repartidos en diferentes proyectos. Con el tiempo, empiezas a notar que ciertas necesidades surgen una y otra vez.

Cada vez que revises un análisis similar, puedes intentar mejorar un poco tu juego, en comparación con la iteración anterior. Refactoriza código de estilo copiar/pegar usando patrones más robustos y comienza a encapsular “movimientos” clave en funciones auxiliares, que eventualmente podrían migrar a su propio archivo. Una vez que llegue a esta etapa, estará en una excelente posición para dar el siguiente paso y crear un paquete.

5.8.3 El código del paquete es diferente.

Escribir código de paquete es un poco diferente a escribir scripts en R y es natural sentir cierta incomodidad al realizar este ajuste. Estos son los errores más comunes que nos hacen tropezar a muchos de nosotros al principio:

  • El código del paquete requiere nuevas formas de trabajar con funciones en otros paquetes. El archivo DESCRIPCIÓN es la forma principal de declarar dependencias; no hacemos esto a través library(somepackage).
  • Si desea que los datos o archivos estén disponibles de forma persistente, existen métodos de almacenamiento y recuperación específicos del paquete. No puedes simplemente poner archivos en el paquete y esperar lo mejor.
  • Es necesario ser explícito sobre qué funciones están orientadas al usuario y cuáles son ayudas internas. De forma predeterminada, las funciones no se exportan para que otras personas las utilicen.
  • Se requiere un nuevo nivel de disciplina para garantizar que el código se ejecute en el momento previsto (tiempo de compilación versus tiempo de ejecución) y que no haya efectos secundarios no deseados.

  1. Sys.time() devuelve un objeto de clase POSIXct, por lo tanto, cuando llamamos a format(), en realidad estamos usando format.POSIXct(). Lea la ayuda de ?format.POSIXct si no esta familiarizado con este formato de caracteres.↩︎

  2. Poner todo en un solo archivo, con este nombre, no es lo ideal, pero técnicamente está permitido. Discutimos cómo organizar y nombrar los archivos debajo de R/ en Sección 6.1.↩︎

  3. La publicación del blog El tidyverse es para EDA, no para paquetes detalla esto.↩︎

  4. Aquí nos referimos a cuándo se compila el código del paquete, que podría ser cuando se crea el binario (para macOS o Windows; Sección 3.4) o cuando el paquete se instala desde la fuente (Sección 3.5).↩︎

  5. Sys.time() devuelve un objeto de clase POSIXct, por lo tanto, cuando llamamos a format(), en realidad estamos usando format.POSIXct(). Lea la ayuda para ?format.POSIXct si no está familiarizado con dichas cadenas de formato.↩︎

  6. Claramente sería mejor formatear de acuerdo con ISO 8601, que codifica el mes por número, pero por favor, hazme el favor para que este ejemplo sea más obvio.↩︎