4 Conceptos
Para mí, el mejor libro para aprender R es R for Data Science, escrito por el gran Hadley Wickham (existe una versión en español, de la que hablaré luego).
Wickham decidió que el tema para abrir su libro fuese el de visualización de datos:
“La visualización de datos es un gran lugar para empezar porque la recompensa es muy clara: gráficos elegantes e informativos que ayudan a entender los datos.” –Hadley Wickham’s R for Data Science.
Si una persona cuenta con cierto grado de instrucción en R, ciertamente este sería un buen comienzo. ¿Se puede decir lo mismo de las personas que hace apenas cinco minutos instalaron R por primera vez?
Uno de los primeros bloques de código que aparecen en R for Data Science es el siguiente:
## Plot
ggplot(data = mpg) +
geom_point(mapping = aes(x = displ, y = hwy))
Sí, funciona. Uno escribe ese código, lo ejecuta y R devuelve el gráfico. Recuerdo la satisfacción de correr ese código por primera vez y ver el gráfico hacerse… Y recuerdo también que no entendía absolutamente nada de lo que hay en ese bloque de código.
Aunque la apuesta de ir directamente a la visualización de datos me parece valiosa, creo que una persona completamente debutante en programación necesita primero consolidar cierto vocabulario.
R pre-introductorio tiene como objetivo consolidar todo ese vocabulario necesario para afrontar libros mejores, como los de Hadley Wickham.
4.1 R aplica funciones a objetos
Para empezar, necesitamos entender cómo funciona R. Indiqué antes que R nos sirve para trabajar con datos, para realizar análisis estadístico. Pero saber para qué sirve R no nos dice nada sobre cómo funciona R, y no saber cómo funciona realmente este lenguaje fue tal vez lo que más empinó mi proceso de aprendizaje al inicio.
Ok. ¿Cómo funciona R? Aquí vamos: R aplica funciones a objetos. Y nada más… eso es todo lo que hace R, de hecho: aplicar funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
R aplica funciones a objetos.
Miremos, por ejemplo, qué sucede cuando hacemos que R le aplique la función sum()
a los objetos 3
y 4
:
## Compute
sum(3, 4)
#> [1] 7
¡Los sumó! En R, las funciones tienen esos paréntesis así, abiertos, libres, desocupados, como esperando que uno les ponga algo adentro…
De modo que sum()
es una función; en su calidad de función, la podemos aplicar sobre los objetos que depositemos dentro de los paréntesis.
## Compute
sum(1 * 8 - 10)
#> [1] -2
R aplica funciones, sum()
, a objetos, (1 * 8 - 10)
. ¿Sí ven? No exagero si digo que lo estudiado hasta acá cubre ya bastante de esta pre-introducción. Lo que sigue en adelante es conocer más sintaxis para potenciar aún más la destreza funcional de R.
Va otro ejemplo:
## Compute
sum(3, 4) |> as.character()
#> [1] "7"
Usamos el native pipe operator |>
para que R, primero, sumara los números con la función sum()
, y, después, transformara el resultado numérico 7
en el texto "7"
.
El operador |>
lo vamos a encontrar seguido porque es el que nos permite anidar funciones y así el código es más fácil de leer y de escribir. Si no lo queremos usar, podemos meter una función dentro de la otra:
## Compute
as.character(sum(3, 4))
#> [1] "7"
En este caso da bastante lo mismo porque el código no era díficil de leer en primer lugar, pero si uno necesitara anidar muchas funciones seguidas sí se complicaría bastante la lectura.
En suma, estamos aprendiendo que R le aplica funciones a objetos, cada una con algún efecto específico sobre los objetos en cuestión: algunas cargan datos, otras son para limpiarlos, otras los transforman, los modelan, los visualizan, etcétera.
Programar en R es, básicamente, aplicar funciones a objetos:
## Find the max
max(4, 100, -1)
#> [1] 100
## Find the min
min(4, 100, -1)
#> [1] -1
## Transform
toupper("quiero pasar este texto a mayúscula, menos mal existe la función toupper()")
#> [1] "QUIERO PASAR ESTE TEXTO A MAYÚSCULA, MENOS MAL EXISTE LA FUNCIÓN TOUPPER()"
## Transform
tolower("PUES LO CONTRARIO")
#> [1] "pues lo contrario"
En otras palabras, las funciones toman objetos como inputs y los convierten en algo diferente.
Tengo un objeto "No sé si le voy a coger cariño a R"
al que le aplico la función sub()
, y esta función hasta cuenta con parámetros internos que me permiten indicarle a R exactamente qué quiero que haga con ese objeto:
sub(x = "No sé si le voy a coger cariño a R",
pattern = "No sé si",
replacement = "Sé que sí")
#> [1] "Sé que sí le voy a coger cariño a R"
Acabamos de aprender que R es tan gentil que las funciones poseen argumentos para que las podemos configurar y acomodarlas a nuestro deseo y necesidad. En el caso anterior, pattern
, replacement
y x
son argumentos con los que indicamos, respectivamente, qué fracción de un objeto (texto) cambiar, con qué cambiarla, y cuál es el objeto en sí (y no menos importante es apreciar cómo el código se puede picar en las comas para que se lea más fácil -si lo dejara todo corrido, se pone muy largo-).
Habíamos iniciado explorando R en su -poco impresionante- rol de calculadora:
## Compute
100 + 100
#> [1] 200
## Compute
"100" + "100"
#> Error in "100" + "100": non-numeric argument to binary operator
Y cada vez vamos conociendo mejor su potencial:
## Load data
data(World) # tmap
## Filter
north_america <- World |>
filter(continent == "North America")
## Plot
tm_shape(north_america) +
tm_polygons("income_grp", palette = "-Blues") +
tm_legend(legend.position = c("left", "bottom"))
Les digo que de R será difícil aburrirse…
Y si se aburren, pues configuran RStudio distinto y se pasan a practicar Python; de paso, miren cómo "100" + "100"
, que no se podía ejecutar en R, sí es aceptable en Python (aunque no es una suma exactamente):
## THIS IS PYTHON!!!!
"100" + "100"
#> '100100'
La programación se va complicando enriqueciendo conforme uno va integrando más sintaxis, pero por muy complejo que parezca nunca deja de ser básicamente lo mismo: un interplay entre objetos y funciones.
4.2 Objetos
Los objetos almacenan datos (voy a decir elementos). Hay múltiples tipos de elementos y múltiples estructuras de objetos. Atención a esto: tipo y estructura no son lo mismo.
¿Por qué es importante la distinción entre tipos y estructuras? Porque si bien programar en R consiste en aplicar funciones a objetos, cuáles funciones es posible aplicar a un objeto determinado depende enteramente de la estructura del objeto y del tipo de elementos que ese objeto almacene.
Lo anterior se entiende mejor si se pone en práctica:
## Compute
sqrt(4)
#> [1] 2
Sacarle la raíz cuadrada a ese objeto numérico funcionó.
## Compute
sqrt("dude, what u doing?")
#> Error in sqrt("dude, what u doing?"): non-numeric argument to mathematical function
¿Sacarle la raíz cuadrada a ese objeto textual? Pues claro que no iba a funcionar.
Sacarle la raíz cuadrada a un número tiene sentido, sacársela a un texto no:
En R, el tipo de los objetos textuales es character
y el de los objetos numéricos es numeric
. Los objetos numéricos se subdividen en double
, si tienen decimales, e integer
, si son enteros.
Esto está muy fácil.
Va de nuevo: cuál función puedo aplicar depende enteramente de cómo sea el objeto al que se la pienso aplicar. Y este será uno de los errores más frecuentes que van a experimentar al principio: siempre que topen con un error, descarten primero que no se deba a que la función aplicada no es apropiada para esa estructura de objeto o para el tipo de elementos que el objeto contiene… Un buen número de veces el error viene justo de ahí, ya verán que sí.
Sigamos mirando ejemplos medio burdos pero necesarios de cómo los objetos almacenan datos. Puedo crear un objeto llamado my_last_name
y guardar ahí mis apellidos:
## Create object
my_last_name <- "Alvarado-Mena"
Bien. Logré que R almacenara un dato, "Alvarado-Mena"
, en un objeto; además, hice que R le asignara un nombre, my_last_name
, a ese objeto.
Los objetos que uno crea con el operador <-
se van enlistando en el environment (región superior derecha).
Tengo dos formas de imprimir los objetos que he creado. Primero, simplemente llamándolos por su nombre (si ya los había creado antes, obvio):
## Print
my_last_name
#> [1] "Alvarado-Mena"
La segunda alternativa es meterlo todo entre paréntesis al momento mismo de crear el objeto (a mí me encanta hacer esto):
## Create object
(my_last_name <- "Alvarado-Mena")
#> [1] "Alvarado-Mena"
Ahora voy a crear otro objeto name
para guardar allí los nombres de mis boxeadores favoritos, un objeto surname
para sus apodos, un objeto birthday
para sus fechas de nacimiento, un objeto age
para sus edades, y un objeto active
para indicar si están activos actualmente:
## Create objects
name <- c("Saúl Álvarez", "Román González", "Roberto Durán", "Mike Tyson")
name
#> [1] "Saúl Álvarez" "Román González" "Roberto Durán"
#> [4] "Mike Tyson"
surname <- c("Canelo", "Chocolatito", "Mano de Piedra", "Iron Mike")
surname
#> [1] "Canelo" "Chocolatito" "Mano de Piedra"
#> [4] "Iron Mike"
birthday <- c("1990-07-18", "1987-06-17", "1951-06-16", "1966-06-30")
birthday
#> [1] "1990-07-18" "1987-06-17" "1951-06-16" "1966-06-30"
age <- c(32, 35, 71, 56)
age
#> [1] 32 35 71 56
active <- c(T, T, F, F)
active
#> [1] TRUE TRUE FALSE FALSE
Esa función c()
es absolutamente vital, pero de ella me ocuparé luego.
Con estos tres ejemplos, espero, se va entendiendo mejor cómo los objetos almacenan datos en R.
¿Recuerdan que mencioné que los elementos dentro de los objetos tienen un tipo? Pues miren que todos los elementos en name
son del mismo tipo (llevan comillas porque son texto); y en age
hay puro número; los que hay en active
son todos de una clase que R denomina logical
(toman los valores TRUE
o FALSE
). Hay más tipos (clases, mejor dicho, como factor
, shape
, etcétera) que por hoy vamos a ignorar para no complicarnos demasiado.
Yo puedo recurrir a la función class()
y obtener como resultado la clase a la que pertenece un objeto; hay otras funciones similares, aunque no iguales, que capturan diferencias conceptuales que a nuestros efectos no son importantes pero es mejor que ustedes estén al tanto:
## Inspect
class(name)
#> [1] "character"
class(surname)
#> [1] "character"
class(birthday)
#> [1] "character"
class(age)
#> [1] "numeric"
class(active)
#> [1] "logical"
## Inspect
typeof(name)
#> [1] "character"
class(name)
#> [1] "character"
mode(name)
#> [1] "character"
typeof(iris)
#> [1] "list"
class(iris$Sepal.Length)
#> [1] "numeric"
¿Por qué es importante todo esto? Bueno, ya dije por qué: cuáles funciones es posible aplicar a un objeto depende enteramente de la estructura del objeto y del tipo de elementos que éste almacene.
Usaré tipo y clase más o menos de manera intercambiable; es una simplificación conveniente y necesaria. No son lo mismo, sin embargo (menos mal su distinción no es relevante para empezar a programar en R). La estructura sí es una cosa aparte; la estudiaremos en breve.
En determinadas circunstancias, uno puede cambiar la clase de los objetos. Por ejemplo, hay una clase específica para fechas: date
. Claramente el objeto birthday
debería ser de ese tipo, así que lo vamos a convertir:
## Transform
as.Date(birthday)
#> [1] "1990-07-18" "1987-06-17" "1951-06-16" "1966-06-30"
Ahora voy a verificar que la transformación sucedió:
## Inspect
class(birthday)
#> [1] "character"
No, no sucedió. Sigue siendo character
. ¿Por qué no cambió? No cambió porque, a pesar de que ejecuté correctamente as.Date(birthday)
, debí haber salvado el resultado. Y esto de salvar resultados es clave: otra fuente inagotable de errores para cualquier debutante es no salvar resultados, o, al contrario, salvar resultados inadvertidamente, salvarlos cuando no era esa la intención (por ejemplo, si hubiésemos hecho un cambio en el objeto birthday
que realmente no planeábamos llevar a cabo).
A efectos de guardar resultados, una opción es reescribir el nombre del objeto:
Ahora sí funcionó: transformé un objeto de una clase (character
) a otra (date
).
Llevo todo el texto insistiendo en que programar en R es fundamentalmente aplicar funciones a objetos; así, por ejemplo, existe una función para calcular el promedio, mean()
, y nada me impide aplicarle esa función al objeto age
para obtener la edad promedio:
## Compute
mean(age)
#> [1] 48.5
Ahora sé que estos sujetos tienen una edad promedio de 48.5 años. Ese código funcionó perfectamente bien porque age
almacena números: su clase es numeric
y, tal como lo abordamos atrás, tiene sentido sacar el promedio de un conjunto de números, ¿cierto?
Por el contrario, si aplicara la función mean()
al objeto name
, R generará un error (una advertencia, más exactamente) porque name
no almacena números sino palabras (su clase es character
) y, evidentemente, no tiene sentido buscar el promedio de un objeto que no está constituido por números:
## Compute
mean(name)
#> Warning in mean.default(name): argument is not numeric or
#> logical: returning NA
#> [1] NA
El ejemplo anterior demuestra, una vez más, que los objetos pueden contener elementos de una clase u otra, y que dependiendo de cuál sea su clase les es posible soportar unas funciones y no otras.
Hasta ahora, me he estado ocupando de la clase de los objetos, no de su estructura. Hablemos de la estructura de los objetos, pues.
Objetos como name
, age
y active
sólo pueden contener elementos de una única clase.
A los objetos que sólo pueden almacenar objetos de una misma clase se les conoce como vector
(o atomic vectors, para ponerlo exacto). Los vectores -la estructura más elemental en R- son colecciones ordenadas de elementos, como decir un contenedor. La función length()
arroja el número de elementos contenidos en un vector.
## Print
surname
#> [1] "Canelo" "Chocolatito" "Mano de Piedra"
#> [4] "Iron Mike"
## Compute
length(surname)
#> [1] 4
R tiene varias estructuras de objetos más. list
, por ejemplo, refiere a objetos que, al contrario de los vectores, sí pueden almacenar elementos de distintas clases:
Por cierto, aprecien cómo puedo partir el código a la altura de las comas para que se mire bonito:
Las listas, como la que acabo de crear, pueden contener elementos de varias clases distintas: un nombre y un apodo (character
), una edad (numeric
), una condición lógica (logical
), y de paso usé la función as.Date()
para que la fecha R la convirtiera de character
a date
.
Estamos aprendiendo que las listas son objetos heterogéneos (les entra cualquier dato, independientemente de su clase), mientras los vectores son objetos homogéneos (sólo aceptan datos de una misma clase). ¿Todo fácil, verdad?
Entonces los vectores y las listas tienen una característica que los hace diferentes (la que acabo de resaltar: un vector sólo almacena elementos de una misma clase, mientras una lista puede almacenar elementos de distintas clases). Pero los vectores y las listas poseen una característica en la que sí se parecen: ambos son unidimensionales. La unidimensionalidad es medio difícil de explicar en seco, así que voy a pasar a otra cosa por un segundo y luego retomo la idea.
Más allá de las listas y los vectores, otra estructura muy común en R es data frame
; de hecho, es la estructura más común: quienes trabajamos con R pasamos el día entero sobando data frames.
Es bien fácil crear un data frame y lo vamos a ejemplificar con exactamente los mismos datos que usamos hace un rato:
## Create data frame
df <- data.frame(
name = c("Saúl Álvarez", "Román González", "Roberto Durán", "Mike Tyson"),
surname = c("Canelo", "Chocolatito", "Mano de Piedra", "Iron Mike"),
birthday = c("1990-07-18", "1987-06-17", "1951-06-16", "1966-06-30"),
age = c(32, 35, 71, 56),
active = c(T, T, F, F)
)
## Print
df
#> name surname birthday age active
#> 1 Saúl Álvarez Canelo 1990-07-18 32 TRUE
#> 2 Román González Chocolatito 1987-06-17 35 TRUE
#> 3 Roberto Durán Mano de Piedra 1951-06-16 71 FALSE
#> 4 Mike Tyson Iron Mike 1966-06-30 56 FALSE
Haré una pausa para destacar cuatro hechos de máxima relevancia sobre la estructura de los objetos. Necesito que ahora mismo conglomeren toda su atención aquí porque las siguientes cuatro lecciones, si se asimilan bien, son el cheat code para que el proceso de aprendizaje adelante varias pantallas de golpe:
Primero, noten que cada estructura de objeto tiene su propia función creadora:
c()
para crear vectores,list()
para crear listas,data.frame()
para crear data frames.Segundo, observen (vayan al environment de R y den click en el objeto df para abrirlo) que los data frames poseen dos dimensiones: tienen filas (observaciones) y columnas (variables), como decir las spreadsheets de Microsoft Excel. Tener dos dimensiones es una característica que no tienen ni los vectores ni las listas, y no lo tienen porque que son unidimensionales (esto es lo que no pude explicar en seco hace unos minutos).
Tercero, ¿sí notaron que las columnas del data frame que creé arriba son todas de diferentes clases? Son
character
,numeric
,logical
,date.
O sea, los data frames y las listas son similares en ese aspecto: son tipos de objetos capaces de albergar múltiples clases de datos, algo que está vedado para los vectores.Cuarto, aunque los data frames son bidimensionales y pueden albergar columnas de variedad de clases, las columnas en sí mismas son (1) unidimensionales y (2) almacenan elementos de una única clase, lo que quiere decir que ¡las columnas de los data frames son vectores! El hecho de que los data frames sean algo así como racimos de vectores -esto es bastante evidente en nuestro ejemplo pues, de hecho, las columnas en nuestro data frame son exactamente los mismos vectores que antes fuimos creando cada uno por separado- es clave a la hora de someter nuestros datos a selecciones y transformaciones.
Bonus track: ¡Los data frames son listas de vectores! Pero por ahora no es importante comprender esto. La metáfora de los data frames como “racimos de vectores” es más que suficiente.
En resumen, R dispone de al menos tres estructuras de objetos, cada una propicia para ciertos usos y no otros, cada una compatible con ciertas funciones y no otras:
- Unidimensionales con elementos de una única clase:
vector
. - Unidimensionales con elementos de varias clases:
list
. - Bidimensionales con elementos de varias clases:
data frame
.
Por si se capta mejor con una tabla:
Un tipo nada más | Varios tipos | |
Una dimensión | Vectores | Listas |
Dos dimensiones | Matrices | Data frames |
Y de los vectores sale la estructura matrix
. La función matrix()
simplemente toma un vector y lo acomoda en dos dimensiones. Las matrices serán fundamentales para cualquier R user enfocado en Data Science porque hay muchísimos conceptos estadísticos que se expresan en álgebra de matrices.
En fin, así es como uno manipula datos en R: a veces manipulando una lista, a veces un data frame, a veces un vector. Dije “manipulando” como pude haber dicho “aplicando funciones”… Programar en R es aplicar funciones a objetos, y tanto la estructura de los objetos como el tipo de elementos que almacenen son los factores determinantes de cuáles funciones es válido aplicar a un objeto específico.
Tan mínima como parezcan las diferencias entre estructuras y tipos de objetos, no ser plenamente consciente de ellas desordenó mi proceso de aprendizaje y me costó tiempo valioso.
Consideremos este ejemplo: digamos que uno ya aprendió que la función length()
es la que se aplica para obtener el número de elementos almacenados en un objeto. Perfecto. Le aplico la función length()
al objeto (vector
) en el que había guardado cuatro nombres y el comando corre espléndidamente, me devuelve un 4
que me realiza y me convence de que nací para programar:
## Get the number of elements
length(name)
#> [1] 4
Otro día vuelvo a estar en una necesidad similar, pero esta vez el objeto que tengo es un data frame
, no un vector
, y lo que necesito es contarle las filas. Si uno es un novato programador que no se fija tanto en eso de tipos y estructuras y simplemente toma el objeto que le ponen por delante y le aplica la función que medio se sabe, es probable que el primer intento sea con la misma función length()
porque esa es la que uno recuerda que hace algo parecido a contarle las patas al animal:
## Get the number of rows
length(df)
#> [1] 5
Sin embargo, R devuelve un valor, 5
, que no corresponde al número de filas sino al de las columnas. Entonces todo se convierte en frustración, en bronca con uno mismo. ¿Por qué no da el resultado correcto una función que hace nada sí me sirvió para resolver una necesidad similar? ¿Por qué R es tan insufrible? Fácil: el problema aquí es que length()
no es exactamente una función para aplicar en data frames sino en vectores.
Dichosamente existe otra función, nrow()
, que sí es apta para data frames y que consigue bastante bien lo que yo tenía en mente, es decir, contar el número de filas:
## Get the number of rows
nrow(df)
#> [1] 4
La programación en R está llena de frustraciones como la anterior. Por eso he de insistir: siempre que se opera sobre un objeto, hay que tener completamente claro de qué estructura es tal objeto y de qué clase son los elementos que contiene. Si se trata de vectores, se utiliza la función class()
para determinar la clase de sus elementos. Si se trata de data frames, lo primordial es explorar la clase de las columnas pues las columnas son las variables.
Por eso siempre que inicio un trabajo de una vez creo esta función customGlimpse()
-que no la programé yo, de algún lado que no recuerdo la habré tomado, porque programar es en gran medida andar robando cosas de internet- para pedirle a R que me dé el detalle las columnas de un data frame cada vez que haga falta:
## Create function to display variables
customGlimpse <- function(df){
data.frame(
col_name = colnames(df),
col_index = 1:ncol(df),
col_class = sapply(df, class),
row.names = NULL
)
}
Ese código de arriba tiene mucha cosa que no es importante entender de momento. Pero miren qué bonito devuelve el nombre de la columna, su índice (i.e., qué número ocupa en el data frame) y la clase de sus elementos:
## Explore variables
customGlimpse(df)
#> col_name col_index col_class
#> 1 name 1 character
#> 2 surname 2 character
#> 3 birthday 3 character
#> 4 age 4 numeric
#> 5 active 5 logical
Ahora admirémosla aplicada al conjunto de datos starwars
del paquete tidyverse
:
## Explore variables
customGlimpse(starwars)
#> col_name col_index col_class
#> 1 name 1 character
#> 2 height 2 integer
#> 3 mass 3 numeric
#> 4 hair_color 4 character
#> 5 skin_color 5 character
#> 6 eye_color 6 character
#> 7 birth_year 7 numeric
#> 8 sex 8 character
#> 9 gender 9 character
#> 10 homeworld 10 character
#> 11 species 11 character
#> 12 films 12 list
#> 13 vehicles 13 list
#> 14 starships 14 list
Tremenda función. Por cierto, ¿sí notaron que la existencia de la función customGlimpse()
implica que R nos deja crear nuestras propias funciones?
4.3 Funciones
Sobre las funciones voy a divagar menos pues lo dicho hasta acá ya las ha iluminado bastante.
Las funciones se extraen de paquetes (los paquetes también pueden traer data sets, como starwars
) o uno mismo las puede crear (y customGlimpse()
es un claro ejemplo de una función que uno mismo programó). También R trae un montón de funciones cargadas desde su instalación, un tema que ampliaré en el próximo capítulo.
Además de ajustarse a la estructura y la clase del objeto al que se apliquen, las funciones requieren de ciertos ajustes internos. Para ello, las funciones poseen argumentos; los argumentos son los que acomodan la función a nuestro objetivo específico.
Algunos de los argumentos están definidos por default y otros es uno quien debe especificarlos. seq()
es una función que ejemplifica con claridad qué son los argumentos (from
, to
, by
):
## Create sequence from 1 to 10 with an increment of 2
seq(from = 1, to = 10, by = 2)
#> [1] 1 3 5 7 9
El output de esa función no ingresa al environment si no se lo asigno a un objeto. Si quiero guardar ese output entonces tendría que utilizar el operador <-
que ya hemos visto aparecer varias veces arriba:
## Create sequence from 1 to 10 with an increment of 2
seq <- seq(from = 1, to = 10, by = 2)
## Print
seq
#> [1] 1 3 5 7 9
Y de paso recordemos la otra forma de imprimir un objeto (encerrándolo todo entre paréntesis):
## Create sequence from 1 to 10 with an increment of 2
(seq <- seq(from = 1, to = 10, by = 2))
#> [1] 1 3 5 7 9
Si no sabemos cómo se usa una función, RStudio cuenta con un atajo para accesar a toda su documentación. En el caso de seq()
, simplemente habría que correr en la consola el comando ?seq
para desplegar una ventana con los pormenores de la función, entre ellos, sus argumentos.
La documentación de R puede ser un poco abrumadora y no siempre es clara, pero en general es excelente y es una de sus mejores características, esto gracias a que los requisitos para crear y dar mantenimiento a los paquetes son exigentes y ello resulta en que el ecosistema de R exhiba altos niveles de calidad.
??
funciona para búsquedas más generales. Pongamos que uno quiere explorar qué hay en R sobre estadística bayesiana, entonces conviene ejecutar ??bayes
para que nos lleven al menú temático.
Recordemos que podemos crear nuestras propias funciones:
## Create function
add <- function(one_number, any_other_number){ # R, take these inputs
sum <- one_number + any_other_number # Do this to them
return(sum) # And gimme this output!
}
## Compute
add(10, 12)
#> [1] 22
Y, por último, recordemos que uno debe siempre prestar atención a cuál es el return value de la función (en el caso anterior, un vector numérico). ¿Por qué? Ya se la saben: porque las funciones devuelven objetos, y qué se puede hacer luego con ese objeto depende enteramente de su estructura y su clase:
## Compute
sqrt(add(add(10, 10), add(6, 6)) + 4)
#> [1] 6
Ahora vamos a hablar de otro concepto medular: los operadores.
4.4 Operadores
Los operadores son, en realidad, funciones. Son funciones de uso tan frecuente que ameritó crearles una sintaxis más simple, un atajo.
4.4.1 <-
El assignment operator es para asignar objetos a nombres, de modo tal que luego podemos usar los nombres en su lugar:
## Assign
four <- 4
## Print, option A
(four <- 4)
#> [1] 4
## Print, option B
four
#> [1] 4
## Compute
sqrt(four)
#> [1] 2
Aunque en ciertas ocasiones =
también sirve para asignar, es mejor que nos quedemos con <-
pues es el que funciona en todos los casos. Reservemos =
para la asignación de los argumentos de una función; por ejemplo, cuando calculamos el promedio de un vector con missing values (valores faltantes: NA
), R nos va a esperar que explícitamente le indiquemos que omita los NA
; de lo contrario, nos arrojará otro NA
:
Ahora miren cómo sí funciona:
¿La diferencia? na.rm = TRUE
4.4.2 [], $
R cuenta con buen arsenal de sintaxis para accesar elementos específicos dentro de un objeto. Los indexing operators son grandes asistidores. Su uso es posible porque R indexa los elementos que componen un objeto. ¿Y qué significa eso? Significa que cada elemento tiene una posición, y a esa posición se puede accesar por su número.
La indexación (i.e., que cada elemento posee un número y que podemos sacarlo del objeto si lo llamamos por ese número) se aprecia mejor con un ejemplo:
## Create vector
some_letters <- c("j", "h", "x", "y", "l")
## Index
some_letters[1]
#> [1] "j"
some_letters[2]
#> [1] "h"
some_letters[3]
#> [1] "x"
some_letters[4]
#> [1] "y"
some_letters[5]
#> [1] "l"
R cuenta los elementos a partir de 1 -a diferencia de Python y básicamente cualquier otro lenguage de programación, que cuentan desde 0-. Arriba, empleamos []
para indexar con base en la posición de los elementos dentro del vector.
Atención con :
, una variedad de operador (integer-sequence operator) que nos asiste al hacer secuencias de números enteros y del que normalmente tomaremos ventaja para indexar varios elementos a la vez:
## Create integer sequences
14:21
#> [1] 14 15 16 17 18 19 20 21
21:14
#> [1] 21 20 19 18 17 16 15 14
-1:1
#> [1] -1 0 1
1:length(15:25)
#> [1] 1 2 3 4 5 6 7 8 9 10 11
## Index
some_letters[3:5]
#> [1] "x" "y" "l"
Ya aprendimos cómo se indexa un vector. Indexar un data frame es parecido: dado que los data frames poseen dos dimensiones, hemos de especificar la fila y la columna de interés, mediante una sintaxis tipo [fila, columna]
:
## Index
starwars[4, 1] # Row 4, Column 1
#> # A tibble: 1 × 1
#> name
#> <chr>
#> 1 Darth Vader
Si quisiera extraer toda la fila, el código sería este:
## Index
starwars[4, ]
#> # A tibble: 1 × 14
#> name height mass hair_…¹ skin_…² eye_c…³ birth…⁴ sex
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr>
#> 1 Darth … 202 136 none white yellow 41.9 male
#> # … with 6 more variables: gender <chr>, homeworld <chr>,
#> # species <chr>, films <list>, vehicles <list>,
#> # starships <list>, and abbreviated variable names
#> # ¹hair_color, ²skin_color, ³eye_color, ⁴birth_year
#> # ℹ Use `colnames()` to see all variable names
Y si quisiera toda la columna, son dos mis opciones. La primera es llamar a la columna por su número (|> head()
no es necesario; lo utilizo únicamente para que me imprima unos poquitos ejemplos en lugar de todas las filas) :
## Index
starwars[, 1] |> head()
#> # A tibble: 6 × 1
#> name
#> <chr>
#> 1 Luke Skywalker
#> 2 C-3PO
#> 3 R2-D2
#> 4 Darth Vader
#> 5 Leia Organa
#> 6 Owen Lars
O bien, puedo usar el operador $
para llamar a la columna por su nombre (no por su número):
## Index
starwars$name
#> [1] "Luke Skywalker" "C-3PO"
#> [3] "R2-D2" "Darth Vader"
#> [5] "Leia Organa" "Owen Lars"
#> [7] "Beru Whitesun lars" "R5-D4"
#> [9] "Biggs Darklighter" "Obi-Wan Kenobi"
#> [11] "Anakin Skywalker" "Wilhuff Tarkin"
#> [13] "Chewbacca" "Han Solo"
#> [15] "Greedo" "Jabba Desilijic Tiure"
#> [17] "Wedge Antilles" "Jek Tono Porkins"
#> [19] "Yoda" "Palpatine"
#> [21] "Boba Fett" "IG-88"
#> [23] "Bossk" "Lando Calrissian"
#> [25] "Lobot" "Ackbar"
#> [27] "Mon Mothma" "Arvel Crynyd"
#> [29] "Wicket Systri Warrick" "Nien Nunb"
#> [31] "Qui-Gon Jinn" "Nute Gunray"
#> [33] "Finis Valorum" "Jar Jar Binks"
#> [35] "Roos Tarpals" "Rugor Nass"
#> [37] "Ric Olié" "Watto"
#> [39] "Sebulba" "Quarsh Panaka"
#> [41] "Shmi Skywalker" "Darth Maul"
#> [43] "Bib Fortuna" "Ayla Secura"
#> [45] "Dud Bolt" "Gasgano"
#> [47] "Ben Quadinaros" "Mace Windu"
#> [49] "Ki-Adi-Mundi" "Kit Fisto"
#> [51] "Eeth Koth" "Adi Gallia"
#> [53] "Saesee Tiin" "Yarael Poof"
#> [55] "Plo Koon" "Mas Amedda"
#> [57] "Gregar Typho" "Cordé"
#> [59] "Cliegg Lars" "Poggle the Lesser"
#> [61] "Luminara Unduli" "Barriss Offee"
#> [63] "Dormé" "Dooku"
#> [65] "Bail Prestor Organa" "Jango Fett"
#> [67] "Zam Wesell" "Dexter Jettster"
#> [69] "Lama Su" "Taun We"
#> [71] "Jocasta Nu" "Ratts Tyerell"
#> [73] "R4-P17" "Wat Tambor"
#> [75] "San Hill" "Shaak Ti"
#> [77] "Grievous" "Tarfful"
#> [79] "Raymus Antilles" "Sly Moore"
#> [81] "Tion Medon" "Finn"
#> [83] "Rey" "Poe Dameron"
#> [85] "BB8" "Captain Phasma"
#> [87] "Padmé Amidala"
Ahora bien, miren que el output no es exactamente el mismo (hay que escoger cuál función emplear según la necesidad del momento):
La indexación habilita la posibilidad de borrar elementos o modificarlos:
## Print the original object
some_letters
#> [1] "j" "h" "x" "y" "l"
## Delete value
some_letters[-2]
#> [1] "j" "x" "y" "l"
## Delete more than one value
some_letters[-c(2, 3)]
#> [1] "j" "y" "l"
## Change value
some_letters[2] <- "M"
## Inspect
some_letters
#> [1] "j" "M" "x" "y" "l"
4.4.3 |>, %>%, +
Re importantes los pipe operators. Arriba ya hemos tomado ventaja de los pipes para anidar funciones, de modo tal que el output de una se convierte en el input de la siguiente:
## Wrangle data
df.wc |>
filter(NameTeam == "Costa Rica") |> # (1)
group_by(player) |> # (2)
summarize( # (3)
offersToReceiveSUM = sum(OffersToReceiveTotal
)) |>
arrange(desc(offersToReceiveSUM)) |> # (4)
head(5) # (5)
#> # A tibble: 5 × 2
#> player offersToReceiveSUM
#> <chr> <dbl>
#> 1 Joel CAMPBELL 136
#> 2 Celso BORGES 127
#> 3 Bryan OVIEDO 99
#> 4 Keysher FULLER 94
#> 5 Yeltsin TEJEDA 93
En este ejemplo, tomé un conjunto de datos del Mundial Qatar 2022 y anidé funciones:
filter()
para filtrar sólo los jugadores que corresponden a la Selección de Costa Rica.group_by()
para agrupar las estadísticas por jugador -la unidad de análisis de este conjunto de datos es jugador-por-partido, de modo tal que incluye, por ejemplo, una fila para Joel Campbell contra España, una para Joel contra Japón, y otra para Joel contra Alemania; en el ejercicio que estoy haciendo ahora mismo quiero agrupar los datos de cada jugador para crear una variable que agregue una medición en particular-.summarize()
ysum
para sumar -en este caso- todas las veces que cada jugador pidió la bola en los tres partidos, y este resultado lo asigné a una variable nueva -offersToReceiveSUM
–.arrange()
para ordenarlos de mayor a menor, como en un ranking.head()
, finalmente, para imprimir nada más que los primeros cinco, con lo cual creé el top 5 de jugadores de la Sele que más veces pidieron la bola durante el Mundial y, para sorpresa de nadie, el que más la pidió fue Joel Campbell.
A los pipes podemos darle uso más en corto:
Sin embargo, tal como lo ilustra el análisis de los jugadores de la Sele que más pidieron la bola en el Mundial, los pipes son particularmente útiles a la hora de someter nuestros datos a selecciones y transformaciones (data wrangling). Si no fuera por los pipes, el código se nos volvería muy confuso y error-prone y costaría montones leerlo.
Algunos paquetes anidan funciones con otro pipe distinto. Las funciones de ggplot2
, el icónico y muy poderoso paquete para visualizar datos, utiliza el operador +
:
## Plot
ggplot(mtcars, aes(x = as.factor(cyl), y = mpg)) +
geom_boxplot(fill = "pink") +
ggtitle("Feo, porque pulir visualizaciones toma DEMASIADO tiempo") +
theme_get()
Un último comentario: el pipe %>%
se ve más frecuentemente que el pipe |>
. Por razones técnicas que aquí no tienen lugar, les aconsejo quedarse con |>
pues ese es el que viene integrado en R (si no lo tienen, ¡actulicen R y RStudio!).
4.4.4 >, <, ==
Pues no hay mucho qué decir: estos operadores ejecutan operaciones lógicas:
## Compute
1 == 2
#> [1] FALSE
2^2 == 4
#> [1] TRUE
3 > 4
#> [1] FALSE
3 < 4
#> [1] TRUE
Antes vimos que =
es un operador para asignar nombres a objetos, de modo que para usarlo en compaciones lo escribimos doble: ==
. Este detalle toma rato asimilarlo y les va a deparar un significativo caudal de errores mientras tanto.
Miren la utilidad de estos operadores a la hora de filtrar datos:
## Filter
starwars |>
filter(height > 230) |>
arrange(height)
#> # A tibble: 2 × 14
#> name height mass hair_…¹ skin_…² eye_c…³ birth…⁴ sex
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr>
#> 1 Tarfful 234 136 brown brown blue NA male
#> 2 Yarael… 264 NA none white yellow NA male
#> # … with 6 more variables: gender <chr>, homeworld <chr>,
#> # species <chr>, films <list>, vehicles <list>,
#> # starships <list>, and abbreviated variable names
#> # ¹hair_color, ²skin_color, ³eye_color, ⁴birth_year
#> # ℹ Use `colnames()` to see all variable names
4.4.5 &, |, TRUE, FALSE
Podemos combinar los operadores de comparación con los operadores booleanos TRUE
, FALSE
, &
y |
para ejecutar filtrados más exigentes:
## Filter
starwars |>
filter(height > 200 & gender == "masculine" & mass > 135) |>
arrange(height) |>
select(name, height, gender, mass)
#> # A tibble: 3 × 4
#> name height gender mass
#> <chr> <int> <chr> <dbl>
#> 1 Darth Vader 202 masculine 136
#> 2 Grievous 216 masculine 159
#> 3 Tarfful 234 masculine 136
Le acabamos de encargar a R encontrar los sujetos que cumplieran estos tres atributos: más altos que cierto número, y de cierto género y más pesados que cierto otro número.
Aquí abajo, en cambio, le pedimos a R encontrar los sujetos que fuesen de una especie en particular o que tuviesen un nombre específico:
## Filter
starwars |>
filter(species == "Droid" | name == "Darth Vader") |>
select(species, name)
#> # A tibble: 7 × 2
#> species name
#> <chr> <chr>
#> 1 Droid C-3PO
#> 2 Droid R2-D2
#> 3 Human Darth Vader
#> 4 Droid R5-D4
#> 5 Droid IG-88
#> 6 Droid R4-P17
#> 7 Droid BB8
4.4.6 ~
El operador ~
significa “en función de” y es común encontrarlo en las funciones que corren regresiones:
## Compute
lm(mass ~ height, data = starwars)
#>
#> Call:
#> lm(formula = mass ~ height, data = starwars)
#>
#> Coefficients:
#> (Intercept) height
#> -13.8103 0.6386
Este es el output de una regresión lineal. Hay un capítulo entero sobre esta materia más adelante.
El orden de las variables es importante: la que está antes de ~
es la variable dependiente y todas las que vengan después son las variables independientes: Y ~ X
.
4.4.7 %in%, in
Estos operadores son perfectos para filtrar datos cuando tenemos varios targets. Sólo comparen las siguientes dos tareas:
## Filter
starwars |>
filter(name == "Luke Skywalker")
#> # A tibble: 1 × 14
#> name height mass hair_…¹ skin_…² eye_c…³ birth…⁴ sex
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr>
#> 1 Luke S… 172 77 blond fair blue 19 male
#> # … with 6 more variables: gender <chr>, homeworld <chr>,
#> # species <chr>, films <list>, vehicles <list>,
#> # starships <list>, and abbreviated variable names
#> # ¹hair_color, ²skin_color, ³eye_color, ⁴birth_year
#> # ℹ Use `colnames()` to see all variable names
## Filter
starwars |>
filter(name %in% c("Darth Maul", "Obi-Wan Kenobi", "Luke Skywalker"))
#> # A tibble: 3 × 14
#> name height mass hair_…¹ skin_…² eye_c…³ birth…⁴ sex
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr>
#> 1 Luke S… 172 77 blond fair blue 19 male
#> 2 Obi-Wa… 182 77 auburn… fair blue-g… 57 male
#> 3 Darth … 175 80 none red yellow 54 male
#> # … with 6 more variables: gender <chr>, homeworld <chr>,
#> # species <chr>, films <list>, vehicles <list>,
#> # starships <list>, and abbreviated variable names
#> # ¹hair_color, ²skin_color, ³eye_color, ⁴birth_year
#> # ℹ Use `colnames()` to see all variable names
Y son perfectos para ensamblar for loops, por citar apenas un ejemplo más:
Bonus track: los loops son un ingrediente esencial de la programación. Si observan con cuidado, van a ver que el código anterior nos sirvió para repetir la operación i ^ 2
para valores de i
que iban cambiando a lo largo del vector c(3, 4, 5)
; si no fuera por los loops, habríamos programado algo así:
## Compute
3 ^ 2
#> [1] 9
4 ^ 2
#> [1] 16
5 ^ 2
#> [1] 25
No parece difícil, podría pensar alguien. Pero tan sólo imagínense qué perdida de tiempo sería elevar al cuadrado todos los números del 1 al 50… En cambio, con un for loop es bien sencillo:
## Create empty vector
empty_vector <- vector(length = 50)
## Compute
for(i in 1:length(empty_vector)){
empty_vector[[i]] <- i ^ 2
}
## Print
empty_vector
#> [1] 1 4 9 16 25 36 49 64 81 100 121
#> [12] 144 169 196 225 256 289 324 361 400 441 484
#> [23] 529 576 625 676 729 784 841 900 961 1024 1089
#> [34] 1156 1225 1296 1369 1444 1521 1600 1681 1764 1849 1936
#> [45] 2025 2116 2209 2304 2401 2500
4.4.8 !, -
Los operadores de negación son para cambiar de TRUE
a FALSE
, y viceversa:
## Create vector
(this_vector_is_true <- TRUE)
#> [1] TRUE
## Transform
!this_vector_is_true
#> [1] FALSE
La negación nos simplifica la vida cuando, por poner un ejemplo, queremos encontrar los valores faltantes (NA
) dentro de un vector o data frame:
## Prepare data
example <- c(1, 2, NA, 4, NA)
## Determine whether there are NA
is.na(example)
#> [1] FALSE FALSE TRUE FALSE TRUE
## Single them out, option A
example[is.na(example)]
#> [1] NA NA
## Single them out, option B
example[which(is.na(example))]
#> [1] NA NA
## Remove them, option A
example[!is.na(example)]
#> [1] 1 2 4
## Remove them, option B
example[-which(is.na(example))]
#> [1] 1 2 4
## Save
(example_A <- example[!is.na(example)])
#> [1] 1 2 4
(example_B <- example[-which(is.na(example))])
#> [1] 1 2 4
Cuando nos enfrentamos a un data frame del cual nos interesan prácticamente todas las variables, y para descartar las pocas que no vamos a necesitar, podemos seleccionar en negativo esas poquitas que no son de nuestro interés en lugar de fastidiarnos la vida escribiendo el nombre de todas las variables que sí queremos conservar:
## Select
starwars |>
select(!c(films, vehicles, hair_color))
#> # A tibble: 87 × 11
#> name height mass skin_…¹ eye_c…² birth…³ sex gender
#> <chr> <int> <dbl> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke S… 172 77 fair blue 19 male mascu…
#> 2 C-3PO 167 75 gold yellow 112 none mascu…
#> 3 R2-D2 96 32 white,… red 33 none mascu…
#> 4 Darth … 202 136 white yellow 41.9 male mascu…
#> 5 Leia O… 150 49 light brown 19 fema… femin…
#> 6 Owen L… 178 120 light blue 52 male mascu…
#> 7 Beru W… 165 75 light blue 47 fema… femin…
#> 8 R5-D4 97 32 white,… red NA none mascu…
#> 9 Biggs … 183 84 light brown 24 male mascu…
#> 10 Obi-Wa… 182 77 fair blue-g… 57 male mascu…
#> # … with 77 more rows, 3 more variables: homeworld <chr>,
#> # species <chr>, starships <list>, and abbreviated
#> # variable names ¹skin_color, ²eye_color, ³birth_year
#> # ℹ Use `print(n = ...)` to see more rows, and `colnames()` to see all variable names
4.5 Más particularidades
4.5.1 Aritmética vectorizada
Si a R le pedimos una división entre dos vectores con el mismo número de elementos, R divide entre sí aquellos que tienen el mismo índice:
A veces los vectores no tienen el mismo número de elementos. Abajo, R divide cada elemento del vector largo entre el único elemento del otro vector:
Es decir, cuando los vectores son de diferente largo, R “recicla” el más pequeño para lograr que ambos vectores calcen:
## Compute
c(2, 6, 8, 9) / c(2, 3)
#> [1] 1 2 4 3
## Compute
c(2, 3) / c(2, 6, 8, 9)
#> [1] 1.0000000 0.5000000 0.2500000 0.3333333
No obstante, para poder reciclar vectores (como en los dos casos anteriores), R necesita que el largo de un vector (e.g., 4) sea múltiplo del largo del otro vector (e.g., 2). Si esto no se cumple, R nos lo hará saber:
4.5.2 Missing values
En R, los valores faltantes se representan como NA
, que quiere decir Not Available. Un NA
transmite que, pese a la expectativa de contar con un dato, lo desconocemos:
## Explore data
starwars |>
select(name, hair_color) |>
head()
#> # A tibble: 6 × 2
#> name hair_color
#> <chr> <chr>
#> 1 Luke Skywalker blond
#> 2 C-3PO <NA>
#> 3 R2-D2 <NA>
#> 4 Darth Vader none
#> 5 Leia Organa brown
#> 6 Owen Lars brown, grey
Con los NA
básicamente lo que R nos quiere decir es que no sabe qué va ahí. De ahí este comportamiento tan interesante:
## Compare
NA == NA
#> [1] NA
Capaz uno esperaría que NA == NA
sea TRUE
, pero R se abstiene de ello pues al no saber qué es NA
, NA
podría entonces ser cualquier cosa.
En teoría, debemos quitar los NA
antes de analizar los datos. Algunas veces corremos el riesgo de perder una significativa cantidad de observaciones. Esto es muy sensible y escapa de los alcances de R pre-introductorio. Sólo puedo recomendar que antes de tomar este tipo de decisiones (remover una fila, remover una columna) valoren con precaución cuántos datos van a perder y si otras estrategias son mejores (imputar los datos faltantes, remover la fila o la variable sólo para ciertos análisis, etcétera).
Por otro lado, NaN
significa Not a Number y podemos topar con uno de esos, por ejemplo, si intentamos hacer un cálculo que matemáticamente no es válido:
## Compute
0/0
#> [1] NaN
4.5.3 Importar datos
R pre-introductorio recurre mayoritariamente a data frames que vienen integrados en paquetes.
Cuando trabajamos en R por motivos labores o académicos, todas las veces necesitamos cargar archivos que están en la computadora. Y como eso puede complicarse un poco, es bueno presentar un par de estrategias.
Mi primera recomendación es trabajar en el marco de un R project.
Por ejemplo, dado que todo el material de R pre-introductorio lo mantenemos en la misma carpeta donde está ubicado el archivo pre_intro_r.Rproj, se nos hace re fácil navegar la computadora con tan sólo colocar el cursor justo en el medio de las comillas de una función que carga datos y pulsar la tecla tab:
## Load data
read_csv(file = "")
Inténtenlo ustedes. ¿Qué pasó? ¿Hermoso, cierto?
Otras buena opción puede ser here::here()
, que les recomiendo googlear, y un salvavidas es file.choose()
, una función que abre una ventana para que uno pueda buscar el archivo manualmente (pero esta es una mala práctica que sólo les aconsejaría si están en una urgencia):
## Load data
read_csv(file.choose())
Tómese en cuenta que hay muchas funciones para cargar datos y cuál llamar depende enteramente del tipo de archivo que planeen analizar. Los ejemplos anteriores usan read_csv()
, una función que nos hará el trabajo si el archivo es de tipo CSV. Sin importar el tipo de archivo, sepan desde ya que Google tiene todas las respuestas.
4.6 Un vocabulario
Regresemos al bloque de código del principio, el que tomé del libro de Hadley Wickham:
## Plot
ggplot(data = mpg) +
geom_point(mapping = aes(x = displ, y = hwy))
Ahora que podemos mirarlo con otros ojos, esto es lo que encontramos: tres funciones: ggplot()
, geom_point()
y aes()
; varios objetos (como mpg
, asignado al argumento data =
de la función ggplot()
); hay un operador +
que, en este caso, está creando un pipeline… Y el conjunto de todo ese código es el que produce la visualización.
O sea, ahora nos damos cuenta de que ese código diminuto estaba repleto de sintaxis que simplemente no podíamos distinguir antes… Ya la distinguimos, ya estamos en condiciones de leer los libros de Hadley Wickham y sacarles provecho de veras.