June 22, 2020

1950 words 10 mins read

Creando paquetes en R

Probablemente conoces la enorme utilidad que tiene el crear nuestras propias funciones en R; pasar de tener que escribir la misma rutina cada vez que la necesitamos a tan solo llamarla y aplicarla en donde sea necesario definitivamente nos ayuda a optmimizar nuestros flujos de trabajo.

Sin embargo, muchas veces nos quedamos atrapados en este paso y entonces vamos acumulando scripts con funciones. Scripts, que en el mejor de los casos, tenemos que sourcear adecuadamente cada vez y que, en el peor de ellos, olvidamos que existen y entonces terminamos reescribiendo nuestras rutinas.

Pues precisamente ese paso adicional que nos permitirá organizar mejor nuestras funciones creadas y poder tenerlas a la mano fácilmente se llama: crear un paquete (sí, como que esos que instalamos del CRAN o de GitHub).

Crear un paquete nos permitirá:

  • Organizar nuestras funciones y saber siempre dónde están.
  • Documentarlas para no olvidar qué hacen y cómo se usan.
  • Instalarlas en R y así tenerlas siempre a la mano.
  • Compartirlas (si así lo deseamos) para facilitarle la vida a alguien.
  • Aprender cosas geniales de R, 😉.

Así que eso es lo que aprenderás en este post/tutorial: a armar un paquete con tus propias funciones.

Manos a la obra

N.B. Las instrucciones a continuación consideran que estás usando RStudio como IDE.

Lo primero que necesitamos es tener nuestras funciones ya creadas. En este ejemplo construiremos un pequeño paquete para descargar opiniones de productos de Amazon y crear nubes de palabras con ellas.

Nuestro script de funciones luce así:

Mostrar

library(polite)
library(rvest)
library(stringr)
library(tm)
library(wordcloud)
library(wordcloud2)

get_reviews <- function(id, pages){
  webpage <- paste0("https://www.amazon.com.mx/product-reviews/", id)
  session <- bow(webpage, force = TRUE)
  reviews_list <- list()
  
  for (i in 1:pages) {
    reviews_list[[i]] <- scrape(session, query = list(pageNumber=i)) %>% 
      html_nodes("[class='a-size-base review-text review-text-content']") %>% 
      html_text()
  }
  
  reviews <- unlist(reviews_list)
  
  return(reviews)
}

clean_reviews <- function(reviews){
  
  cleaned <- str_replace_all(reviews, "[\r\n]", "") %>% 
    str_replace_all(., "[.]", " ") %>% 
    str_replace_all(., "[,]", " ") %>% 
    str_replace_all(., "[;]", " ") %>% 
    tolower() %>% 
    trimws() %>% 
    paste(collapse = " ")
  
  return(cleaned)
    
}

reviews_wordcloud <- function(cleaned_reviews){
  doc <- Corpus(VectorSource(cleaned_reviews))
  doc <- tm_map(doc, removeWords, stopwords(kind = "spanish"))
  m <- as.matrix(TermDocumentMatrix(doc))
  palabras <- sort(rowSums(m), decreasing = TRUE)
  df <- data.frame(palabra = names(palabras), freq = palabras)
  
  wordcloud(words = df$palabra, freq = df$freq, rot.per = 0, colors = brewer.pal(12, "Paired"))
}

reviews_wordcloud2 <- function(cleaned_reviews){
  doc <- Corpus(VectorSource(cleaned_reviews))
  doc <- tm_map(doc, removeWords, stopwords(kind = "spanish"))
  m <- as.matrix(TermDocumentMatrix(doc))
  palabras <- sort(rowSums(m), decreasing = TRUE)
  df <- data.frame(palabra = names(palabras), freq = palabras)
  
  wordcloud2(data = df, color = "random-dark", rotateRatio = 0)
}

No entraré a los detalles de cada función pero lo importante es saber lo siguiente:

  1. Tenemos 4 funciones, una para descargar las opiniones, una para limpiarlas y dos para crear las nubes de palabras.
  2. Nuestras funciones usan otras funciones de otros paquetes de R (esto será importante en un paso más adelante).

Ahora sí, a crear el paquete.

N.B. Si estás familiarizado con git y deseas que tu paquete este en un repositorio te recomiendo seguir primero estos pasos.

1

Lo primero que necesitaremos es una carpeta que almacenará todo, lo ideal es que esta carpeta se llame como el paquete, en este caso le llamaremos amazonReviews.

Una vez hecho esto, crearemos un proyecto de R en ese directorio.

Adicionalemte necesitamos un par de paquetes que nos harán la vida más fácil, estos son: devtools, usethis y here (este último no es tan necesario).

2

Ahora tenemos que hacerle saber a R que crearemos un paquete, para ello corremos en la consola la siguiente función:

usethis::create_package(here::here())

# usethis::create_package(getwd()) # en el caso de no haber instalado el paquete here

Al hacerlo nos saldrá una pregunta en la consola que nos pedirá decidir si debemos sobreescribir el archivo RProj que había sido creado en el paso 1, decimos que sí.

Notemos que una vez hecho lo anterior se habrá habilitado un pestaña llamada Build en el recuadro del Environment y además se habrán creado automáticamente algunos archivos en el directorio del proyecto:

archivos creados automáticamente

3

Ahora lo que haremos será llenar el archivo DESCRIPTION con el título del paquete, nuestra información en la sección Authors@R y la descripción.

N.B. Por ahora no modificaremos el campo License, ya llegará el momento.

Package: amazonReviews
Title: Scrapes amazon reviews
Version: 0.0.0.9000
Authors@R: 
    person(given = "David",
           family = "Mateos",
           role = c("aut", "cre"),
           email = "davidmateosmo@gmail.com")
Description: Provides a couple of functions to scrape reviews from Amazon MX and visualise them in wordclouds.
License: `use_mit_license()`, `use_gpl3_license()` or friends to
    pick a license
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.1.0

4

Ahora toca el turno de las funciones.

Tenemos que ir llenando el paquete con nuestras funciones, para ello solo debemos crear un nuevo script y pegar nuestra función.

Antes de guardarla tenemos que modificar un poco este script, empezaremos por documentarla a la R. Esta parte será bastante sencilla pues usaremos a nuestro favor una característica de devtools que convierte cierto tipo de comentarios a la documentación estándar de R (acá usamos la magia de un paquete llamado roxygen2).

Estos comentarios especiales son precedidos por los caractéres #':

#' Scrapes Amazon MX reviews
#'
#' @param id The product id, you can get it from the amazon URL
#' @param pages Number of pages to scrape data from
#' @export

get_reviews <- function(id, pages){
  
  webpage <- paste0("https://www.amazon.com.mx/product-reviews/", id)
  session <- bow(webpage, force = TRUE)
  
  reviews_list <- list()
  
  for (i in 1:pages) {
    reviews_list[[i]] <- scrape(session, query = list(pageNumber=i)) %>% 
      html_nodes("[class='a-size-base review-text review-text-content']") %>% 
      html_text()
  }
  
  reviews <- unlist(reviews_list)
  
  return(reviews)
}

Notemos los comentarios con los indicadores @param y @export, éstos le dicen a R cómo tratar esas partes de la documentación, e.g. @param le indica a R que estamos explicando un parámetro de la función y pondrá esta información en un lugar específico de la documentación.

Por otro lado @export le dice a R que le debe permitir al usuario usar esa función, este parámetro es importantísimo y casi siempre lo usaremos.

5

Ahora nos encargaremos de las dependencias de la función, como mencionamos al inicio, nuestras funciones usan otras funciones de otros paquetes de R por lo que hay que hacer que R sepa cuáles son y de qué paquetes.

Esta función en particular (get_reviews) usa funciones de dos paquetes, a saber polite y rvest, para avisarle a R sobre esto debemos correr la siguiente función:

usethis::use_package("polite")

# √ Setting active project to 'C:/Users/David Alberto MMO/Documents/Workshops/Rpackages/amazonReviews'
# √ Adding 'polite' to Imports field in DESCRIPTION
# * Refer to functions with `polite::fun()`


usethis::use_package("rvest")

# √ Adding 'rvest' to Imports field in DESCRIPTION
# * Refer to functions with `rvest::fun()`

Notemos que R nos avisa cómo debemos usar la funciones de cada paquete, esto es, en lugar de solo usar la función debemos seguir la estructura: nombredepaquete::función, así que debemos modificar la función para que siga esta regla:

#' Scrapes Amazon MX reviews
#'
#' @param id The product id, you can get it from the amazon URL
#' @param pages Number of pages to scrape data from
#' @export

get_reviews <- function(id, pages){
  
  webpage <- paste0("https://www.amazon.com.mx/product-reviews/", id)
  session <- polite::bow(webpage, force = TRUE)
  
  reviews_list <- list()
  
  for (i in 1:pages) {
    reviews_list[[i]] <- polite::scrape(session, query = list(pageNumber=i)) %>% 
      rvest::html_nodes("[class='a-size-base review-text review-text-content']") %>% 
      rvest::html_text()
  }
  
  reviews <- unlist(reviews_list)
  
  return(reviews)
}

Una vez hechas estas modificaciones procedemos a guardar el script con el nombre de la función.

Ahora bien, para saber que todo se ha hecho correctamente iremos a la pestaña Build y daremos click en el botón check o bien, corremos la siguiente función:

devtools::check()

Al final veremos algo como lo siguiente:

> checking DESCRIPTION meta-information ... WARNING
  Non-standard license specification:
    `use_mit_license()`, `use_gpl3_license()` or friends to pick a
    license
  Standardizable: FALSE

> checking R code for possible problems ... NOTE
  get_reviews: no visible global function definition for '%>%'
  Undefined global functions or variables:
    %>%

0 errors v | 1 warning x | 1 note x

Esta retroalimentación que R nos da nos ayudará a saber si estamos siguiendo todas las reglas correctamente o si debemos modificar algo, en este caso hay cosas que resolver.

Por un lado tenemos un WARNING que nos indica que la licencia no es correcta, ignoremos esto pues pronto nos encargaremos de ello. Por otro lado, generamos un NOTE que nos indica que no definimos una función que estamos usando, esta es %>%.

Particularmente, cuando hagamos paquetes que usen el increíble pipe de magrittr debemos correr la siguiente función:

usethis::use_pipe()

Hacemos el check nuevamente y ese NOTE habrá desaparecido (aunque sigue presente el warning).

Ahora tenemos que correr la siguiente función para transformar nuestros comentarios tipo roxygen2 a la documentación estándar de R:

devtools::document()

6

Lo que sigue es repetir los pasos 4 y 5 con cada una de las funciones restantes, haciendo check constantemente para asegurarnos que vamos por buen camino.

7

Con todas las funciones configuradas, estamos aun paso de tener nuestro paquete listo.

Ahora sí, hay que encargarnos de la licencia, ésta nos servirá para proteger nuestro trabajo y hacerle saber a las demás personas cómo puede usarlo.

Tenemos 7 licencias posibles, las cuales recomiendo revisar a detalle para saber cual es la que mejor aplica a tu proyecto:

  • CC0: dedicated to public domain. Appropriate for data packages.
  • MIT: simple and permissive.
  • Apache 2.0: provides patent protection.
  • GPL v3: requires sharing of improvements.
  • AGPL v3: requires sharing of improvements.
  • LGPL v3: requires sharing of improvements.
  • CCBY 4.0: Free to share and adapt, must give appropriate credit. Appropriate for data packages.

En este ejemplo usaremos la licencia MIT:

usethis::use_mit_license("David Mateos") # En lugar de mi nombre deberás poner el tuyo ;D

Damos Check una vez más y debemos ver lo siguiente:

-- R CMD check results --------------------------- amazonReviews 0.0.0.9000 ----
Duration: 18.7s

0 errors v | 0 warnings v | 0 notes v

R CMD check succeeded

Eso significa que todo funciona a la perfección.

Instalando el paquete.

Finalmente podemos instalar nuestro paquete en nuestra máquina, para ello debemos dar click en Install and Restart de la pestaña Build.

Y listo, una vez terminado el proceso podemos usar nuestro paquete y sus funciones como cualquier otro.

Compartiendo el paquete

¿Qué tenemos que hacer si queremos compartir el paquete con alguien?

Tenemos dos opciones, una de ellas es generar una carpeta comprimida que podremos enviar y que se puede instalar fácilmente.

Para ello basta con dar click en More -> Build Source Package de la pestaña Build, automáticamente se generará el archivo necesario que podemos compartir e instalar con la siguiente función:

devtools::install_local("path/to/source/package")

La otra opción es poner nuestro paquete en un repositorio de GitHub, con el cual las personas podrán instarlarlo remotamente como cualquier otro.

Para ello debemos empezar por crear un nuevo repositorio en GitHub asegurandonos de marcar la casilla Initialize this repository with a README.

Posteriormente, en lugar de crear un proyecto de RStudio en una carpeta local, debemos crear un proyecto desde Control de versiones

Y listo con esto pasos terminados ahora sí puedes continuar con la creación del paquete.

Cuando hayas terminado y hayas hecho push de todo el paquete éste se podrá instalar con la siguiente función:

devtools::install_github("link/alrepositorio") # sin la parte: github.com

Recapitulación

Como puedes ver el proceso es bastante sencillo y podemos resumirlo en los siguientes pasos:

  1. Crear el paquete en una carpeta local o clonada de un repositorio.
  2. Llenar el archivo DESCRIPTION.
  3. Escribir y documentar las funciones cuidando las dependencias que existan.
  4. Hacer checks continuamente para garantizar que todo está correcto.
  5. Escoger una licencia (no este paso bien puede hacerse junto con el 2 si ya lo tienes claro).
  6. Instalar y compartir.

Ojalá que este tutorial te ayude a dar ese paso que te permita empezar a crear paquetes tanto para tu uso personal como para compartirlo con la comunidad de R.