6 Hábitos

R pre-introductorio ofrece consejería para profesionalizar el uso de R.

6.1 Emplear estratégicamente RStudio

Uno en realidad no trabaja directamente en R sino en el entorno RStudio. Visualmente, digamos, RStudio dispone de cuatro áreas de trabajo:

Las cuatro regiones de RStudio (en fucsia)

  • Script (región superior izquierda): el script es un editor de texto, uno de los dos lugares en los que podemos escribir código; el código que escribamos aquí se puede guardar como un archivo de tipo R script. En el script se consigna entonces todo aquel código que necesitamos (1) ejecutar en el momento y (2) guardar para su uso posterior.

  • Console (región inferior izquierda): la consola es el otro lugar en el que se puede escribir y ejecutar código, con la esencial diferencia de que el código depositado directamente en la consola no se puede guardar. De modo que uno debería reservar el R script para el código esencial (el que es necesario guardar a efectos de reproducirlo luego) y disponer de la consola para actividades auxiliares como probar un comando, verificar la clase de un objeto, accesar la documentación de una función, o ese tipo de acciones de consulta. Asimismo, R arroja en la consola los resultados de los comandos, las advertencias, los errores.

  • Environment (región superior derecha): el environment exhibe los objetos (data frames, vectores, funciones, etcétera) conforme los vamos creando con nuestro código; el environment además brinda información básica sobre estos objetos (en el caso de data frames, por ejemplo, el número y la clase de las variables, el número de observaciones, el nombre de las columnas). Es conveniente mantener un ojo puesto en este panel cada vez que ejecutamos una línea de código que realiza una acción sobre algún objeto en particular, así sea como extrema precaución de que todo marche según lo planeado; en este sentido, el environment puede darnos señales tempranas de que el código no está consiguiendo lo que uno esperaba. RStudio suele preguntar, al cerrar la sesión, si uno desea guardar el workspace -todo lo que existe en el environment-; lo apropiado es indicar que no y más bien forjar el hábito de intencionalmente guardar el código que creó todos esos objetos en primer lugar, no los objetos en sí mismos (salvo que sean producto de operaciones que R demoró horas en ejecutar, obvio).

  • En la región inferior derecha hay varias pestañas para utilizar a conveniencia: Files para navegar entre carpetas y archivos, Plots para visualizar gráficos o figuras, Packages para instalar paquetes -utilísimo si uno no recuerda bien cómo se escribe-, Help para desplegar la documentación de las funciones, Viewer para -entre otros fines- mirar el producto compilado de un archivo de tipo R Markdown.

6.2 Organizar el trabajo en un R project

El primer paso para trabajar adecuadamente en R es crear un R project mediante los botones File > New Project en la barra superior. Emergerá entonces una ventana para seleccionar un directorio local y un título para el nuevo R project. R creará un archivo en formato Rproj y lo alojará en una carpeta con el mismo nombre que uno indicó.

Siempre se debe verificar que estemos trabajando en el R project correcto:

RStudio indica en qué projecto está trabajando

Como buena práctica de data management, la carpeta que aloja el archivo Rproj debe contener carpetas específicas: (1) input para organizar datos crudos; (2) output para guardar productos como reportes, imágenes o datos procesados; y tal vez (3) scripts, para reunir todo el código.

Los archivos de la carpeta scripts se pueden guardar con arreglo a un método de control de versiones, de modo tal que haya acumulación de archivos tarea1_v1.R, tarea1_v2.R, tarea1_v3.R, y así sucesivamente conforme vamos madurando versiones de un mismo script. Esto vale la pena hacerlo sólo si sabemos de antemano que vamos a generar cambios significativos -y potencialmente fatales- en el código. Este método de control de versiones en realidad no es el ideal: la gestión de versiones de código se debe llevar a término en plataformas especializadas como GitHub, pero eso sería muy avanzado para personas que apenas están traveseando R.

Si varios scripts están elaborados de forma tal que deben correrse uno tras otro (digamos, porque uno carga y limpia los datos que el siguiente analiza), el orden de ejecución debe indicarse en el nombre mismo de los archivos: 1_data_loading.R, 2_data_wrangling.R, 3_data_analysis.R, y los que sigan.

No se borra absolutamente nada. Nunca. En cambio, archivamos el material descartado en una sub-carpeta trash para que ahí vivan esos productos intermedios que de momento son inviables pero capaz en el futuro ofrezcan un último e inesperado servicio. Cada carpeta puede tener su propia sub-carpeta trash; la regla de oro es que los archivos descartados permanezcan sub-almacenados en el mismo lugar en el que estuvieron originalmente.

Una vez creada esta estructura general de carpetas, la próxima vez que debamos trabajar en RStudio accesaremos directamente por el archivo Rproj, no por RStudio. ¿Por qué? Porque en tanto RStudio esté operando dentro del marco general de un R project, el working directory queda determinado de oficio (es la misma carpeta donde está ubicado el archivo Rproj) y esto es clave porque el working directory es la carpeta de origen, la carpeta a partir de la cual R buscará los archivos que uno pretende cargar.

Trabajar en el marco de un R project es liberador (uno se despreocupa de definir el working directory a mano, que puede ser confuso) y simplifica la navegación a través del resto de carpetas (input, output, scripts), que de hecho van a mostrarse en la región inferior derecha y van a desplegarse si uno pulsa la tecla tab mientras el cursor se ubica en el medio de los paréntesis que siguen al argumento file = en las funciones que cargan datos, por ejemplo:

## Load data
read_csv(file = "")

Si bien esto puede parecer algo de menor relevancia, desde ya les aviso que uno de los primeros obstáculos que enfrenta cualquier persona novata en programación es el de lograr que R accese a los archivos que a uno le urge cargar tan sólo para empezar el trabajo, obstáculo que significa un golpe psicológico devastador si no se supera rápido:

— ¡Ni abrir los datos pude, YVV!

6.3 Escribir código prolijo

El código se debe anotar línea por línea -o casi-. Por más evidente que resulte la acción llevada a cabo, el script debe anteponerle un comentario, tipo ## Load data y en la línea inmediata depositar el dichoso comando que carga los datos: load(data). Un código sin anotación al cabo de poco tiempo resultará inescrutable para la propia persona que lo escribió y mucho más lo será para alguien que no.

Es conveniente que el script cuente con un encabezado (título, autoría, fecha) y una declaración de los propósitos a los que sirve. También es recomendable que la instalación de los paquetes se realice justo al inicio del script, no a lo largo del mismo. Si un script es muy extenso se puede seccionar en regiones a efectos de interpretarlo con mayor facilidad; y cualquiera que sea su tamaño, se aconseja señalar explícitamente la última línea: ## The script ends here!

La escritura de código es súper sensible y la omisión de una coma o un paréntesis es suficiente para que desemboque en un error. R distingue las mayúsculas de las minúsculas, por lo que data y Data serían objetos distintos. Los objetos deben tener nombres intuitivos, descriptivos, cortos. R reescribe objetos a nombres y uno no debería emplear el mismo nombre más de una vez -a menos que, en efecto, el objetivo sea reemplazar el contenido asociado a un nombre ya existente-.

Puesto que al programar hay que apoyarse mucho en paréntesis, activar la opción Rainbow Parentheses es una necesidad básica (google it!).

Hay que procurar escribir código ordenado, código que se aprecie profesional, bien espaciado, quebrando las líneas más largas a la altura de las comas o los paréntesis, con sangrías (R sabe ponerlas, just google it!). Toda esta normativa está sobradamente desarrollada en The Tidyverse Style Guide.

En una actividad como la programación, tan dependiente de la redacción exacta y meticulosa, demostrar prolijidad en nuestros hábitos de trabajo es un serio asset competitivo.

6.4 Perfeccionar una estructura de R script

Yo recomiendo algo más o menos así:

#### Title
#### Author
#### Date

# ----- Guidelines (below) ----- #

#### This script serves the purposes of:
#### (1) Wrangling/Analyzing/Modeling/Visualizing [...]
#### (2) [...]

#### Notes: 
#### (1) Be aware of commented-out lines and do NOT run them!
#### (2) [...]

# ----- Guidelines (above) ----- #

## Packages & Functions -------------
## Install packages
if(!require("pacman")) install.packages("pacman")
pacman::p_load(glue, here, janitor, lubridate, skimr, tidyverse)

## Create function to display all variables 
customGlimpse <- function(df){
  data.frame(
    col_name = colnames(df),
    col_index = 1:ncol(df),
    col_class = sapply(df, class),
    row.names = NULL
  )
}

## Create function to display all observations 
showAll <- function(df){
  print(df, n = nrow(df))
}

## Data loading -------------
## Load data
df <- 

## Explore variables
customGlimpse(df)

## Inspect NAs

## Remove NAs

## Data wrangling -------------

## Bind data

## Wrangle data

## Transform variables

## Join data

## Data analysis (EDA) -------------
## Explore variables

## Get summary statistics for numeric variables

## Count observations for non-numeric variables

## Detect outliers

## Data modeling -------------
## Build up the model

## Visualize the model

## Data storage -------------
## Save files

## Save images

## Save objects

#### The script ends here!

Por cierto, a mí me gusta repartir el código en al menos dos scripts:

  • 1_data_wrangling, para cargar los datos y limpiarlos y transformalos.

  • 2_data_modeling, para analizarlos, modelarlos, visualizarlos, etcétera.

6.5 Googlear

Googlear es una actividad cotidiana en el campo de la programación. Una de las fortalezas de R es la entusiasta comunidad de usuarios de la que dispone. Si bien Stack Overflow y RStudio Community son los epicentros de la discusión mundial en torno a R y deberían ser sus fuentes inmediatas de consulta, hay abundancia de sitios web valiosos.

Con plena seguridad, cualquier dificultad que una persona novata en programación esté experimentando ya alguien más la habrá consultado y dejado rastro digital de su duda. Googlear bien nos traerá una mina de respuestas sobre prácticamente cualquier tema (por supuesto, googlear bien implica hacerlo en inglés).

En internet se encuentra mucho y muy buen material, casi siempre gratis, si uno sabe dónde buscar. YouTube es magnífico para consultar tutoriales de calidad, como los que ofrece el canal Dataslice o como este video que cubre cientos de hectáreas de terreno en una hora cronometrada. El hashtag #rstats en Twitter es una caja de sorpresas.

6.6 Analizar código ajeno

A programar sólo se aprende programando, pero revisar código escrito por personas más experimentadas ayuda dramáticamente. Kaggle ha sido mi abastecedor de código ejemplar a lo largo de este proceso de aprendizaje.

A propósito también del punto anterior, yo me beneficiado de llevar registro de mis búsquedas y resultados ilustres en Google. Un día busqué “get column name based on row values r” y encontré una solución efectiva y elegante. La usé. Me sacó del apuro. Muchas gracias. Pero aún más importante que sólo copiar y pegar el comando imposible que uno alegremente recortó de algún blog, lo que verdaderamente se debe hacer es guardar la referencia para estudiarla luego, jugar con ese código ajeno, ponerlo a prueba, correrlo a pedacitos, entender cómo funciona y por qué. Este es el mecanismo lento pero imparable para acumular destrezas en programación.

6.7 Invertir tiempo en los errores

El código puede fallar de dos formas: (1) que no corra del todo porque la consola de entrada no lo aceptó, generó un error; (2) que sí corra pero no haga lo que uno pensaba que haría. Leí en alguna parte -pero tendré que rebuscar la referencia- que la mayor complicación de programar es que R hará exactamente lo que uno le pide, no lo que uno cree que le está pidiendo.

Para agregar dificultad, los mensajes que R devuelve suelen ser poco explicativos, y más de una vez me he visto en la obligación de recurrir al compendio de errores y advertencias de Statistics Globe para entender a qué me estoy enfrentando.

La práctica constante nos irá acercando al momento gozoso en el que las causas por las que el código falla empiezan a percibirse intuitivamente. Algunos sospechosos usuales son:

  • Me faltó un paréntesis, una coma.

  • Me faltó transformar una variable.

  • Me faltó especificar algún argumento de una función.

  • Me olvidé de cargar un paquete (install.packages() los instala pero library() los carga para que se puedan emplear).

  • Entraron en conflicto funciones homónimas de paquetes distintos (este es difícil de notar y particularmente desconcertante).

  • Apliqué a un objeto una función que no correspondía del todo.

  • Escribí mal el nombre de un objeto.

Hay mil maneras de producir código malo y apenas unas cuantas de producirlo bien.

El secreto para desarrollar nuestra intuición en torno a los errores es mostrar compromiso con ellos, correr el código defectuoso línea por línea para determinar dónde están los errores exactamente, investigarlos, reescribir el código que los arrojó, probarlo de nuevo, conseguir uno que sí funcione, someterlo a estrés, y así hasta familiarizarnos con las reglas y los límites de la programación.

6.8 Crear diccionarios de variables

Sobre todo si uno está ensamblando conjuntos de datos con información de diversas fuentes, es un buen hábito tomarse con la misma seriedad el muy profesional -pero no muy divertido- proceso de llevar buen registro de las variables: qué son, en qué unidades están, de dónde salió la información, etcétera. Algo al respecto se elabora con más detalle en este libro.

6.9 Evitar rabbit holes

Hay ciertas dimensiones de la programación que son una fuente interminable de distracciones, y ninguna es tan formidable y peligrosa que la afición por poner las visualizaciones a punto. Es de nunca acabar esto. Nos obsesionamos con cambiar la simbología, los colores, los tamaños, los ejes, los títulos, las líneas, las leyendas… Cuando uno menos se lo espera acabó gastando dos horas en el gráfico más irrelevante de todo el documento. Es una trampa, seriamente les digo.

6.10 Escribir pseudo-code

Siempre es importante pensar antes de actuar, ni se diga en programación. Ya sea una tarea grande o pequeña, obra en nuestro máximo beneficio sentarnos unos cinco minutos y con papel y lapiz empezar a escribir qué es lo que queremos conseguir y cómo planeamos conseguirlo. El producto de esta etapa se llama pseudo-code.

Absténganse de pensar con calma antes de ponerse a producir código verdadero y -si la tarea es de proporciones significativas- con seguridad acabarán atados a abordajes sub-óptimos, como me pasó a mí la vez que quise programar una función que detectara paréntesis mal ordenados y terminé escribiendo el código inagotable y absolutamente impráctico que con mucha vergüenza les voy a confiar a continuación:

#### Parentheses Gatekeeper Function: parengate()

## WARNING! Messiest code ever
## Turn //Rainbow Parentheses// on: 
## Tools>GlobalOps>Code>Display>Rainbow Parentheses

## Load packages ------
# library(tidyverse)

## Create function ------
parengate <- function(s){
  
  ## Split string
  s1 <- unlist(str_split(s, ""))
  
  ## Create an empty vector
  test_vector <- c()
  
  ## Reject strings that start with a close parenthesis or display a mismatch between open and close parentheses
  if(!(s1[1] %in% c("(", "[", "{")) | 
     length(which(s1 %in% c("("))) != length(which(s1 %in% c(")"))) |
     length(which(s1 %in% c("["))) != length(which(s1 %in% c("]"))) |
     length(which(s1 %in% c("{"))) != length(which(s1 %in% c("}")))){ # outer-if
    return("invalid")
  } else{
    
    ## Start for-loop focused on () parentheses
    for(i in s1){
      if(i == "("){ # test 1_(): pair-wise symmetry; e.g.: if "(", then ")" 
        a1 <- which(str_detect(s1, "\\("))[1] + 1 == which(str_detect(s1, "\\)"))
        a1 <- ifelse(length(a1) == 0, 999, a1) # it prevents code failure when there's no close parenthesis
        
        if(a1 == 999 | a1 == FALSE | (length(s1) == 2 & i == "(")){ # test 2_(): if there's no pair-wise symmetry, inspect symmetry between halves
          half1 <- s1[1:(length(s1) / 2)]
          half2 <- s1[(length(s1) / 2):length(s1)][-1]
          half2 <- rev(half2)
          
          s2 <- data.frame(
            combined = str_c(half1, "", half2)
          ) |>
            pull() |>
            strsplit("") |>
            unlist()
          
          a2 <- which(str_detect(s2, "\\("))[1] + 1 == which(str_detect(s2, "\\)"))[1] 
          
          if(is.na(a2) == FALSE & a2 == TRUE){ 
            test_vector[i] <- "valid"
          } else{
            test_vector[i] <- "inv_alid"
          } 
          
        } # test 2_() ends here!
        
        if(a1 == TRUE){ # if there's pair-wise symmetry, continue to the next loop
          next()
        }
        
      } # test 1_() ends here!
      
    } # () for-loop ends here!
    
    ## Start for-loop focused on [] parentheses 
    for(i in s1){
      if(i == "["){ # test 1_[]: inspect pair-wise symmetry; e.g.: if "(", then ")" 
        a3 <- which(str_detect(s1, "\\["))[1] + 1 == which(str_detect(s1, "\\]"))
        a3 <- ifelse(length(a3) == 0, 999, a3) # it prevents code failure when there's no close parenthesis
        
        if(a3 == 999 | a3 == FALSE | (length(s1) == 2 & i == "[")){ # test 2_[]: if there's no pair-wise symmetry, inspect symmetry between halves
          half1 <- s1[1:(length(s1) / 2)]
          half2 <- s1[(length(s1) / 2):length(s1)][-1]
          half2 <- rev(half2)
          
          s2 <- data.frame(
            combined = str_c(half1, "", half2)
          ) |>
            pull() |>
            strsplit("") |>
            unlist()
          
          a4 <- which(str_detect(s2, "\\["))[1] + 1 == which(str_detect(s2, "\\]"))[1] 
          
          if(is.na(a4) == FALSE & a4 == TRUE){ 
            test_vector[i] <- "valid"
          } else{
            test_vector[i] <- "inv_alid"
          } 
          
        } # test 2_[] ends here!
        
        if(a3 == TRUE){ # if there's pair-wise symmetry, continue to the next loop
          next
        }
        
      } # test 1_[] ends here!
      
    } # [] for-loop ends here!
    
    ## Start for-loop focused on {} parentheses
    for(i in s1){
      if(i == "{"){ # test 1_{}: inspect pair-wise symmetry; e.g.: if "(", then ")" 
        a5 <- which(str_detect(s1, "\\{"))[1] + 1 == which(str_detect(s1, "\\}"))
        a5 <- ifelse(length(a5) == 0, 999, a5) # it prevents code failure when there's no close parenthesis
        
        if(a5 == 999 | a5 == FALSE | length(s1) == 2 & (length(s1) == 2 & i == "{")){ # test 2: if there's no pair-wise symmetry, inspect symmetry between halves
          half1 <- s1[1:(length(s1) / 2)]
          half2 <- s1[(length(s1) / 2):length(s1)][-1]
          half2 <- rev(half2)
          
          s2 <- data.frame(
            combined = str_c(half1, "", half2)
          ) |>
            pull() |>
            strsplit("") |>
            unlist()
          
          a6 <- which(str_detect(s2, "\\{"))[1] + 1 == which(str_detect(s2, "\\}"))[1] 
          
          if(is.na(a6) == FALSE & a6 == TRUE){  
            test_vector[i] <- "valid"
          } else{
            test_vector[i] <- "inv_alid"
          } 
          
        } # test 2_{} ends here!
        
        if(a5 == TRUE){ # if there's pair-wise symmetry, the whole thing is valid
          test_vector[i] <- "valid"
        }
        
      } # test 1_{} ends here!
      
    } # {} for-loop ends here!
    
    ## Make a final decision
    test_vector_summ <- str_detect(test_vector, "valid") |>
      all()
    
    if(test_vector_summ == TRUE){
      return("valid")
    } else{
      return("invalid")
    }
    
  } # outer-if ends here
  
} # function ends here!

## Test function ------
## Test some valid cases
parengate("()") 
#> [1] "valid"
parengate("[]") 
#> [1] "valid"
parengate("{}") 
#> [1] "valid"
parengate("{}[]") 
#> [1] "valid"
parengate("()[]{}") 
#> [1] "valid"
parengate("[](){}")
#> [1] "valid"
parengate("{}[]()") 
#> [1] "valid"
parengate("[]()(){}") 
#> [1] "valid"
parengate("{[]}") 
#> [1] "valid"
parengate("([{}])") 
#> [1] "valid"
parengate("{([({{()}})])}")
#> [1] "valid"
parengate("{([({{()}})])}{([({{()}})])}{([({{()}})])}")
#> [1] "valid"

## Test some invalid cases
parengate("([)]") 
#> [1] "invalid"
parengate("([{)}]") 
#> [1] "invalid"
parengate("(]") 
#> [1] "invalid"
parengate("(}") 
#> [1] "invalid"
parengate("{)") 
#> [1] "invalid"
parengate("{]") 
#> [1] "invalid"
parengate("[)") 
#> [1] "invalid"
parengate("[}") 
#> [1] "invalid"
parengate("{{") 
#> [1] "invalid"
parengate("}}") 
#> [1] "invalid"
parengate("((") 
#> [1] "invalid"
parengate("))")
#> [1] "invalid"
parengate("[[") 
#> [1] "invalid"
parengate("]]") 
#> [1] "invalid"
parengate("[[))")
#> [1] "invalid"
parengate("{{]]") 
#> [1] "invalid"
parengate("]()[")
#> [1] "invalid"
parengate("]()]") 
#> [1] "invalid"
parengate("[()[")
#> [1] "invalid"
parengate("[)(]")
#> [1] "invalid"
parengate("[]]]()") 
#> [1] "invalid"
parengate("{}{]")
#> [1] "invalid"
parengate("[}[]")
#> [1] "invalid"
parengate("[][}")
#> [1] "invalid"
parengate("[][)")
#> [1] "invalid"
parengate("[)[]")
#> [1] "invalid"
parengate("(}()")
#> [1] "invalid"
parengate("(]()")
#> [1] "invalid"
parengate("(]()")
#> [1] "invalid"
parengate("()(}")
#> [1] "invalid"
parengate("()(]")
#> [1] "invalid"

#### The script ends here!