knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
library(ai.elementalr.es)
library(RefManageR)

biblio<-ReadZotero(user=5861144, .params = list(tag="elementalr", key="gb603omy3OnQhQ6J77UYrr1p" ), delete.file = FALSE)
WriteBib(biblio, file = "bibliografia.bib", biblatex = TRUE)

Resumen ejecutivo (versión tl;dr)

Este paquete, desarrollado por Academia I con especial foco en quienes se inician en R, tiene tres propósitos:

  1. Aportar funciones de fácil escritura y memorización para realizar operaciones sencillas de manipulación y transformación de objetos y datos;

  2. Funciones "que sean fáciles de recordar" significa, entre otras cosas, que sus nombres y argumentos están escritos en el idioma nativo de quien las usa; en este caso, el "español" (castellano).

  3. Que estas funciones sencillas y fáciles de recordar no tengan un costo en rendimiento y tiempo de ejecución.

Su objetivo es que las personas que comienzan a trabajar con R y necesitan manipular y transformar datos en data frames puedan realizar operaciones simples sin necesidad de investigar y memorizar códigos largos y complejos, que es a lo que obligan los paquetes hoy existentes en R. Pero, además, que la ganancia en facilidad de escritura y memorización de funciones no se traduzca en un costo en rendimiento y desempeño del código medido en tiempo de ejecución.

Para entender la forma en que el paquete simplifica el desarrollo de código, obsérvese cómo se hace exactamente un mismo conjunto de manipulaciones y transformaciones de datos en un data frame primero con el código de R base y luego con el código de ai.elementalr.es.

# El modo tradicional: R Base
data(mtcars)
str(mtcars)

mtcars[,1:5] <- sapply(mtcars[,1:5], as.character) 
mtcars[,2:5] <- sapply(mtcars[,2:5], as.numeric)
mtcars[,1] <- NA
mtcars[,(NCOL(mtcars)-2):NCOL(mtcars)] <-
  sapply(mtcars[,(NCOL(mtcars)-2):NCOL(mtcars)], as.character) 
mtcars[,c("wt", "qsec")] <- 
  format(round(mtcars[,c("wt", "qsec")], digits = 2), nsmall = 3,
        decimal.mark = ",") 
mtcars[is.na(mtcars)] <- 0 


# Las mismas transformaciones con ai.elementalr.es en un pipe:
library("tidyverse")
library("ai.elementalr.es")

data(mtcars)
str(mtcars)

mtcars <- a_caracteres(mtcars, 1:5) %>%
  a_numeros(2:5) %>%
  a_nas(1) %>%
  a_caracteres((NCOL(mtcars)-2):NCOL(mtcars)) %>% 
  formatear_num(columnas = c("wt", "qsec"), decs = 2, sep_decs = ",") %>%
  a_cero()

La diferencia en facilidad de escritura, lectura y memorización entre ambos códigos es autoevidente y no requiere mayores comentarios.

El tiempo de ejecución de ambos códigos es muy similar. Sin embargo, el segundo, con las funciones de ai.elementalr.es, es significativamente más simple y fácil de memorizar.

El paquete está compuesto por otras funciones que realizan otro tipo de transformaciones de objetos y datos. Todas siguen los mismos principios de facilidad de escritura, lectura y memorización, y de eficiencia de ejecución. En otras además se aplica un tercer principio: economía de código.

Es recomendable abandonar poco a poco las funciones de este paquete que manipulan y transforman data frames. Tienen utilidad para quienes se inician en R. Pero una mayor familiaridad con el lenguaje debiera facilitar el uso de las funciones de la base o de tidyverse, que son más flexibles y permiten mayor cantidad de operaciones.



Presentación

El nombre de este paquete, ai.elementalr.es, significa tres cosas:

En una sola frase, ai.elementalr.es es una abreviación de Paquete de Funciones Elementales para R en español, desarrollado por Academia I.

Este nombre tiene varias implicancias. La primera es que el paquete forma parte de un proyecto en el que Academia I ha decidido embarcarse: aportar a que las herramientas de software libre para la ciencia de datos, en particular R y Python, dejen el provincialismo de hablar un único idioma -el inglés- y abracen por fin el multilingüismo de la era global. Academia I, una plataforma de e-learning hispanoparlante, ha adoptado la política de desarrollar todas sus herramientas para ciencia de datos en versiones bilingües, en "español" (castellano) e inglés. Con eso queremos ayudar a que el idioma no sea un obstáculo para que cada vez más personas puedan adoptar y usar estas herramientas.

En segundo lugar, que sean funciones elementales significa que Academia I ofrece herramientas más especializadas y para tareas más sofisticadas. Te invitamos a visitar nuestro repositorio en github para conocer los otros desarrollos en los que estamos trabajando.

Finalmente, que sea necesario recalcar que es un paquete para R significa que también trabajamos en el desarrollo de soluciones para otros lenguajes. Siempre que es posible y relevante, portamos el código de nuestros paquetes a Python y el de nuestros módulos a R.



Los problemas

¿Para qué fue escrito este paquete? Básicamente, para dar respuesta a un conjunto de problemas respecto a la facilidad de uso de código para manipular y transformar datos.

En la actualidad existen dos grandes formatos de código en R: el que se escribe para ejecutar funciones del programa por defecto, comúnmente llamado "base", y el que ha desarrollado Hadley Wickham y su equipo en RStudio.

El formato de código de la base de R tiene las propiedades del lenguaje de programación tradicional: muy lógico, pero ajustado a la forma de pensar, de procesar instrucciones por parte de un computador.

El formato desarrollado por Hadley Wickham y su equipo, que se encuentra en los paquetes de tidyverse, tiene dos pilares: el uso del "pipe" (el encademaniento de funciones a través del símbolo "%>%") y una semántica más centrada en la forma en que los/as seres humanos/as nos comunicamos. Las funciones de sus paquetes (y los argumentos de las funciones) están bautizadas con nombres tomados de verbos del habla cotidiana y están cargados de significado para un/a usuario/a angloparlante; por lo tanto, para cualquiera que domine el inglés, son fáciles de recordar cuando se está escribiendo un código.

Ambos formatos tienen sus ventajas. Mientras el de la base de R es de ejecución rápida y permite estructurar y encadenar lógicamente las instrucciones de programación, el de tidyverse permite hacerlo significativamente, con criterios semánticos, con lenguaje propio de la comunicación entre seres humanos. De hecho, uno de los principios que guía el desarrollo del código de tidyverse es precisamente que se escriba "para humanos, que sea «...centrado en humanos/as» [@wickham-2019, pág. 4].

Cada formato tiene también sus desventajas. Y aunque no son pocas, hay dos que son especialmente importantes: el anglocentrismo y la complejidad. El anglocentrismo no requiere mucha explicación: todas las funciones de la base y de tidyverse, todos los términos usados para que R pueda ejecutar instrucciones, están en inglés. Aunque no es un requisito tener competencias angloparlantes, los/as usuarios/as de R que no hablen inglés tienen mayores dificultades aprendiendo, memorizando y buscando documentación y ayuda que quienes lo dominan fluidamente.

La segunda desventaja, la complejidad, consiste en la necesidad de escribir código extenso, enrevesado y plagado de argumentos para realizar operaciones muy simples. Es cierto que tidyverse ha reducido la dificultad de programación en R gracias a un lenguaje y un mecanismo de encadenamiento de instrucciones muy acorde al funcionamiento de la comunicación humana. Pero eso no significa que la forma tidyverse de hacer las cosas sea más sencilla que la de la base. Una transformación simple en tidyverse requiere memorizar tantos argumentos complejos como en R base. En este caso, "facilidad" y "sencillez" no son sinónimos.

Para entender este problema, obsérvese lo que requiere y demanda convertir el tipo de dato de la columna de un data frame a caracteres. Así se haría en el formato "base":

# Cargamos un data frame cualquiera y vemos qué datos tiene:
data(mtcars)
str(mtcars)

# Todas las columnas son vectores numéricos.

# Se transformarán las 5 primeras a vectores de caracteres manteniendo el contenido:
mtcars[,1:5] <- sapply(mtcars[,1:5], as.character)

# Comprobamos qué columnas son vectores de caracteres
which(sapply(mtcars, is.character) == T)

Y de la siguiente forma se haría a través de tidyverse:

# Cargamos la librería
library("tidyverse")

# Cargamos el data frame cualquiera y vemos qué datos tiene:
data(mtcars)

# Todas las columnas son vectores numéricos.

# Se transformarán las 5 primeras a vectores de caracteres manteniendo el contenido:
mtcars <- mutate(mtcars, across(1:5, as.character))

# Comprobamos qué columnas son vectores de caracteres
which(sapply(mtcars, is.character) == T)

En casos sencillos, para manipulación y transformación de datos que no requieren más de una o dos líneas de código, cualquiera de las dos opciones basta y sobra. Pero la complejidad se presenta cuando es necesario seleccionar columnas con criterios más elaborados y/o encandenar varias manipulaciones y transformaciones. Intentemos la siguiente secuencia:

  1. Transformar a caracteres las primeras cinco columnas;
  2. Revertir la transformación para todas menos la primera;
  3. Transformar a NAs los valores de la primera;
  4. Transformar a caracteres las 3 últimas columnas;
  5. Cambiar el formato de presentación de los datos de las columnas "wt" y "qsec": sustituir el punto (.) por la coma (,) y limitar a dos los decimales mostrados.
  6. Sustituir todos los NAs del data frame por 0.

Este es el resultado con la base de R:

data(mtcars)

mtcars[,1:5] <- sapply(mtcars[,1:5], as.character) # 1. 
mtcars[,2:5] <- sapply(mtcars[,2:5], as.numeric) # 2.
mtcars[,1] <- NA # 3.
mtcars[,(NCOL(mtcars)-2):NCOL(mtcars)] <-
  sapply(mtcars[,(NCOL(mtcars)-2):NCOL(mtcars)], as.character) # 4.
mtcars[,c("wt", "qsec")] <- 
  format(round(mtcars[,c("wt", "qsec")], digits = 2), nsmall = 2,
        decimal.mark = ",") # 5.
mtcars[is.na(mtcars)] <- 0 # 6.

str(mtcars)

Y se haría de la siguiente forma con tidyverse:

library("tidyverse")
# Cargamos el paquete "scales" desarrollado por Hadley Wickham para modificar
# la presentación de datos y etiquetas, fundamentalmente en gráficos:  
library("scales")

data(mtcars)

# Asignamos primero los nombres de las filas a una columna nueva ("marca")
# para que no se pierdan con el procesamiento de dyplr:

mtcars <- rownames_to_column(mtcars, "marca")

# Ejecutamos las transformaciones

mtcars <- mutate(mtcars, across(1:5, as.character)) %>% # 1.
  mutate(across(2:5, as.numeric)) %>% # 2.
  mutate(mpg = NA) %>% # 3.
  mutate(across((NCOL(.)-2):NCOL(.), as.character)) %>% # 4.
  mutate(across(c("wt", "qsec"), ~ scales::number(.x, big.mark = ".",
                                          decimal.mark = ",",
                                          accuracy = 0.01))) %>% # 5.
  mutate(across(1:NCOL(.), ~ replace_na(.x, 0))) # 6.

str(mtcars)

La complejidad de ambas formas de realizar las operaciones salta a la vista. En el caso de la base, el código es difícil de leer. Eso supone que también es difícil de escribir. Y como las transformaciones se aplican a columnas especificas, no es posible encadenar todas las funciones en una única línea de código, lo que tampoco aporta a la economía de escritura propia de los lenguajes de programación tradicional como el de R base.

Tidverse, por su parte, en apariencia subsana el problema con un código más legible y de escritura más directa. La mayor parte de las manipulaciones y transformaciones de datos se hacen con un único verbo: mutate. Sólo se necesita dar con los argumentos adecuados para que la función encuentre las columnas. Sin embargo, ahí comienzan las complejidades: mutate requiere el uso de otras funciones auxiliares (across y/o where) y de fórmulas (en R, las que tienen el carácter "~") en sus argumentos para poder ejecutar la selección de columnas.

Estos auxiliares y las fórmulas son herramientas eficaces y muy prácticas para manipular y aplicar transformaciones a datos que se tienen que seleccionar en base a criterios granulares y complejos. Son muy útiles, por ejemplo, si se usan en conjunto con expresiones regulares. Pero añaden una dificultad de recordación y escritura innecesaria para manipulaciones y transformaciones muy simples, como, por ejemplo, cambiar la clase de los datos de las columnas. Estas transformaciones simples, que se realizan con alguna frecuencia al comenzar a trabajar en R, exigirían dominar y memorizar un código excesivamente extenso y complejo. La desproporción entre un código complejo, recargado, extenso para resultados tan simples no aporta a aligerar la curva de aprendizaje.

Hay, finalmente, una importante desventaja en el procesamiento a través de funciones de tidyverse: el tiempo de ejecución. En efecto, ejecutar esta secuencia de tareas en R base toma entre 4 y 20 veces menos tiempo que a través de tidyverse (ejecutados en un script; en chunks de rmardown los tiempos pueden variar). En un único data frame la diferencia es completamente imperceptible. Pero piénsese qué ocurriría con un script que tuviera que procesar 10.000 data frames. O 100.000. En este último caso estaríamos hablando de una relación de 20 minutos de procesamiento con tidyverse, la formulación más fácil del código, por cada 1 minuto de procesamiento con R base, la más difícil y enrevesada.

En resumen, entonces, en el estado actual del desarrollo de R, la realización de operaciones muy simples de manipulación y transformación de datos presenta tres problemas:

  1. Provincialismo: en cualquiera de los dos formatos hoy dominantes, los nombres de las funciones están en inglés; no son amigables para quienes no están familiarizado con el idioma; tampoco la documentación, oficial y no oficial;

  2. Complejidad: se requieren códigos extensos y recargados, que no guardan ninguna proporción con la simpleza de los resultados;

  3. Desempeño: la versión de código más simple, la de tidyverse, toma entre 3 y 10 veces más tiempo de ejecución en un script. La escasa ganancia en facilidad de escritura supone un costo en desempeño.



La solución

El presente paquete trata de dar una respuesta a los tres problemas:

  1. Sus funciones están escritas en español (castellano), con nombres muy significativos y fáciles de recordar para usuarios/as castizoparlantes;

  2. Las funciones son "wrappers" simples de otras funciones complejas. La simplificación se produce fundamental aunque no exclusivamente en la estructura de los argumentos;

  3. Las funciones han sido escritas a partir de R base y con el cuidado de que la facilidad de escritura y memorización no se traduzca en un costo en desempeño.

Con las funciones del paquete, las seis transformaciones se realizarían de la siguiente forma usando las funciones sin pipe:

library("ai.elementalr.es")

data(mtcars)

mtcars <- a_caracteres(mtcars, 1:5) # 1.
mtcars <- a_numeros(mtcars, 2:5) # 2.
mtcars <- a_nas(mtcars, 1) # 3.
mtcars <- a_caracteres(mtcars, (NCOL(mtcars)-2):NCOL(mtcars)) # 4. 
mtcars <- formatear_num(mtcars, columnas = c("wt", "qsec"),
                        decs = 2, sep_decs = ",") # 5.
mtcars <- a_cero(mtcars) # 6.

head(mtcars)

Como se puede apreciar, la escritura del código se ha simplificado de forma significativa. Y lo que hace cada función se explica fácilmente sólo leyendo el nombre.

Pero esa no es la única forma de escribir el código. Hay una más eficiente y otra más sencilla. La eficiente es, en el marco de la sintaxis de la base de R, encadenando todas las funciones en una única instrucción, algo que no se podía sin este paquete debido a que cada transformación se realiza sobre partes distintas del data frame. Con las funciones de ai.elementalr.es quedaría así:

library("ai.elementalr.es")

data(mtcars)

mtcars <- a_cero( # 6.
    formatear_num( # 5. 
      a_caracteres( # 4.
        a_nas( # 3. 
          a_numeros( # 2. 
            a_caracteres(mtcars, 1:5), # 1.
          2:5), # 2. 
        1), # 3.
      (NCOL(mtcars)-2):NCOL(mtcars)), # 4.
    columnas = c("wt", "qsec"), decs = 2, sep_decs = ",") # 5.
    # 6.
)

str(mtcars)

Esta forma de escribir las funciones optimiza el tiempo de ejecución ligeramente, pero complejiza de forma significativa la escritura y lectura del código. Aunque es posible y hasta más cómodo para quien esté habituado a trabajar con el formato de la base de R, probablemente para la mayor parte de usuarios/as la ligera ganancia en tiempo de ejecución no amerite la dificultad de escritura y lectura.

La forma más sencilla de incorporar estas funciones es escribiendo código en formato tidyverse, encadenando las funciones con el pipe:

library("tidyverse")
library("ai.elementalr.es")

data(mtcars)

mtcars <- a_caracteres(mtcars, 1:5) %>%  # 1.
  a_numeros(2:5) %>%  # 2.
  a_nas(1) %>%  # 3.
  a_caracteres((NCOL(mtcars)-2):NCOL(mtcars)) %>%  # 4. 
  formatear_num(columnas = c("wt", "qsec"), decs = 2, sep_decs = ",") %>% # 5.
  a_cero() # 6.

str(mtcars)

Esta última es la forma más sencilla de usar las funciones de ai.elementalr.es Y su desempeño en tiempo de ejecución es uno de los mejores. En definitiva, en un encadenamiento a través del pipe, las funciones del paquete son aún más sencillas de escribir (de leer, de recordar) que las de tidyverse, y, como se ve en el siguiente acápite, no tienen sus limitaciones de desempeño.

listar(): la lógica de la simplificación

El primer gran criterio que ha guiado el desarrollo de las funciones del presente paquete es la facilidad de uso. Pero no es el único. El segundo gran criterio ha sido la simplificación y la economía, y se puede apreciar en particular en la función listar().

Esta función realiza la tarea simple de agregar elementos a una lista. Pero hace en una única línea de código las operaciones que de otro modo tomarían tres líneas. Imaginemos que tenemos un objeto (data frame, vector, lista) con un pequeño inventario de nuestros libros. Se va a llamar mis_libros:

mis_libros <- c("El péndulo de Foucault", "Germinal", "Artificios", "Angela's ashes")

Luego nos damos cuenta que sería bueno tener todos los inventarios organizados en una lista, que se llamará mis_cosas. Si queremos agregar el inventario de libros, tendríamos que ejecutar el siguiente código:

# Creamos primero la lista
mis_cosas <- list()

# Agregamos luego el objeto con el inventario de libros:
mis_cosas <- c(mis_cosas, list(mis_libros))

Ahora tenemos la lista que queríamos crear. Pero tenemos los datos de los libros duplicado en la lista y en el objeto original fuera de la lista, en el entorno global. Para ordenar nuestro entorno de trabajo, eliminamos el objeto original:

rm(mis_libros)

Con tres líneas de código quedó creada nuestra lista y despejado nuestro GlobalEnv.

listar() hace estas tres cosas en una sola línea de código: crea la lista si no existe, agrega el objeto al principio o al final de la lista, y elimina del entorno global el objeto agregado si así se desea. Todas las instrucciones anteriores quedarían resumidas a lo siguiente:

# Eliminemos primero el objeto "mis_cosas" del entorno global
# para apreciar cómo trabaja `listar()`
rm(mis_cosas)

# Creamos el objeto, que fue eliminado en el ejemplo anterior:
mis_libros <- c("El péndulo de Foucault", "Germinal", "Artificios", "Angela's Ashes")

# Ahora creamos la lista, añadimos el objeto y
# lo eliminamos del entorno global, en una sola instrucción
listar(lista = mis_cosas, obj = mis_libros, rm = T)

Para añadir nuevos objetos, sólo deben crearse y con otra línea de código de listar quedan integrados:

# Creamos nuevo objeto
mis_vinilos <- c("Abbey Road", "Kind of blue", "Nevermind",
     "Bigger, better, faster, more", "Parte de la religión")

# Agregamos a la lista, en el primer lugar y eliminamos el objeto del entorno
# global para que no esté duplicado
mis_cosas <- listar(mis_cosas, mis_vinilos, pos = 1, rm = T)

Referencias



academia-i/ai.elementalr.es documentation built on Dec. 18, 2021, 10:21 p.m.