Creando un paquete de R: una guía informal

En el canal de Twitch de Psicometries hicimos un directo en el que explicamos cómo se ha creado el paquete de R {psicotuiteR}, indicando cada paso lo mejor que hemos podido para que puedas replicarlo, contribuir al mismo paquete o incluso crear tu propio paquete en el futuro.
r
package
twitter
tutorial
Author

Alicia Franco-Martínez

Published

Sunday, November 14, 2021

En este tutorial en colaboración con Alicia Franco-Martínez 1, explicaremos cómo se ha creado el paquete de R {psicotuiteR}. En el canal de Twitch de Alicia, Psicometries (al que también debéis suscribiros), hicimos un directo en el que intentamos mostrar por encima lo que trataré en este tutorial. Trataremos de explicar cada paso lo mejor que podamos para que puedas replicarlo, contribuir al mismo paquete o incluso crear tu propio paquete en el futuro.

1 Merece la pena seguir a Alicia en Twitter: hace muy buena divulgación sobre psicometría, estadística y R.

¿Qué es el paquete {psicotuiteR}?

El paquete psicotuiteR es un paquete muy simple que hemos creado para que la gente de la comunidad de #psicotuiter, en Twitter, pudiera experimentar con él añadiendo funciones o jugando con los datos que incluye. Podéis ver más información sobre el paquete en su página web. La comunidad es psicotuiter es un grupo de castellanoparlantes que hablan, entre otras cosas, sobre psicología y salud mental en Twitter.

¿Qué es un paquete de R?

En el manual R packages de Hadley Wickham y Jenny Bryan, se describe un paquete de R así:

In R, the fundamental unit of shareable code is the package. A package bundles together code, data, documentation, and tests, and is easy to share with others.

A mí me gusta definirlo como un grupo de funciones documentadas que se agrupan siguiendo el formato que el alto consejo jedi2 ha dictado.

2 La gente que gestiona CRAN, vaya. Tienen cierta fama de boomers.

Por cierto, R packages es el mejor recurso que existe en este momento para aprender a hacer paquetes de R. No hay nada en este tutorial que no esté incluido ahí–incluso mejor explicado–a excepción de algún que otro comentario autodespectivo al pie de página.

¿Por qué hacer un paquete de R?

Cuando programamos, es común que necesitemos ejecutar la misma línea de código varias veces. Cuando esto ocurre, en lugar de escribir y ejecutar la misma línea de código una y otra vez en la consola de R, podemos escribir un script. Un script de R es un archivo con la extensión .R3 que contiene las diferentes líneas de código que queremos ejecutar.

3 Por convención se suele usar la “R” mayúscula, aunque la mayoría de los sistemas operativos son indiferentes a que escribamos las extensiones en mayúsculas o minúsculas. Hagas lo que hagas, trata de ser consistente.

A veces el mismo script contiene líneas de código muy parecidas que ejecutamos para aplicar, por ejemplo, la misma función sobre objetos diferentes. Por ejemplo:

lm(x ~ y, data = df1)
lm(x ~ y, data = df2)

En el bloque de código de arriba, df1 y df2 son objetos de tipo data.frame con variables similares pero diferentes observaciones. Idealmente, no deberíamos repetir la misma línea de código más de una vez. Digo idealmente porque la alternativa es escribir una función. No siempre merece la pena hacer una función, por mucho que la gente más purista insista. Si estás leyendo esto doy por hecho que te interesa hacer tu código más conciso y replicable.

En todo caso es generalmente recomendable definir una serie de funciones antes de trabajar sobre tus datos. Una función es un conjunto de comandos que se ejecutan en orden cuando damos la orden4. Estos comandos se agrupan bajo el nombre que asignemos a la función. Así podemos condensar nuestro código en funciones para que sea más conciso.

4 Definición mediocre pero obligada. Lo siento.

En un último nivel de abstracción, nivel cerebro galáctico, está agrupar nuestras funciones en un paquete. Hacer esto tiene unos cuantos beneficios:

  • No tendremos que definir las funciones cada vez que abramos un script: bastará con cargar el paquete y sus funciones estarán disponibles para usarlas.
  • Será más fácil compartir nuestro código con otras personas: es común que nuestras funciones requieran, tener instalados ciertos paquetes externos. Por ejemplo, si mi función usa la función mutate del paquete dplyr, quien quiera usar mi función deberá tener instalado dplyr (a veces es inluso necesario tener instalada la misma versión del paquete). Este es uno de las principales amenazas a la reproducibilidad de nuestro código. Un paquete de R, sin embargo, se asegura que, durante la instalación, se instalen las dependencias necesarias en el ordenador de la otra persona,
  • Podremos documentar las funciones fácilmente (se acabaron los comentarios escuestos e indescifrables en nuestros scripts) y hacer esta documentación accesible a quien use nuestro paquete al ejecutar ?mifuncion (puedes ejecutar ?mean para ver un ejemplo de cómo se verá nuestra documentación).

Hay más motivos por los que puede ser buena idea crear un paquete de R, como por ejemplo trabajar con tu propio código de forma más cómoda y reproducible5, para compartir y documentar bases de datos (a veces nuestro paquete no incluirá ninguna función, sino únicamente unos datos y su documentación) o para aprender R más a fondo y sus entresijos. Yo he aprendido más intentando hacer paquetes de R que en cualquer tutorial.

5 Tu yo futuro te lo agradecerá y si algún día se puede viajar en el tiempo lo harás para darte un beso en la frente por ello.

¿Qué necesito para hacer un paquete de R?

Para seguir este tutorial, y en general para crear un paquete de R necesitaremos instalar en nuestro ordenador6:

6 En el momento de escribir este tutorial yo estoy usando R 4.0.4, RStudio 1.4.1103, rmarkdown 2.11.1, devtools 2.4.2 y usethis 2.0.1. Echa un vistazo a esta guía para ver cómo instalar una versión específica de un paquete de R.

Puedes instalar estos paquetes así:

install.packages("devtools", "usethis", "rmarkdown")

Cuando tengamos todo instalado, reiniciaremos nuestra sesión de RStudio y después cargaremos devtools y usethis (usaremos rmarkdown más adelante):

library(devtools)
library(usethis)

Inicializando el paquete

Salvo contadas excepciones7, las personas de bien utilizaremos RStudio para programar en R, en lugar de usar únicamente la consola como hacen les psicópatas. Trabajar en un proyecto de RStudio nos facilitará mucho la vida al hacer un paquete de R y deberías hacerlo casi siempre que programas en R (como mínimo, te ahorrará mucho tiempo buscando documentos dentro de carpetas)8.

7 Por ejemplo, si tienes el suficiente tiempo libre como para configurar una sesión de R medianamente funcional en Visual Studio Code.

8 Nunca está demás ver este tutorial de Danielle Navarro sobre cómo organizar un repositorio

Para crear un paquete tenemos dos opciones: hacerlo a través de RStudio (ver tutorial) o usando usethis en nuestra consola. A mi me gusta la segunda opción:

create_package(path = "psicotuiteR") # abre nueva ventana

Este comando creará una nueva carpeta. Lo hará dentro de tu repositorio de inicio, el cual puedes consultar ejecutando getwd() en tu consola (asumiendo que nos has cambiado el directorio por tu cuenta previamente). Esta carpeta contendrá varios archivos y una carpeta:

.gitignore
.Rbuildignore
DESCRIPTION
NAMESPACE
psicotuiteR.Rproj
R
Important

Los nombres de la mayoría de archivos y carpetas en este directorio son importantes. Trata de no cambiarlos si no es imprescindible.

Uno de los archivos en la carpeta tiene el mismo nombre que el paquete y la extensión .Rproj. Cada vez que queramos trabajar en nuestro paquete es recomendable abrir la sesión de RStudio haciendo doble click sobre el archivo .Rproj. Veremos qué son el resto de archivos más adelante.

Pues bien: técnicamente, ¡ya tenemos un paquete de R! Si ejecutamos el código de abajo, el paquete se instalará como si fuera uno más en nuestro directorio de paquetes de R.

devtools::install()

Encontrarás la carpeta de instalación junto a la de los demás paquetes que hays instalado en tu ordenador. Puedes consultar dónde se instalan tus paquetes de R ejecutando .libPaths() en tu consola. La primera ruta que aparezca será donde encontrarás tu paquete instalado (en mi caso, encontraré la carpeta ~/Documents/R/win-library/4.0/psicotuiteR).

La función que hemos ejecutado, install() del paquete devtools simula lo que otra persona haría al ejecutar la función install.packages() si nuestro paquete estuviera disponible en CRAN. Ahora mismo, podríamos cargar nuestro paquete con library(psicotuiteR) y trabajar con él. Por supuesto, nuestro paquete aún está vacío. En las próximas secciones veremos cómo añadir funciones, entre otras cosas, a nuestro paquete.

Antes de hacer eso, ahí va un truco: cuando añadimos o hacemos cambios en el paquete, necesitaremos actualizar nuestra sesión y cargar el paquete de nuevo y volver a ejecutar las funciones para comprobar que todo funciona correctamente. En lugar de ejecutar install() cada vez que queremos ver si nuestro paquete funciona, podemos ejecutar load_all() (también del paquete devtools) sin siquera reiniciar la sesión y así cargar de nuevo el paquete actualizado como si alguien hubiera cargado el paquete usando library(). Los contenidos del paquete que se cargarán usando load_all() son los que hay en el nuestro repositorio (desde el que estamos desarrollando el paquete), y no desde el directorio en el que se instala el paquete si ejecutamos install(). ¡Esto es mucho más rápido y eficiente!

Otro truco: puesto que vamos a utilizar las funciones de los paquetes devtools y usethis a menudo–y por lo tanto vamos a necesitar cargar estos paquetes mediante library(devtools) y library(usethis) cada vez que iniciemos una sesión de R– podría ser recomendable añadir esos dos comandos a nuestro archivo .Rprofile (ver esta guía para más información sobre .Rprofile. Las líneas de código que contenga ese archivo serán ejecutadas en cada inicio de sesión que hagamos en nuestro proyecto de RStudio. Podemos hacer esto usando las siguientes funciones de usethis:

library(usethis)
use_usethis() 
use_devtools()

Esto creará un archivo llamado .Rprofile en nuestro directorio y añadirá, entre otras cosas muy útiles, los comandos library(usethis) y library(devtools).

DESCRIPTION

El primer archivo que vamos a explicar se llama DESCRIPTION. La mayor parte de la información general (o meta-información) de nuestro paquete se encuentra en este archivo: autoría, ajustes generales, dependencias, etc. Puedes consultar el DESCRIPTION de psicotuiteR para hacerte una idea de cómo se ve cuando está editado. ¡Presta especial atención a cómo hemos indicado la autoría!

DESCRIPTION es uno de los pocos archivos que tendremos que editar tanto a mano como mediante otras funciones del paquete usethis. Tendremos que cambiar el título, autoría y descripción del paquete a mano (además de algún otro campo), mientras que, por ejemplo, los campos Imports, Depends y Suggests serán rellenados más adelante mediante la función use_package(), de usethis. Cuando hablemos de dependencias, más adelante, comentaremos un par de cosas saobre estos tres campos.

Funciones en R

Las funciones de R son el cuerpo principal de un paquete de R. Contienen el código que se ejecutará en nuestras funciones y su correspondiente documentación. Las funciones de por sí no tienen gran misterio. Las puedes hacer más simples o más complejas. Generalmente, por motivos de legibilidad suele ser mejor mantener nuestras funciones lo más simples que podamos. Es mejor escribir muchas funciones que hacen tareas pequeñas que pocas funciones que lo hacen todo. No tienes por qué hacer disponibles todas las funciones que escribas: puedes mantener algunas funciones para uso interno dentro de otras funciones que sí están disponibles al público (ahora veremos cómo). Hay muchos tutoriales para aprender a hacer funciones en R. Por ejemplo este. Merece la pena ganar algo de confianza en poder hacer funciones de R: empodera mucho y te hace entender muchos de los errores que te encontrarás a lo largo de tu vida programando en R. Ahí va un ejemplo de función muy simple:

print_name <- function(
    author = "Gon"
){
    print(author)
}

Esas líneas de código definen la función print_name(). Esta función incluye un argumento llamado author, que, por defecto, toma el valor "Gon". Si definimos la función y ejecutamos print_name() en nuestra consola de R, nos devolverá el valor `“Gon”.

Guardaremos esta función en un archivo con la extensión .R dentro de la carpeta R/ en nuestro repositorio principal (el nombre que pongamos a este archivo da igual, pero trata de que sea informativo de su contenido). Yo llamaré a este archivo print_name.R. Ahora nuestro directorio se ve así:

.gitignore
.Rbuildignore
DESCRIPTION
NAMESPACE
psicotuiteR.Rproj
R
    |-print_name.R

Documentación

Ahora vamos a documentar esa función. R utiliza un tipo de lenguaje parecido a LaTeX para escribir la documentación de un paquete. Este lenguaje se llama R documentation. Técnicamente, podríamos escribir toda la documentación de cada función de nuestro paquete en este lenguaje y guardar cada archivo en la carpeta man/ con la extensión .Rd. En esta carpeta es donde R espera encontrar la documentación. Gracias a Dios (y a la buena voluntad de la comunidad de R), podemos incluir los contenidos de esos archivos como si fueran comentarios en nuestros scripts de R, encima de cada función. La función document() de devtools se encargará de generar todos los .Rd necesarios en la carpeta man/ por nosotrxs. Volveremos a esto más adelante. Por ahora, observa un ejemplo de función documentada:

#' Print author
#' @export print_name
#' @usage print_author()
#' @import dplyr
#' @importFrom tidyr drop_na
#' @description Print the name of the author of the package we are developing
#' @param author Name of the package author
#' @author Gonzalo Garcia-Castro
print_name <- function(
    author = "Gon"
){
    print(author)
}

En la parte de abajo encontramos las mismas líneas de código que hemos visto antes. En la parte de arriba encontramos la documentación de la función. Estas líneas de código están precedidas por el símbolo #'. La apóstrofe indica que no es un comentario cualquiera, sino parte de la documentación de la función. En la mayoría de estas líneas indicamos mediante el símbolo @ qué campo estamos describiendo (autoría, descripción, un argumento, etc.).

Por ejemplo, la línea de abajo indica que estamos describiendo uno de los argumentos de la función (por alguna razón que desconozco, se ha decidido refererirse a los argumentos como “@param” y no como @arg“). La primera línea se asume que es el título de la función y por eso no hace falta indicar @title antes de "Print author".

#' @param author Name of the package author

Una vez hayamos rellenado la documentación de nuestra función, ejecutaremos document() y se generará automáticamente un archivo llamado print_name.Rd en la carpeta man/. Podemos comprobar que la documentación se ha guardado correctamente ejecutando ?print_name. Se debería abrir la ventana Help en uno de los paneles de RStudio. Algunos consejos cuando rellenes la documentación de tus funciones:

  • Explica las cosas con claridad, pero no tengas miedo de extenderte o repetir las cosas. Más documentación siempre es mejor que menos documentación, siempre que se expliquen las cosas con claridad y se mantenga cierto sistema en la estructura de la documentación.
  • Echa un vistazo a la documentación de las funciones princpiales de tus paquetes favoritos. Es la mejor forma de saber cómo documentar un función y qué campos son los más importantes.

Viñetas y artículos

Una forma más elaborada de documentar un paquete es crear viñetas. Una viñeta (o vignette, en inglés) es un documento en el que se explica con detalle cómo se trabaja con el paquete, como si fuera un tutorial. Las viñetas son particularmente útiles para quienes usan el paquete por primera vez, y deberían ilustrar, como mínimo, algún ejemplo sobre cómo se usan las funciones. Un buen ejemplo de viñeta es esta, del paquete {dplyr}, en la que indican cómo usar la función group_by(). Para crear una viñeta ejecutaremos el siguiente comando:

use_vignette(
  name = "print-name" # así se llamará el archivo,
  title = "Imprimiendo un nombre" # así se titulará la viñeta
)

Este comando creará una carpeta llamada vignettes/ y creará un archivo con la extensión .Rmd (Rmarkdown). Los archivos Rmarkdown son una mezcla entre un archivo Markdown (que tienen la extensión .md) y un script de R. Markdown es un lenguaje de edición de textos bastante sencillo (desde luego más sencillo que LaTeX). Rmarkdown permite intercalar bloques de código de R en medio del texto. Cuando renderizamos el documento, esos bloques de código se ejecutan y el resultado se incluye como texto o imagen. Este tipo de documentos son muy útiles para crear informes y actualizar los datos que incluyen con sólo un click. Si quieres aprender a user Rmarkdown (100% recomendado), el manual R Markdown: The Definitive Guide de Yihui Xie es el mejor recurso. Una vez escribamos nuestra viñeta podremos incluirla en nuestra documentación ejecutando:

build_vignettes()
document()

Como dato curioso, este mismo tutorial que estás leyendo es una viñeta incluida en el paquete psicotuiteR. Muy meta todo, ¿verdad?

Dependencias y otras pesadillas

Con frecuencia nuestras funciones de R dependerán, a su vez, de funciones de otros paquetes. Considera la siguiente función:

#' Una función que añade una variable
#' @param x Una serie de caracteres
#' @returns El número incluido en x
extrae_numeros <- function(x){
  y <- as.numeric(str_extract(x, "\\d"))
  return(y)
}

Como indica la documentación, esta función devuelve los números incluidos en x, una serie de caracteres que introducimos como argumento, en formato numérico. Para ello utiliza la función str_extract(). Esta función pertenece al paquete {stringr}. Tal y como está escrita esta función, cuando instalemos el paquete y la ejecutemos no dará un error: R nos indicará que la función str_extract no existe. Podría ocurrírsenos usar algo como stringr::str_extract o library(stringr) dentro de la función. Pero esto no es buena idea porque muchas veces no funcionará. Necesitamos incluir la función str_extract y stringr como dependencias de nuestro paquete, para que cuando alguien instale el paquete esa función se encuentre disponible para el paquete cuando ejecutemos nuestra función extrae_numeros(). Para indicar una dependencia, debemos hacerlo en dos pasos:

  1. Ejecutar use_package("stringr") para incluir stringr en la lista de paquetes que deben instalarse junto al nuestro. Al hacerlo, verás que stringr ha sido incluido en el campo Imports del archivo DESCRIPTION.
  2. Incluir la función str_extract del paquete stringr en la documentación de la función de la siguiente forma:
#' @importFrom stringr str_extract

Ahora nuestra función debería verse así:

#' Una función que añade una variable
#' @param x Una serie de caracteres
#' @importFrom stringr str_extract
#' @returns El número incluido en x
extrae_numeros <- function(x){
  y <- as.numeric(str_extract(x, "\\d"))
  return(y)
}

La función str_extract estará disponible y nuestra función podrá ejecutarse sin problemas. Otra opción sería importar el paquete stringr al completo, con todas sus funciones. En lugar de indicar (importFrom?) con el nombre del paquete y la función que queremos importar, indicaríamos @import y solamente nombre del paquete: #' @import stringr. Sin embargo, casi siempre es mejor indicar las dependencias una a una, aunque sea repetitivo: así será más fácil detectar de dónde viene cada función que usamos. Si vamos a usar varias funciones del mismo paquete, podemos indicarlas una debajo de otra:

#' @importFrom stringr str_extract
#' @importFrom stringr str_detect
#' @importFrom stringr str_replace

Sólo en el caso de que utilicemos muchísimas funciones del mismo paquete en nuestra función tendrá cierto sentido importar el paquete al completo. Las dependencias de nuestro paquete sólo estará disponibles cuando ejecutemos document() y se actualice la documentación. Esto es porque document() no sólo construye la documentación del paquete (los archivos .Rd en man/), sino que también modifica el archivo NAMESPACE para enumerar las dependencias. Echa un vistazo a NAMESPACE de psicotuiteR. Este archivo ha sido generado al ejecutar document(). Una de nuestras funciones incluye #' @import janitor clean_names y por tanto esta función ha sido incluida en el NAMESPACE.

Hemos aprendido a indicar dependencias de otros paquetes en nuestro código através de la documentación: funciones que nuestras propias funciones necesitan. Para hacer nuestras funciones disponibles en el paquete, necesitaremos incluirlas también en el NAMESPACE, pero no como dependencias, sino como exportaciones: en lugar de incluirlas en la documentación usando @importFrom, lo haremos usando #' @export. Volviendo al ejemplo anterior: #' @export extrae_numeros. Nuestra función se ver ahora así:

#' Una función que añade una variable
#' @export extrae_numeros
#' @param x Una serie de caracteres
#' @importFrom stringr str_extract
#' @returns El número incluido en x
extrae_numeros <- function(x){
  y <- as.numeric(str_extract(x, "\\d"))
  return(y)
}

La segunda línea indica que está función debe estar disponible en nuestro paquete con el nombre extrae_numeros. Aunque es posible exportar la función con un nombre diferente al que le hemos asignado en el script (R nos dará un aviso, pero no un error), es importante que así sea.

Podrías preguntarte: ¿qué sentido tiene crear funciones si no van a estar disponibles para quien use el paquete? Pues porque a veces es recomendable hacer funciones pequeñas para uso interno que, aunque su funcion no es de gran interés para el público, ayuden a otras funciones más complejas que sí tienen sentido que use el público. Sea como sea, técnicamente sí se puede acceder a este tipo de funciones usando el operador :::.

Si quieres hacer la prueba, ve a la consola de R y compara las sugerencias que salen cuando escribes usethis:: y usethis:::. Todas esas funciones que salen en el segundo caso no están disponibles cuando cargamos el paquete usando library() porque las personas que han creado el paquete usethis han considerado que no las necesitamos, aunque sí las necesitan las funciones que usamos.

En resumen, cuando ejecutemos document(), se incluirán en el NAMESPACE las funciones que hayamos indicado en la documentación mediante #' @export. Nunca modificaremos NAMESPACE archivo a mano, sino que cambiaremos sus contenidos modificando la documentación de nuestras funciones en el script de R y luego ejecutaremos document(). Con toda probabilidad, la mayoría de los problemas que vas a encontrar al desarrollar un paquete de R (también los más frustrantes) se deban a sus dependencias. Con el tiempo aprenderás a detectar estos problemas y entender por qué ocurren. Te damos un abrazo por adelantado: been there, done that.

Hay dos dependencias un poco especiales: si usamos pipes, “pipas”–o como quiera Dios que se traduzca al español–en nuestras funciones, en lugar de indicar # @importFrom dplyr %>% en cada función, podremos ejecutar use_pipe() y esta función se encargará de incluir esta dependencia en la documentación por nosotros. Lo hará en un script llamado con el nombre de nuestro paquete, en la carpeta R/, que creará automáticamente. Si alguna de nuestras funciones devuelve un objeto en forma de data.frame y queremos usar la función tibble, del paquete tibble para que quede mejor, también podemos usar la función use_tibble(), que hará lo mismo que use_pipe(), aladiendo tibble a la lista de dependencias.

Por último, es recomendable mantener las dependencias de paquetes al mínimo. Cada vez que incluimos una dependencia corremos el riesgo de que alguno de los paquetes de los que dependemos cambie de versión y nuestro paquete deje de funcionar porque una de las funciones de ese paquete que utilizamos ha cambiado. CRAN impone un límite de dependencias en 20.

Datos internos, datos externos, datos brutos

Muchos paquetes incluyen objetos del tipo data.frame. Por ejemplo, el paquete {dplyr} contiene el objeto starwars. Este objeto es un data.frame con información sobre muchos personajes del universo de Star Wars. Podemos acceder a este objeto así dplyr::starwars.

# A tibble: 6 × 14
  name         height  mass hair_…¹ skin_…² eye_c…³ birth…⁴ sex   gender homew…⁵
  <chr>         <int> <dbl> <chr>   <chr>   <chr>     <dbl> <chr> <chr>  <chr>  
1 Luke Skywal…    172    77 blond   fair    blue       19   male  mascu… Tatooi…
2 C-3PO           167    75 <NA>    gold    yellow    112   none  mascu… Tatooi…
3 R2-D2            96    32 <NA>    white,… red        33   none  mascu… Naboo  
4 Darth Vader     202   136 none    white   yellow     41.9 male  mascu… Tatooi…
5 Leia Organa     150    49 brown   light   brown      19   fema… femin… Aldera…
6 Owen Lars       178   120 brown,… light   blue       52   male  mascu… Tatooi…
# … with 4 more variables: species <chr>, films <list>, vehicles <list>,
#   starships <list>, and abbreviated variable names ¹​hair_color, ²​skin_color,
#   ³​eye_color, ⁴​birth_year, ⁵​homeworld

Este objeto está documentado: si ejecutamos ?dplyr::starwars se abrirá el panel de documentación de RStudio. Este tipo de objetos son muy útiles para mostrar ejemplos de uso de las funciones de un paquete. Algunos paquetes incluso han sido diseñados con el único propósito de compartir una base de datos documentada, como por ejemplo el paquete {palmerpenguins}, que apenas contiene el objeto penguins: una base de datos sobre la anatomía demlos pingüinos de la Isla de Palmer. En esta sección veremos cómo incluir una base de datos como ésta en nuestro paqeute de R y cómo documentarla.

Datos internos y externos

Podemos incluir un data.frame por defecto en nuestro paquete de dos formas: como un objeto interno o como un objeto externo. Hacerlo de la primera forma es el equivalente a crear una función sin exportarla al NAMESPACE: nos sirve para utilizarla dentro de las funciones internas del paquete, pero no será inmediatamente visible para quien cargue el paquete usando library (aunque igualmente podrá hacerlo usando el operador :::). Cuando incluimos el data.frame como objeto externo, sí se podrá acceder a él cuando cargemos el paquete (o mediante el operador ::).

Para guardar un data.frame de cualquiera de las dos formas, primero debemos tenerlo definido entre las variables de nuestra sesión de R. Por ejemplo, supón que hemos definido el data.frame clima (disponible en el paquete psicotuiteR):

# A tibble: 6 × 37
     id lugar  genero   psico1 psico2 psico3 tw1   tw2   tw_pers tw_di…¹ tw_an…²
  <int> <chr>  <chr>    <chr>   <int>  <int> <chr> <chr>   <int>   <int>   <int>
1     1 Europa Mujer    Posgr…      3      4 Mas … Vari…       4       3       0
2     2 Europa Hombre   Posgr…      1      5 Mas … Vari…       4       2       3
3     3 Europa No bina… Posgr…      4      3 Meno… Vari…       4       2       1
4     4 Europa Hombre   Posgr…      4      3 Mas … Vari…       5       5       0
5     5 Europa Hombre   Posgr…      5      1 Mas … Vari…       4       2       3
6     6 Europa Hombre   Profe…      5      3 Mas … Vari…       1       4       0
# … with 26 more variables: tw_tuits <int>, tw_hilos_div <int>,
#   tw_hilos_ot <int>, tw_rt <int>, tw_like <int>, tw_resp <int>,
#   exp_tw1 <int>, exp_tw2 <int>, exp_tw3 <int>, exp_tw4 <int>, comp1 <chr>,
#   comp2_verg <int>, comp2_quediran <int>, comp2_calidad <int>,
#   comp2_error <int>, comp2_conf <int>, comp2_nunca <int>, comp3_divulg <int>,
#   comp3_apren <int>, comp3_deb_cien <int>, comp3_deb_poli <int>,
#   comp3_pers <int>, comp3_nunca <int>, psico_tw1 <chr>, psico_tw2 <int>, …

Utilizaremos la función use_data() del paquete usethis para incluirlo en nuestro paquete. Si queremos exportarlo al NAMESPACE especificaremos internal = FALSE en los argumentos de la función (opción por defecto). Si no queremos exportarlo especificaremos internal = TRUE:

use_data(clima) # objeto externo
use_data(clima, internal = TRUE) # objeto interno

Esta función creará (en caso de que no exita ya) una carpeta en nuestro directorio llamada data/ y guardará el data.frame como un archivo .rds (como un objeto de R), en nuestro caso lo guardará con el nombre clima.rds. En el caso de que el objeto ya exista, debemos incluir overwrite = TRUE en los argumentos para que nos permita sobreescribirlo.

use_data() también creará un archivo llamado clima.R en la carpeta R. Como la extensión indica, este archivo es un script de R como el que usamos para las funciones. Debemos documentar nuestros datos en este archivo usando Roxigen, tal y como hacemos para las funciones. Echa un vistazo al archivo clima.R del paquete psicotuiteR. Verás que sólo contiene documentación, excepto por la línea final, que contiene "clima". No necesita nada más que el nombre del objeto bajo el cual exportaremos el data.frame. De lo demás se encargará, como siempre la función document().

Datos brutos

Hay una forma más de incluir datos, aun más reproducible que la anterior: incluir los datos brutos, como un archivo .csv, .txt, .tsv, .xlsx, etc. También podemos incluir en nuestro paquete el script de R con el código de que hemos usando para procesar los datos contenidos en el archivo y para llegar a la forma final del objeto que guaradamos mediante use_data(). La función use_raw_data() del paquete usethis se encarga de esto.

use_raw_data()

Esta función creará dos carpetas (en caso de que no existan ya): inst/ y data-raw/. La carpeta inst/ (abreviatura de installation) de un paquete de R incluye archivos externos (por ejemplo, .txt, scripts de Python, Stan, C++…) que queremos que estén disponibles cuando alguien cargue el paquete, pero no son archivos que normalmente se incluyan en un paquete. Es recomendable guardar el archivo con nuestros datos brutos en la carpeta inst/. En la carpeta data-raw/ se creará un archivo con la extensión .R. En este archivo (que se abrirá automáticamente cuando ejecutemos use_data_raw()) escribiremos el código que procesa los datos y los deja como queremos que se guarden en el paquete. La última línea del script (incluida por defecto) guarda el objeto resultante como datos externos mediante la función use_data(). Así, cada vez que queramos actualizar el objeto, sólo tendremos que ejecutar este código.

Archivos externos e inst/

Como hemos mencionado, cualquier archivo que queramos incluir en nuestro paquete y no tenga la extensión .R o .rds, debería estar dentro de la carpeta inst/. Cuando alguien instale el paquete, los archivos de esta carpeta se moverán al directorio principal del paquete. Puedes hacer la prueba ejecutando la función install() de devtools. Cuando lo hayas hecho ve a la carpeta del paquete (recuerda que los paquetes se instalan en la primera ruta que indique .libPaths()). Cualquier archivo que hayas dejado en la carpeta inst/ aparecerá en el directorio principal ahora. A veces querremos acceder a estos archivos dentro de nuestras funciones. Esto puede ser un poco complicado. La práctica más recomendable es hacerlo usando la función:

system.file("clima.csv", package = "psicotuiteR")`

En la línea de código anterior, estaremos accediendo al archivo clima.csv, que hemos incluido en la carpeta inst/, pero que al instalar el paquete se encontrará en ~/Documents/R/win-library/4.0 (al menos en mi ordenador).

Important

Si la función no encuentra ese archivo devolverá "", en lugar de un error. Esto puede llevar a confusión. Si quieres que la función devuelva un error si no encuentra el archivo, añade mustWork = TRUE a los argumentos:

system.file("clima.csv", package = "psicotuiteR", mustWork = TRUE)`

Manteniendo y compartiendo el paquete

Git y GitHub

Un paquete de R puede volverse complejo en poco tiempo: scripts con muchas funciones, funciones que dependen de otras funciones, funciones dependen de funciones de otros paquetes… Es fácil liarla. Git es una buena herramienta para controlar cómo va cambiando el paquete. Te permite llevar la cuenta de cómo ha cambiado cada archivo dentro del paquete, poder volver a una versión específica del mismo archivo, o tener diferentes versiones del mismo paquete funcionando a la vez de forma independiente. Este último punto es especialmente útil si queremos “jugar” con una versión de prueba del paquete mientras otras personas se pueden descargar una versión estable del mismo. Todas estas utilidades se conocen como control de versiones, una versión sofisticada y menos cortoplacista de crear un millón de archivos de similar contenido y nombres incrementalmente más creativos con el fin de no perder contenido.

Por otro lado, GitHub es una red social que permite almacenar y compartir repositorios mediante control de versiones mediante Git9. Varias personas pueden acceder al repositorio cuando está alojado en GitHub (un paquete de R, en nuestro caso) y sugerir cambios como si estuvieran trabajando sobre dicho paquete en un sólo ordenador. Por desgracia, el uso de git o GitHub está fuera del alcance de este tutorial por varios motivos10. El mejor manual que conozco (y también el más accesible) para aprender a usar Git y GitHub (especialmente para quien ya trabaja en R) es Happy Git and GitHub for the useR, de Jenny Bryan. Aprender Git no siempre es fácil pero siempre merece la pena11.

9 No es la única plataforma disponible para hacer esto: Gitlab y Bitbucket, entre otras, hacen lo mismo, aunque son menos populares. Además, por si esto calma alguna conciencia inquita, no son propiedad de Microsoft, al contrario de GitHub.

10 El primero de todos siendo que no tengo la confianza suficiente como para hacerlo (yo mismo la lío con Git cada día). El segundo es que aunque stuviera esa confianza, me daría infinita pereza hacerlo. Hacedme caso y echad un vistazo el libro que recomiendo.

11 En mi honesta, humilde, ignorable opinión.

12 Más que un paquete de R, devtools es una collección de funciones de otros paquetes que han sido agrupadas por su utilidad a la hora de desarrollar paquetes (“devtools” es la abreviatura de developer tools). La función install_github pertenece, originalmente, al paquete {remotes}

Git y GitHub cumplen una función muy especial para quienes hacemos un paquete en R: la función install_github, del paquete {devtools}12, permite instalar un paquete sin necesidad de que esté publicado en CRAN. Hablaremos en otro tutorial sobre CRAN, pero por ahora nos interesa saber que podemos compartir cualquier paquete a través de GitHub usando install_github, pero para poder instalarlo usando install.packages, como normalmente hacemos, ese paquete necesita estar publicado en CRAN. Publicar en CRAN requiere un proceso de revisión que en ocasiones es díficil solventar (y a veces innecesario). Para compartir nuestro paquete sin necesidad de pasar por ese calvario, lo haremos a través de GitHub.

Para hacerlo, primero debemos crear un usuario de GitHub, crear un nuevo repositorio, hacer click en el botón verde que dice Code y finalmente copiar el enlace que aparece.

En nuestra sesión de R, ejecutamos las siguientes líneas de código:

use_git() # esta línea inicializa Git en el repositorio
use_git_remote(name = "origin", url = "https://github.com/gongcastro/psicotuiteR.git") # sustituye ese link por el que hayas copiado de GitHub
use_github_ignore() # crea un archivo llamado .gitignore que indica a Git qué archivos ignorar
git_vaccinate() # añade más cosas a .gitignore para evitar subir información sensible a GitHub

Poniendo a prueba el paquete con {testthat} y test()

Los cambios que introducimos en nuestras funciones de R pueden provocar que fallos que a veces no detectamos inmediatamente. Algunos fallos no producen un error, sino que hacen que nuestras funciones se comporten de forma diferente a la que esperamos. Por ejemplo, un data.frame que devuelve nuestra función podría contener una variable con una clase character en lugar de logical. Este comportamiento indeseado podría pasar desapercibido cuando probemos las funciones que hemos cambiado. Para detectar estos problemas debemos poner a prueba todo el código cada vez que hacemos cambios. Hacer esto de forma manual cada vez puede ser muy tedioso. El paquete {testthat} se encarga de hacer esto por nosotros.

Si ejecutamos use_testhat, se creará una carpeta llamada tests en nuestro repositorio principal. Dentro de esta carpeta, hay otra carpeta llamada testthat. Cualquier script de R que guardemos en esa carpeta se ejecutará automáticamente cuando ejecutemos test() en nuestra consola. Estos scripts deberían seguir tener el siguiente contenido:

test_that("Los datos de clima se cargan correctamente", {expect_error(data("clima"), NA)})

En esta línea de código estamos creando un test mediante la función test_that (del paquete {testhat}). Primero incluimos un mensaje que indique qué estamos “testeando” en específico (en este caso, que podemos cargar el dataset clima sin error). La función expect_error ejecuta el código de dentro, y evalúa si el resultado se corresponde con lo que indiquemos en el segundo argumento (en nuestro caso NA, que significa que no hay fallo). Para ver con más detalle cómo poner a prueba el paquete que has creado y aprender buenas prácticas en este tema puedes ver la documentación del paquete {testhat}.

Reuse

Citation

BibTeX citation:
@online{garcia-castro2021,
  author = {Garcia-Castro, Gonzalo},
  title = {Creando Un Paquete de {R:} Una Guía Informal},
  date = {2021-11-14},
  langid = {en}
}
For attribution, please cite this work as:
Garcia-Castro, G. (2021, November 14). Creando un paquete de R: una guía informal.