SOFTWARE II

Análisis de datos en R para Ciencia de Datos

Grado en Ciencia de Datos Aplicada • Javier Álvarez Liébana

¡Bienvenidos a R!

Dejad vuestras hojas de cálculo, Anacondas y SAS a un lado

¡Buenas!

Correo: . Despacho: 722 (3ª planta). Tutorías (curso 2024-2025): lunes y martes de 11 a 13.

  • Javier Álvarez Liébana, de Carabanchel. Licenciado en Matemáticas (UCM). Doctor en estadística (UGR)

  • Encargado de la visualización y análisis de datos covid del Principado de Asturias (2021-2022).

  • Miembro de la Sociedad Española de Estadística e IO y la Real Sociedad Matemática Española.

  • Formador de R en RTVE y ISCIII (actualmente) y el Ayuntamiento de Madrid (2024).

Actualmente, investigador y docente en la Facultad de Estadística de la UCM, e investigador colaborador del Network Science Institute de la Northeastern University (Boston). Divulgando por Twitter e Instagram

Objetivos

  • Quitarnos el miedo a los errores en programación → a programar se aprende programando

  • Entender los conceptos básicos de R desde cero → aprender a abstraer ideas y algoritmos

  • Utilidad de programar → flujos de trabajo reproducibles, transparentes y mantenibles

  • Introducción al análisis y preprocesamiento de datos → {tidyverse}

  • Adquirir habilidades en la visualización de datos{ggplot2}

Evaluación

  • Asistencia. No será obligatoria pero se valorará positivamente la participación.
  • Evaluación continua: se han planteado 4 entregas individuales (5%-15%-25%-30%), así como una entrega final grupal (25%) (entre 4 y 7 personas).
  • Examen final: solo obligatorio en caso de que en la continua individual (sin contar la grupal) no superes el 3.5 sobre 10 de nota media. En caso contrario el día de la entrega final grupal (día del examen oficial de enero) deberás realizar un examen final en lugar de la grupal.
  • Nota máxima: para tener una nota final superior al 9 deberás tener al menos un 8.5 de nota media en la evaluación continua individual (sin contar la grupal).

Planificación entregas

  • Entrega I (5%): 26 de septiembre (segunda mitad de clase, 60 minutos).

  • Entrega II (15%): 10 de octubre (120 minutos).

  • Entrega III (25%): 11 de noviembre (120 minutos).

  • Entrega IV (30%): aprox 19 de diciembre (120 minutos).

 

  • Entrega grupal (25%) o examen final: 20 de enero (10:00-13:30)

 

Se podrán modificar las fechas por saturación con otras asignaturas siempre y cuando el/la delegado/a lo solicite con más de 7 días de antelación.

Planificación (parte I)

CLASE SEMANA FECHAS TOPIC EJ. WORKBOOK ENTREGA
1 S1 9 sep Primeros pasos en R 💻
2 S1 12 sep Tipos básicos de datos y vectores 💻 💻 🐣
3 S2 16 sep Textos 💻 🐣
4 S2 19 sep Primeras bases de datos 💻 💻 🐣
5 S3 26 sep Quarto y entrega I 🐣 🎯 5%
6 S4 30 sep Estructuras de control 💻 💻 🐣
7 S4 3 oct Funciones 💻 🐣
8 S5 7 oct Tidy data 💻 🐣
9 S5 10 oct Entrega II 🎯 15%
10 S6 14 oct Tidyverse: filas 💻 💻 🐣
11 S6 17 oct Tidyverse: columnas 💻 💻 🐣 🐣 🐣
12 S7 21 oct Tidyverse: resúmenes 💻 🐣 🐣
13 S7 24 oct Joins. Import/export 💻 💻 🐣 🐣

Planificación (parte II)

CLASE SEMANA FECHAS TOPIC EJ. WORKBOOK ENTREGA
14-15-16 S8 28 y 31 oct Concurso
17-18 S9 4-7 nov Listas. Casos reales 💻 🐣 🐣
18 S9 7 nov Casos reales 🐣 🐣 🐣
19 S10 11 nov Entrega III 🎯 25%
20 S10 14 nov Intro al dataviz
21 S11 18 nov ggplot1
22 S11 21 nov ggplot2
23 S12 25 nov ggplot3
24 S12 28 nov ggplot4
25 S13 2 dic casos prácticos (factores)
26 S13 5 dic inferencia
27 S14 9 dic casos prácticos (r base también)
28 S14 12 dic sql + arrow
29 S15 16 dic
30 S15 19 dic Entrega IV 🎯 30%

Materiales

 

Clase 1: primeros pasos

Instalando R y RStudio. Primeros pasos. Scripts y proyectos

Requisitos

Para el curso los únicos requisitos serán:

  1. Conexión a internet (para la descarga de algunos datos y paquetes).
  1. Instalar R: será nuestro lenguaje. La descarga la haremos (gratuitamente) desde https://cran.r-project.org/

R vs RStudio

Programaremos como escribimos (castellano, por ejemplo) → R es lenguaje

  • Necesitaremos una gramática, un lenguaje (R)
  • Y un entorno, por ejemplo un Word (RStudio), para escribirlo

Instalación de R

El lenguaje R será nuestra gramática y ortografía (nuestras reglas de juego)

  • Paso 1: entra en https://cran.r-project.org/ y selecciona tu sistema operativo.

  • Paso 2: para Mac basta con que hacer click en el archivo .pkg, y abrirlo una vez descargado. Para sistemas Windows, debemos clickar en install R for the first time y después en Download R for Windows. Una vez descargado, abrirlo como cualquier archivo de instalación.

  • Paso 3: abrir el ejecutable de instalación.

Cuidado

Siempre que tengas que descargar algo de CRAN (ya sea el propio R o un paquete), asegúrate de tener conexión a internet.

Primera operación

Para comprobar la instalación, tras abrir R, deberías ver el R GUI (Graphical User Interface) con una pantalla blanca similar a esta (consola).

Primer código: a una variable llamada a le asignaremos el valor 1 (escribiremos el código en la consola y daremos «enter»). Tras ello haremos la suma a + b.

a <- 1

Primera operación

Para comprobar la instalación, tras abrir R, deberías ver el R GUI (Graphical User Interface) con una pantalla blanca similar a esta (consola).

Primer código: a una variable llamada a le asignaremos el valor 1 (escribiremos el código en la consola y daremos «enter»). Tras ello haremos la suma a + b.

a <- 1
b <- 2

Primera operación

Para comprobar la instalación, tras abrir R, deberías ver el R GUI (Graphical User Interface) con una pantalla blanca similar a esta (consola).

Primer código: a una variable llamada a le asignaremos el valor 1 (escribiremos el código en la consola y daremos «enter»). Tras ello haremos la suma a + b.

a <- 1
b <- 2
a + b
[1] 3

Fíjate que…

En la consola aparece un número [1]: simplemente es un contador de elementos (como contar filas en un Word)

Instalación de R Studio

RStudio será el Word que usaremos para escribir (lo que se conoce como un IDE: entorno integrado de desarrollo).

  • Paso 1: entra la web oficial de RStudio (ahora llamado Posit) y selecciona la descarga gratuita.

  • Paso 2: selecciona el ejecutable que te aparezca acorde a tu sistema operativo.

  • Paso 3: tras descargar el ejecutable, hay que abrirlo como otro cualquier otro y dejar que termine la instalación.

Organización de RStudio

Al abrir RStudio seguramente tengas tres ventanas:

  • Consola: es el nombre para llamar a la ventana grande que te ocupa buena parte de tu pantalla. Prueba a escribir el mismo código que antes (la suma de las variables) en ella. La consola será donde ejecutaremos órdenes y mostraremos resultados.

Organización de RStudio

Al abrir RStudio seguramente tengas tres ventanas:

  • Environment: la pantalla pequeña (puedes ajustar los márgenes con el ratón a tu gusto) que tenemos en la parte superior derecha. Nos mostrará las variables que tenemos definidas.

Organización de RStudio

Al abrir RStudio seguramente tengas tres ventanas:

  • Panel multiusos: la ventana que tenemos en la parte inferior derecha no servirá para buscar ayuda de funciones, además de para visualizar gráficos.

¿Qué es R? ¿Por qué R?

¿Qué es R? ¿Por qué R?

R es la evolución del trabajo de los laboratorios Bell con el lenguaje S, que fue llevado al mundo del software libre por Ross Ihaka y Robert Gentleman en los años 90. La version R 1.0.0 se publicó el 29 de febrero de 2000.

¿Qué es R? ¿Por qué R?

R es el lenguaje estadístico por excelencia, creado por y para estadísticos/as, con 6 ventajas fundamentales frente a Excel, SAS, Stata o SPSS:

  • Lenguaje de programación: la obviedad → análisis replicables
  • Gratuito: la filosofía de la comunidad de R es el compartir código bajo copyleftuso ético de dinero y algoritmos
  • Software libre: no solo es gratis sino que permite acceder libremente a código ajeno, incluso al propio código fuenteflexibilidad y transparencia (Free and Open Source Software FOSS)

¿Qué es R? ¿Por qué R?

R es el lenguaje estadístico por excelencia, creado por y para estadísticos/as, con 6 ventajas fundamentales frente a Excel, SAS, Stata o SPSS:

  • Lenguaje modular: hemos instalado lo mínimo, pero existen códigos de otras personas que podemos reusar (casi 20 000 paquetes) → ahorro de tiempo e innovación inmediata
  • Lenguaje de alto nivel: facilita la programación (como Python) → menor curva de aprendizaje
  • Comunidad y empleabilidad: junto con Python es el lenguaje más utilizado en el campo de la estadística y la ciencia de datos en investigación, docencia, empresas (Línea Directa, Mapfre, Telefónica, Orange, Apple, Spotify, Netflix, El País, Civio, HP, etc) y organismos públicos (ISCIII, CNIC, CNIO, INE, IGN, CIS, CEO, DGT, AEMET, RTVE, etc)

¿Por qué programar?

  • Automatizar → te permitirá automatizar tareas recurrentes.

  • Replicabilidad → podrás replicar tu análisis siempre de la misma manera.

  • Flexibilidad → podrás adaptar el software a tus necesidades.

  • Transparencia → ser auditado por la comunidad.

Idea fundamental: paquetes

Una de las ideas claves de R es el uso de paquetes: códigos que otras personas han implementado para resolver un problema

  • Instalación: descargamos los códigos de la web (necesitamos internet) → comprar un libro, solo una vez (por ordenador)
install.packages("ggplot2")
  • Carga: con el paquete descargado, indicamos qué paquetes queremos usar cada vez que abramos RStudiotraer el libro de la estantería
library(ggplot2)

Idea fundamental: paquetes

Una vez instalado, hay dos manera de usar un paquete (traerlo de la estantería)

  • Paquete entero: con library(), usando el nombre del paquete sin comillas, cargamos en la sesión todo el libro
library(ggplot2)
  • Funciones concretas usando paquete::funcion le índicamos que solo queremos una página concreta de ese libro
ggplot2::geom_point()

Te vas equivocar

Durante tu aprendizaje va a ser muy habitual que las cosas no salgan a la primera → te vas equivocar. No solo será importante asumirlo sino que es importante leer los mensajes de error para aprender de ellos.

  • Mensajes de error: precedidos de «Error in…» y serán aquellos fallos que impidan la ejecución
"a" + 1 
Error in "a" + 1: non-numeric argument to binary operator
  • Mensajes de warning: precedidos de «Warning in…» son los (posibles) fallos más delicados ya que son incoherencias que no impiden la ejecución
# Ejecuta la orden pero el resultado es NaN, **Not A Number**, un valor que no existe
sqrt(-1)
Warning in sqrt(-1): NaNs produced
[1] NaN

Scripts (documentos .R)

Un script será el documento en el que programamos, nuestro archivo .doc (aquí con extensión .R) donde escribiremos las órdenes. Para abrir nuestro primero script, haz click en el menú en File < New File < R Script.

Cuidado

Es importante no abusar de la consola: todo lo que no escribas en un script, cuando cierres, lo habrás perdido.

Cuidado

R es case-sensitive: es sensible a mayúsculas y minúsculas por lo que x y X representa variables distintas.

Ejecutando el primer script

Ahora tenemos una cuarta ventana: la ventana donde escribiremos nuestros códigos. ¿Cómo ejecutarlo?

  1. Escribimos el código a ejecutar.
  1. Guardamos el archivo .R haciendo click en Save current document.
  1. El código no se ejecuta salvo que se lo indiquemos. Tenemos tres opciones de ejecutar un script:
  • Copiar y pegar en consola.
  • Seleccionar líneas y Ctrl+Enter
  • Activar Source on save a la derecha de guardar: no solo guarda sino que ejecuta el código completo.

Sé organizado: proyectos

De la misma manera que en el ordenador solemos trabajar de manera ordenada por carpetas, en RStudio podemos hacer lo mismo para trabajar de manera eficaz creando proyectos.

Un proyecto será una «carpeta» dentro de RStudio, de manera que nuestro directorio raíz automáticamente será la propia carpeta de proyecto (pudiendo pasar de un proyecto a otro con el menu superior derecho).

Podemos crear uno en una carpeta nueva o en una carpeta ya existente.

💻 Tu turno

📝 Crea en tu ordenador una carpeta de la asignatura y crea dentro de ella el proyecto de RStudio: es ahí donde vas a guardar todo lo que hagamos a lo largo de este curso.Tras crear el proyecto tendrás un archivo R Project. A continuación crea en dicha carpeta dos subcarpetas: datos (es ahí donde irás guardando los distintos datasets que usaremos) y scripts (es ahí donde irás guardando los archivos .R de cada clase)

📝 Dentro del proyecto crea un script ejercicios-clase1.R (dentro de la carpeta scripts). Una vez creado define en él una variable de nombre a y cuyo valor sea -1. Ejecuta el código de las 3 maneras explicadas.

Código
a <- -1

📝 Añade debajo otra línea para definir una variable b con el valor 5. Tras ello guarda la multiplicación de ambas variables. Ejecuta el código como consideres.

Código
b <- 5
a * b # sin guardar
multiplicacion <- a * b # guardado

📝 Modifica el código inferior para definir dos variables c y d, con valores 3 y -1. Tras ello divide las variables y guarda el resultado.

c <- # deberías asignarle el valor 3
d <- # deberías asignarle el valor -1
Código
c <- 3
d <- -1
c / d # sin guardar
division <- c / d # guardado

📝 Asigna un valor positivo a x y calcula su raíz cuadrada; asigna otro negativo y y calcula su valor absoluto con la función abs().

Código
x <- 5
sqrt(x)

y <- -2
abs(y)

Toma nota

Comandos como sqrt(), abs() o max() son lo que llamamos funciones: líneas de código que hemos «encapsulado» bajo un nombre, y dado unos argumentos de entrada, ejecuta las órdenes (una especie de atajo). En las funciones los argumentos irán SIEMPRE entre paréntesis

📝 Usando la variable x ya definida, completa/modifica el código inferior para guardar en una nueva variable z el resultado guardado en x menos 5.

z <- ? - ? # completa el código
z
Código
z <- x - 5
z

📝 Define una variable x y asígnale el valor -1. Define otra y y asígnale el valor 0. Tras ello realiza las operaciones a) x entre y; b) raíz cuadrada de x. ¿Qué obtienes?

Código
x <- -1
y <- 0

x / y
sqrt(x)

📝 Escribe el código inferior en tu script. ¿Por qué crees que no funciona?

x <- -1
y <- 0

X + y
Error: object 'X' not found

Clase 2: primeros datos y vectores

¿Qué tipos de celdas (datos) existen? Concatenando celdas: vectores

De la CELDA a la TABLA

¿Qué tipo de dato podemos tener en cada celda de una tabla?

  • Celda: dato individual de un tipo concreto.
  • Variable: concatenación de valores del mismo tipo (vectores en R).
  • Matriz: concatenación de variables del mismo tipo y longitud.
  • Tabla: concatenación de variables de distinto tipo pero igual longitud
  • Lista: concatenación de variables de distinto tipo y distinta longitud

Pero antes…buenas prácticas

Antes de seguir, es importante que sepas algo cuánto antes: empezar en la programación puede ser frustrante

Al igual que cuando aprendes un idioma nuevo, el primer obstáculo a solventar no es tanto qué decir sino cómo decirlo de manera correcta. Y en R pasa lo mismo, así que vamos a normalizar nuestra forma de programar lo máximo posible para evitar errores futuros.

  • Tip 1: asignar, evaluar y comparar no es lo mismo. Si te has fijado en R estamos usando <- para asignar valores a variables. Usaremos = para evaluar argumentos en funciones y == para saber si dos elementos son iguales.
x <- 1 # asignar
x = 1 # evaluar
x == 1 # comparar

Pero antes…buenas prácticas

  • Tip 2: programa como escribes. Al igual que cuando redactas en castellano, acostúmbrate a incorporar espacios y saltos de línea paranoquedarteciego (es una buena práctica y no un requisito porque R no procesa los espacios)
x <- 1 # óptimo
x<-1 # regu
x<- 1 # peor (decídete)
  • Tip 3: no seas caótico/a, estandariza nombres. Acostúmbrate siempre a nombrar las variables de la misma manera. El único requisito es que debe empezar siempre por una letra (y sin tildes). La forma más recomendable es la conocida como snake_case
variable_en_modo_snake_case
otraFormaMasDificilDeLeer
hay.gente.que.usa.esto
Incluso_Haygente.Caotica_que.NoMereceNuestraATENCION

Pero antes…buenas prácticas

  • Tip 4: facilita la lectura y escritura, pon límites. En Tools < Global Options puedes personalizar algunas opciones de RStudio. En Code < Display podemos indicarle en Show margin que los scripts nos muestren un margen “imaginario” (no interacciona con el código) para “forzarnos” a realizar un salto de línea.

Pero antes…buenas prácticas

  • Tip 5: el tabulador es tu mejor amigo. En RStudio tenemos una herramienta maravillosa: si escribes parte del nombre de una variable o función y tabulas, RStudio te autocompleta

Pero antes…buenas prácticas

  • Tip 6: ni un paréntesis soltero. Siempre que abras un paréntesis deberás cerrarlo. Para facilitar esta tarea entra en Tools < Global Options < Code < Display y activa la opción Rainbow parentheses

Pero antes…buenas prácticas

  • Tip 7: fíjate en el lateral izquierdo. No solo podrás ver la línea de código por la que vas sino que, en caso de estar cometiendo un error de sintaxis, el propio RStudio te avisará.
  • Tip 8: intenta trabajar siempre por proyectos (para esta clase, crea un script clase2.R en el proyecto que creamos en la anterior clase)

 

Ver más tips en https://r4ds.had.co.nz/workflow-basics.html#whats-in-a-name

Celdas: tipos de datos

¿Existen variables más allá de los números en la ciencia de datos? Piensa por ejemplo en los datos que podrías guardar de una persona:

  • La edad o el peso será un número.
edad <- 33
  • Su nombre será una cadena de texto (conocida como string o char).
nombre <- "javi"
  • A la pregunta «¿estás matriculado en la Facultad?» la respuesta será lo que llamamos una variable lógica (TRUE si está matriculado o FALSE en otro caso).
matriculado <- TRUE
  • Su fecha de nacimiento será precisamente eso, una fecha.

Variables numéricas

El dato más sencillo (ya lo hemos usado) serán las variables numéricas. Para saber la clase de dato en R de una variable tenemos la función class()

a <- 5

Variables numéricas

El dato más sencillo (ya lo hemos usado) serán las variables numéricas. Para saber la clase de dato en R de una variable tenemos la función class()

a <- 5
class(a)

Para saber su tipología (naturaleza o formato) variable tenemos typeof()

typeof(1) # 1 pero almacenado como un valor real (double, con decimales)
[1] "double"
typeof(as.integer(1)) # 1 pero almacenado como un entero.
[1] "integer"

Fíjate que…

En R tenemos una colección de funciones que empiezan por as.x() y que sirven como funciones de conversión: un dato que era de un tipo, lo convertimos a tipo x.

Variables numéricas

Además de los números «normales» tendremos el valor más/menos infinito codificado como Inf o -Inf

1/0
[1] Inf
-1/0
[1] -Inf

Y valores que no son números reales not a number (indeterminaciones, complejos, etc) codificado como NaN

0/0
[1] NaN
sqrt(-2)
[1] NaN

Variables numéricas

Con las variables numéricas podemos realizar las operaciones aritméticas de una calculadora: sumar (+)…

a + b
[1] 7

…raíz cuadrada (sqrt())…

sqrt(a)
[1] 2.236068

… potencias (^2, ^3)…

a^2
[1] 25

…valor absoluto (abs()), etc.

abs(a)
[1] 5

Variables de texto

Imagina que además de la edad de una persona queremos guardar su nombre: ahora la variable será de tipo character

nombre <- "Javier"
class(nombre)
[1] "character"

Las cadenas de texto son un tipo con el que obviamente no podremos hacer operaciones aritméticas (sí otras operaciones como pegar o localizar patrones).

nombre + 1 # error al sumar número a texto
Error in nombre + 1: non-numeric argument to binary operator

Recuerda que…

Las variables de tipo texto (character o string) van SIEMPRE entre comillas: no es lo mismo TRUE (valor lógico, binario) que "TRUE" (texto).

Primera función: paste

Como hemos comentado R llamaremos función a un trozo de código encapsulado bajo un nombre, y que depende de unos argumentos de entrada. Nuestra primera función será paste(): dadas dos cadenas de texto nos permite pegarlas.

paste("Javier", "Álvarez")
[1] "Javier Álvarez"

Fíjate que por defecto nos pega las cadenas con un espacio, pero podemos añadir un argumento opcional para indicarle el separador (en sep = ...).

paste("Javier", "Álvarez", sep = "*")
[1] "Javier*Álvarez"

Primera función: paste

¿Cómo saber qué argumentos necesita una función? Escribiendo en consola ? paste te aparecerá una ayuda en el panel multiusos.

En dicha ayuda podrás ver en su cabecera que argumentos ya tiene asignados por defecto la función

Existe una función similar llamada paste0() que pega por defecto con sep = "" (sin nada).

paste0("Javier", "Álvarez")
[1] "JavierÁlvarez"

Primera función: paste

Los argumentos (y su detalle) también pueden ser consultado tabulando (detras una coma).

Funciones: argumentos por defecto

Es muy importante entender el concepto de argumento por defecto de una función en R: es un valor que la función usa pero a veces podemos no ver porque ya tiene un valor asignado.

# Hacen lo mismo
paste("Javier", "Álvarez")
[1] "Javier Álvarez"
paste("Javier", "Álvarez", sep = " ")
[1] "Javier Álvarez"

Toma nota

El operador = lo reservaremos para asignar argumentos dentro de funciones. Para todas las demás asignaciones usaremos <-

Primer paquete: glue

Una forma más intuitiva de trabajar con textos es usar el paquete {glue}: lo primero que haremos será «comprar el libro» (si nunca lo hemos hecho). Tras ello cargamos el paquete

install.packages("glue") # solo la primra vez
library(glue)

Con la función glue() de dicho paquete podemos usar variables dentro de cadenas de texto. Por ejemplo, «la edad es de … años», donde la edad está guardada en una variable.

edad <- 33
glue("La edad es de {edad} años")
La edad es de 33 años

Dentro de las llaves también podemos ejecutar operaciones

unidades <- "días"
glue("La edad es de {edad * 365} {unidades}")
La edad es de 12045 días

Variables lógicas

Otro tipo fundamental serán las variables lógicas o binarias (dos valores):

  • TRUE: verdadero guardado internamente como un 1.
  • FALSE: falso guardado internamente como un 0.
soltero <- TRUE # ¿Es soltero? --> SÍ
class(soltero)
[1] "logical"

Dado que internamente están guardados como variables binarias, podemos realizar operaciones aritméticas con ellas

2 * TRUE
[1] 2
FALSE - 1
[1] -1

Variables lógicas

Como veremos en breve, las variables lógicas en realidad puede tomar un tercer valor: NA o dato ausente, representando las siglas de not available, y será muy habitual encontrarlo dentro de una base de datos.

ausente <- NA
ausente + 1
[1] NA

Importante

Las variables lógicas NO son variables de texto: "TRUE" es un texto, TRUE es un valor lógico.

TRUE + 1
[1] 2
"TRUE" + 1
Error in "TRUE" + 1: non-numeric argument to binary operator

Condiciones lógicas

Los valores lógicos suelen ser resultado de evaluar condiciones lógicas. Por ejemplo, imaginemos que queremos comprobar si una persona se llama Javi.

nombre <- "María"

Con el operador lógico == preguntamos sí lo que tenemos guardado a la izquierda es igual que lo que tenemos a la derecha: es una pregunta

nombre == "Javi"
[1] FALSE

Con su opuesto != preguntamos si es distinto.

nombre != "Javi"
[1] TRUE

Fíjate que…

No es lo mismo <- (asignación) que == (estamos preguntando, es una comparación lógica).

Condiciones lógicas

Además de las comparaciones «igual a» frente «distinto», también comparaciones de orden como menor que <, mayor que >, <= o >=.

¿Tiene la persona menos de 32 años?

edad <- 34
edad < 32 # ¿Es la edad menor de 32 años?
[1] FALSE

¿La edad es mayor o igual que 38 años?

edad >= 38
[1] FALSE

¿El nombre guardado es Javi?

nombre <- "Javi"
nombre == "Javi"
[1] TRUE

Variables de fecha

Un tipo de datos muy especial: los datos de tipo fecha.

fecha_char <- "2021-04-21"

Parece una simple cadena de texto pero debería representar un instante en el tiempo. ¿Qué debería suceder si sumamos un 1 a una fecha?

fecha_char + 1
Error in fecha_char + 1: non-numeric argument to binary operator

Las fechas NO pueden ser texto: debemos convertir la cadena de texto a fecha.

 

Para trabajar con fechas usaremos el paquete {lubridate}, que deberemos instalar antes de poder usarlo.

install.packages("lubridate")

Variables de fecha

Una vez instalado, de todos los paquetes (libros) que tenemos, le indicaremos que nos cargue ese concretamente.

library(lubridate) # instala si no lo has hecho

Para convertir a tipo fecha usaremos la función as_date() del paquete {lubridate} (por defecto en formato yyyy-mm-dd)

 

# ¡no es una fecha, es un texto!
fecha_char + 1
Error in fecha_char + 1: non-numeric argument to binary operator
class(fecha_char)
[1] "character"
fecha <- as_date("2023-03-28")
fecha + 1
[1] "2023-03-29"
class(fecha)
[1] "Date"

Variables de fecha

En as_date() el formato de fecha por defecto es yyyy-mm-dd así si la cadena de texto no se introduce de manera adecuada…

as_date("28-03-2023")
[1] NA

Para cualquier otro formato debemos especificarlo en el argumento opcional format = ... tal que %d representa días, %m meses, %Y en formato de 4 años y %y en formato de 2 años.

as_date("28-03-2023", format = "%d-%m-%Y")
[1] "2023-03-28"
as_date("28-03-23", format = "%d-%m-%y")
[1] "2023-03-28"
as_date("03-28-2023", format = "%m-%d-%Y")
[1] "2023-03-28"
as_date("28/03/2023", format = "%d/%m/%Y")
[1] "2023-03-28"

Variables de fecha

En dicho paquete tenemos funciones muy útiles para manejar fechas:

  • Con today() podemos obtener directamente la fecha actual.
today()
[1] "2024-11-17"
  • Con now() podemos obtener la fecha y hora actual
now()
[1] "2024-11-17 21:48:22 CET"
  • Con year(), month() o day() podemos extraer el año, mes y día
fecha <- today()
year(fecha)
[1] 2024
month(fecha)
[1] 11

Resúmenes de paquetes

Amplia contenido

Tienes un resumen en pdf de los paquetes más importantes en la carpeta correspondiente en el campus

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Define una variable que guarde tu edad (llamada edad) y otra con tu nombre (llamada nombre)

Código
edad <- 33
nombre <- "Javi"

📝 Comprueba con dicha variable edad si NO tiene 60 años o si se llama "Ornitorrinco" (debes obtener variables lógicas como resultado)

Código
edad != 60 # distinto de
nombre == "Ornitorrinco" # igual a

📝 ¿Por qué el código inferior da error?

edad + nombre
Error in edad + nombre: non-numeric argument to binary operator

📝 Define otra variable llamada hermanos que responda la pregunta «¿tienes hermanos?» y otra variable que almacene tu fecha de nacimiento (llamada fecha_nacimiento).

Código
hermanos <- TRUE

library(lubridate) # sino lo tenías ya cargado
fecha_nacimiento <- as_date("1989-09-10")

📝 Define otra variable con tus apellidos (llamada apellidos) y usa glue() para tener, en una sola variable llamada nombre_completo, tu nombre y apellidos separando nombre y apellido por una coma

Código
apellidos <- "Álvarez Liébana"
glue("{nombre}, {apellidos}")

📝 De fecha_nacimiento extrae el mes.

Código
month(fecha_nacimiento)

📝 Calcula los días que han pasado desde la fecha de tu nacimiento hasta hoy (con la fecha de nacimiento definida en el ejercicio 4).

Código
today() - fecha_nacimiento

Vectores: concatenar

Cuando trabajamos con datos normalmente tendremos columnas que representan variables: llamaremos vectores a una concatenación de celdas (valores) del mismo tipo (lo que sería una columna de una tabla).

La forma más sencilla es con el comando c() (c de concatenar), y basta con introducir sus elementos entre paréntesis y separados por comas

edades <- c(32, 27, 60, 61)
edades
[1] 32 27 60 61

Consejo

Un número individual x <- 1 (o bien x <- c(1)) es en realidad un vector de longitud uno –> todo lo que sepamos hacer con un número podemos hacerlo con un vector de ellos.

Vectores: concatenar

Como ves ahora en el environment tenemos una colección de elementos guardada

edades
[1] 32 27 60 61

La longitud de un vector se puede calcular con length()

length(edades)
[1] 4

También podemos concatenar vectores entre sí (los repite uno tras otro)

c(edades, edades, 8)
[1] 32 27 60 61 32 27 60 61  8

Secuencias numéricas

El vector más famoso será el de tipo numérico, y en concreto, las conocidas como secuencias numéricas (por ejemplo, los días del mes), usadas para, entre otras cosas, indexar bucles.

El comando seq(inicio, fin) nos permite crear una secuencia numérica desde un elemento inicial hasta uno final, avanzando de uno en uno.

seq(1, 31)
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31

Fíjate que si hacemos eso con caracteres no podremos hacerlo ya que no hay un orden a priori entre textos.

"a":"z"
Error in "a":"z": NA/NaN argument

Secuencias numéricas

Un atajo es el comando 1:n, que nos devuelve lo mismo que seq(1, n)

1:7
[1] 1 2 3 4 5 6 7

Si el elemento inicial es mayor que el final, entenderá que la secuencia es en orden decreciente.

7:-3
 [1]  7  6  5  4  3  2  1  0 -1 -2 -3

También podemos definir otro tipo de distancia (paso) entre consecutivos con el argumento by = ...

seq(1, 7, by = 0.5) # secuencia desde 1 a 7 de 0.5 en 0.5
 [1] 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0

Secuencias numéricas

Otras veces nos interesará definir una secuencia con una longitud concreta

seq(1, 50, l = 7) # secuencia desde 1 a 50 de longitud 7
[1]  1.000000  9.166667 17.333333 25.500000 33.666667 41.833333 50.000000

Incluso puede nos interese generar un vector de n elementos repetidos

rep(0, 7) # vector de 7 ceros
[1] 0 0 0 0 0 0 0

Dado que internamente son guardados como números también podremos hacer esto con fechas.

seq(as_date("2023-09-01"), as_date("2023-09-10"), by = 1)
 [1] "2023-09-01" "2023-09-02" "2023-09-03" "2023-09-04" "2023-09-05"
 [6] "2023-09-06" "2023-09-07" "2023-09-08" "2023-09-09" "2023-09-10"

Vectores de caracteres

Un vector es una concatenación de elementos del mismo tipo, pero no tienen porque ser necesariamente números. Vamos a crear una frase de ejemplo.

frase <- "Me llamo Javi"
frase
[1] "Me llamo Javi"
length(frase)
[1] 1

En el caso anterior no era un vector, era un solo elemento de texto. Para crear un vector debemos usar de nuevo c() y separar elementos entre comas

vector <- c("Me", "llamo", "Javi")
vector
[1] "Me"    "llamo" "Javi" 
length(vector)
[1] 3

Vectores de caracteres

¿Qué sucederá si concatenamos elementos de diferente tipo?

c(1, 2, "javi", "3", TRUE)
[1] "1"    "2"    "javi" "3"    "TRUE"

Fíjate que como todos tienen que ser del mismo tipo, lo que hace R es convertir todo a texto, violando la integridad del dato

c(3, 4, TRUE, FALSE)
[1] 3 4 1 0

Es importante entender que los valores lógicos en realidad están almacenados internamente como 0/1

Operaciones con vectores

Con los vectores numéricos podemos hacer las mismas operaciones aritméticas que con los números → un número es un vector (de longitud uno)

¿Qué sucederá si sumamos o restamos un valor a un vector?

x <- c(1, 3, 5, 7)
x + 1
[1] 2 4 6 8
x * 2
[1]  2  6 10 14

Cuidado

Salvo que indiquemos lo contrario, en R las operaciones con vectores son siempre elemento a elemento

Suma de vectores

Los vectores también pueden interactuar entre ellos, así que podemos definir, por ejemplo, sumas de vectores (elemento a elemento)

x <- c(2, 4, 6)
y <- c(1, 3, 5)
x + y
[1]  3  7 11

Dado que la operación (por ejemplo, una suma) se realiza elemento a elemento, ¿qué sucederá si sumamos dos vectores de distinta longitud?

z <- c(1, 3, 5, 7)
x + z
[1]  3  7 11  9

Lo que hace es reciclar elementos: si tiene un vector de 4 elementos y sumamos otro de 3 elementos, lo que hará será reciclar del vector con menor longitud.

Comparar vectores

Una operación muy habitual es preguntar a los datos mediante el uso de condiciones lógicas. Por ejemplo, si definimos un vector de temperaturas…

¿Qué días hizo menos de 22 grados?

x <- c(15, 20, 31, 27, 15, 29)
x < 22
[1]  TRUE  TRUE FALSE FALSE  TRUE FALSE

Nos devolverá un vector lógico, en función de si cada elemento cumple o no la condición pedida (de igual longitud que el vector preguntado)

Si tuviéramos un dato ausente (por error del aparato ese día), la condición evaluada también sería NA

y <- c(15, 20, NA, 31, 27, 7, 29, 10)
y < 22
[1]  TRUE  TRUE    NA FALSE FALSE  TRUE FALSE  TRUE

Comparar vectores

Las condiciones lógicas pueden ser combinadas de dos maneras:

  • Intersección: todas las condiciones concatenadas se deben cumplir (conjunción y con &) para devolver un TRUE
x < 30 & x > 15
[1] FALSE  TRUE FALSE  TRUE FALSE  TRUE
  • Unión: basta con que al menos una se cumpla (conjunción o con |)
x < 30 | x > 15
[1] TRUE TRUE TRUE TRUE TRUE TRUE

Con any() y all() podemos comprobar que todos los elementos cumplen

any(x < 30)
[1] TRUE
all(x < 30)
[1] FALSE

Acceder a elementos

Otra operación muy habitual es la de acceder a elementos. La forma más sencilla es usar el operador [i] (acceder al elemento i-ésimo)

edades <- c(20, 30, 33, NA, 61) 
edades[3] # accedemos a la edad de la tercera persona 
[1] 33

Dado que un número no es más que un vector de longitud uno, esta operación también la podemos aplicar usando un vector de índices a seleccionar

y <- c("hola", "qué", "tal", "estás", "?")
y[c(1:2, 4)] # primer, segundo y cuarto elemento
[1] "hola"  "qué"   "estás"

Consejo

Para acceder al último, sin preocuparnos de cuál es, podemos pasarle como índice la propia longitud x[length(x)]

Eliminar elementos

Otras veces no querremos seleccionar sino eliminar algunos elementos. Deberemos repetir la misma operación pero con el signo - delante: el operador [-i] no selecciona el elemento i-ésimo del vector sino que lo «des-selecciona»

y
[1] "hola"  "qué"   "tal"   "estás" "?"    
y[-2]
[1] "hola"  "tal"   "estás" "?"    

En muchas ocasiones los queremos seleccionar o eliminar en base a condiciones lógicas, en función de los valores, así que pasaremos como índice la propia condición (recuerda, x < 2 nos devuelve un vector lógico)

edades <- c(15, 21, 30, 17, 45)
nombres <- c("javi", "maría", "laura", "carla", "luis")
nombres[edades < 18] # nombres de los menores de edad
[1] "javi"  "carla"

Sumar vectores

También podemos hacer uso de operaciones estadísticas como por ejemplo sum() que, dado un vector, nos devuelve la suma de todos sus elementos.

x <- c(1, -2, 3, -1)
sum(x)
[1] 1

¿Qué sucede cuando falta un dato (ausente)?

x <- c(1, -2, 3, NA, -1)
sum(x)
[1] NA

Por defecto, si tenemos un dato ausente, la operación también será ausente. Para poder obviar ese dato, usamos un argumento opcional na.rm = TRUE

sum(x, na.rm = TRUE)
[1] 1

Sumar vectores

Como hemos comentado que los valores lógicos son guardados internamente como 0 y 1, podremos usarlos en operaciones aritméticas.

Por ejemplo, si queremos averiguar el número de elementos que cumplen una condición (por ejemplo, menores que 3), los que lo hagan tendrán asignado un 1 (TRUE) y los que no un 0 (FALSE) , por lo que basta con sumar dicho vector lógico para obtener el número de elementos que cumplen

x <- c(2, 4, 6)
sum(x < 3)
[1] 1

Suma acumulada

Otra operación habitual que puede sernos útil es la suma acumulada con cumsum() que, dado un vector, nos devuelve un vector a su vez con el primero, el primero más el segundo, el primero más el segundo más el tercero…y así sucesivamente.

x <- c(1, 5, 2, -1, 8)
cumsum(x)
[1]  1  6  8  7 15

¿Qué sucede cuando falta un dato (ausente)?

x <- c(1, -2, 3, NA, -1)
cumsum(x)
[1]  1 -1  2 NA NA

En el caso de la suma acumulada lo que sucede es que a partir de ese valor, todo lo acumulado posterior será ausente.

Diferencia

Otra operación habitual que puede sernos útil es la diferencia (con retardo) con diff() que, dado un vector, nos devuelve un vector con el segundo menos el primero, el tercero menos el segundo, el cuarto menos el tercero…y así sucesivamente.

x <- c(1, 8, 5, 3, 9, 0, -1, 5)
diff(x)
[1]  7 -3 -2  6 -9 -1  6

Con el argumento lag = podemos indicar el retardo de dicha diferencia (por ejemplo, lag = 3 implica que se resta el cuarto menos el primero, el quinto menos el segundo, etc)

x <- c(1, 8, 5, 3, 9, 0, -1, 5)
diff(x, lag = 3)
[1]  2  1 -5 -4 -4

Media

Otras operaciones habituales son la media, mediana, percentiles, etc.

  • Media: medida de centralidad que consiste en sumar todos los elementos y dividirlos entre la cantidad de elementos sumados. La más conocida pero la menos robusta: dado un conjunto, si se introducen valores atípicos o outliers (valores muy grandes o muy pequeños), la media se perturba con mucha facilidad.
x <- c(165, 170, 181, 191, 150, 155, 167, NA, 173, 177)
mean(x, na.rm = TRUE)
[1] 169.8889

Percentiles

Otras operaciones habituales son la media, mediana, percentiles, etc.

  • Mediana: medida de centralidad que consiste en ordenar los elementos y quedarse con el que ocupa la mitad.
x <- c(165, 170, 181, 191, 150, 155, 167, 173, 177)
median(x)
[1] 170
  • Percentiles: medidas de posición (nos dividen en partes iguales los datos).
quantile(x) # por defecto percentiles 0-25-50-75-100
  0%  25%  50%  75% 100% 
 150  165  170  177  191 
quantile(x, probs = c(0.1, 0.4, 0.9))
  10%   40%   90% 
154.0 167.6 183.0 

Ordenar vectores

Por último, una acción habitual es saber ordenar valores:

  • sort(): devuelve el vector ordenado. Por defecto de menor a mayor pero con decreasing = TRUE podemos cambiarlo
edades <- c(81, 7, 25, 41, 65, 20, 33, 23, 77)
sort(edades)
[1]  7 20 23 25 33 41 65 77 81
sort(edades, decreasing = TRUE)
[1] 81 77 65 41 33 25 23 20  7
  • order(): devuelve el vector de índices que tendríamos que usar para tener el vector ordenado
order(x)
[1] 5 6 1 7 2 8 9 3 4
x[order(x)]
[1] 150 155 165 167 170 173 177 181 191

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Define el vector x como la concatenación de los 5 primeros números impares. Calcula la longitud del vector

Código
# Dos formas
x <- c(1, 3, 5, 7, 9)
x <- seq(1, 9, by = 2)

length(x)

📝 Accede al tercer elemento de x. Accede al último elemento (sin importar la longitud, un código que pueda ejecutarse siempre). Elimina el primer elemento.

Código
x[3]
x[length(x)]
x[-1]

📝 Obtén los elementos de x mayores que 4. Calcula el vector 1/x y guárdalo en una variable.

Código
x[x > 4]
z <- 1/x
z

📝 Crea un vector que represente los nombres de 5 personas, de los cuales uno es desconocido.

Código
nombres <- c("Javi", "Sandra", NA, "Laura", "Carlos")
nombres

📝 Encuentra del vector x de ejercicios anteriores los elementos mayores (estrictos) que 1 Y ADEMÁS menores (estrictos) que 7. Encuentra una forma de averiguar si todos los elementos son o no positivos.

Código
x[x > 1 & x < 7]
all(x > 0)

📝 Dado el vector x <- c(1, -5, 8, NA, 10, -3, 9), ¿por qué su media no devuelve un número sino lo que se muestra en el código inferior?

x <- c(1, -5, 8, NA, 10, -3, 9)
mean(x)
[1] NA

📝 Dado el vector x <- c(1, -5, 8, NA, 10, -3, 9), extrae los elementos que ocupan los lugares 1, 2, 5, 6.

Código
x <- c(1, -5, 8, NA, 10, -3, 9)
x[c(1, 2, 5, 6)]
x[-2]

📝 Dado el vector x del ejercicio anterior, ¿cuales tienen un dato ausente? Pista: las funciones is.algo() comprueban si el elemento es tipo algo (tabula)

Código
is.na(x)

📝 Define el vector x como la concatenación de los 4 primeros números pares. Calcula el número de elementos de x menores estrictamente que 5.

Código
x[x < 5] 
sum(x < 5)

📝 Calcula el vector 1/x y obtén la versión ordenada (de menor a mayor) de las dos formas posibles

Código
z <- 1/x
sort(z)
z[order(z)]
Código
min(x)
max(x)

📝 Encuentra del vector x los elementos mayores (estrictos) que 1 y menores (estrictos) que 6. Encuentra una forma de averiguar si todos los elementos son o no negativos.

Código
x[x > 1 & x < 7]
all(x > 0)

🐣 Caso práctico

En el paquete {datasets} (ya instalado por defecto) tenemos diversos conjuntos de datos y uno de ellos es airquality. Debajo te he extraído 3 variables de dicho dataset (fíjate que se hace con datos$variable, ese dolar será importante en el futuro).

Los datos capturan medidas diarias (n = 153 observaciones) de la calidad del aire en Nueva York, de mayo a septiembre de 1973. Se midieron 6 variables: niveles de ozono, radiación solar, viento, temperatura, mes y día.

library(datasets)
temperature <- airquality$Temp
month <- airquality$Month
day <- airquality$Day

Intenta responder a las preguntas planteadas en el workbook

Clase 3: textos

Profundizando en variables de tipo texto

Profundizando en textos

Aunque no podamos hacer operaciones aritméticas con ellos si serán importante algunas operaciones que podamos realizar con las cadenas de texto

Para eso usaremos el paquete {stringr} (dentro del mismo «universo de paquetes» de {lubridate}, del que hablaremos más adelante)

library(stringr)

Profundizando en textos

En dicho paquete vamos a trabajar particularmente con cuatro familias de funciones

  • Manipulación

  • Tratamiento de espacios

  • Búsqueda de patrones

Utilidades básicas

  • Longitud de un texto: para calcular la longitud de una cadena de texto podemos usar str_length()
str_length("abc")
[1] 3
str_length("abc 123 *")
[1] 9

Como ves cuenta tanto números como espacios como caracteres que no sean alfanuméricos. Además si el texto es ausente devuelve ausente

str_length(NA)
[1] NA

Las funciones del paquete están preparadas para ser vectorizadas por lo que si aplicamos una función a un vector de dos elementos la aplica a ambos.

str_length(c("abc", "defghi"))
[1] 3 6

Utilidades básicas

  • Ordenar: con str_order() podemos ordenar cadenas de texto según su orden alfabético (distinguiendo ..._sort() y ..._order() como con los números)
x <- c("y", "i", "k")
str_order(x)
[1] 2 3 1
str_sort(x)
[1] "i" "k" "y"

Manipulación

  • Extraer subcadenas: dada una cadena de texto, str_sub(texto, star = ..., end = ...) nos extrae la subcadena desde la posición star hasta end (si es negativo empieza a contar por detrás).
str_sub("abcd efg", star = 4, end = 6)
[1] "d e"
str_sub("abcd efg", star = 5)
[1] " efg"
str_sub("abcd efg", star = 4, end = -2)
[1] "d ef"

Manipulación

  • Extraer subcadenas: la función str_sub() permite aplicarlo a de manera vectorial a múltiples cadenas de texto, e incluso usarla para asignar valores.
x <- c("abcdef", "ghifjk")
str_sub(x, star = 3, end = -2)
[1] "cde" "ifj"
str_sub(x, star = -1, end = -1)
[1] "f" "k"
# En ambas cadenas, sustituimos por * en la posición 2
str_sub(x, star = 2, end = 2) <- "*"

Manipulación

  • Duplicar cadenas: con str_dup(..., times = ...), dada una cadena de texto (o varias), podemos repetir una cadena times veces.
str_dup("abc", times = 3)
[1] "abcabcabc"
x <- c("abcdef", "ghifjk")
str_dup(x, times = c(2, 5))
[1] "abcdefabcdef"                   "ghifjkghifjkghifjkghifjkghifjk"

Manipulación

  • Concatenar cadenas: con str_c podemos concatenar distintas cadenas de texto (con sep = ... indicamos el caracter que hará de separador)
str_c("Buenos días", "Mi nombre es Javier")
[1] "Buenos díasMi nombre es Javier"
str_c("Buenos días", "Mi nombre es Javier", sep = ". ")
[1] "Buenos días. Mi nombre es Javier"

Manipulación

  • Mayúsculas/minúsculas: con str_to_...() podemos convertir textos a mayúsculas (..._upper), a minúsculas (..._lower) y a título (..._title, primera letra de cada palabra en mayúscula)
str_to_upper("me llamo Javi")
[1] "ME LLAMO JAVI"
str_to_lower("me llamo Javi")
[1] "me llamo javi"
str_to_title("me llamo Javi")
[1] "Me Llamo Javi"

Manipulación

  • Reemplazar: str_replace() busca un patrón dado en una cadena de texto y, si la encuentra, la sustituye pro otra de reemplazo
str_replace(c("javi", "sandra", "carlos"), pattern = "i", replacement = "*")
[1] "jav*"   "sandra" "carlos"

Con str_replace_all() reemplazamos todas las coincidencias (por defecto sino solo se reemplaza la primera)

str_replace(c("javi", "sandra", "carlos"), pattern = "a", replacement = "*")
[1] "j*vi"   "s*ndra" "c*rlos"
str_replace_all(c("javi", "sandra", "carlos"), pattern = "a", replacement = "*")
[1] "j*vi"   "s*ndr*" "c*rlos"

Espacios en blanco

  • Rellenar: la función str_pad() rellena una cadena con espacios (al inicio por defecto) para que tenga anchura indicada. Con side = "both" como argumento extra nos añade en ambos lados. Con side = "right" los añade al final. Con pad = ... podemos decidir si queremos rellenar con otro tipo de caracter (espacio por defecto).
str_pad("abc", width = 6)
[1] "   abc"
str_pad("abc", 12, side = "both")
[1] "    abc     "
str_pad("abc", 6, side = "right", pad = "*")
[1] "abc***"

Si width es menor que la longitud de la cadena, no hace nada.

str_pad("abc",  width = 2)
[1] "abc"

Espacios en blanco

  • Eliminar espacios: con str_trim() podemos eliminar espacios en blanco al inicio y al final de la cadena. Si añadimos side = ... podemos cambiar si queremos que solo los elimine al final o al inicio (por defecto, en ambos). Con str_squish() cambiamos cualquier sucesión de espacios en blanco en medio del texto por uno solo (y elimina al inicio y final)
str_trim(" abcde   fghi ")
[1] "abcde   fghi"
str_trim(" abcde   ")
[1] "abcde"
str_trim(" abcde   ", side = "left")
[1] "abcde   "
str_squish(" abcde   fghi ")
[1] "abcde fghi"

Patrones

  • Detectar: con str_detect() podemos detectar si una cadena de texto contiene o no una secuencia de caracteres
str_detect(c("javi álvarez", "javi reyes", "sandra reyes"), pattern = "javi")
[1]  TRUE  TRUE FALSE
str_detect(c("javi álvarez", "javi reyes", "sandra reyes"), pattern = "reyes")
[1] FALSE  TRUE  TRUE
str_detect(c("javi álvarez", "javi reyes", "sandra reyes"), pattern = "carlos")
[1] FALSE FALSE FALSE

Patrones

  • Expresiones regulares: no solo vamos a poder detectar patrones simples sino que podemos hacer uso de las conocidas como expresiones regulares, indicándole por ejemplo que queremos localizar todo patrón que sea, al menos una letra
str_detect(c("a", "ab", "abc", "abcd"), pattern = "[a-z]")
[1] TRUE TRUE TRUE TRUE

Si tras los corchetes indicamos {n} podemos detectar aquellas cadenas con n letras consecutivas

str_detect(c("a", "ab", "abc", "abcd"), pattern = "[a-z]{3}")
[1] FALSE FALSE  TRUE  TRUE

Patrones

  • Expresiones regulares: un buen manejo de estas expresiones puede sernos muy útil para, por ejemplo, detectar formatos correctos en DNI o números de teléfono (de Madrid, por ejemplo).

Vamos a considerar que un formato correcto de DNI es aquel seguido por 8 números ([0-9]{8}) seguido directamente de una letra mayúscula ([A-Z]).

str_detect(c("5055A198-W", "50508040W", "5050505W", "50508040-W"),
           pattern = "[0-9]{8}[A-Z]")
[1] FALSE  TRUE FALSE FALSE

Podemos buscar distintos patrones a la vez concatenándolos con una |

str_detect(c("5055A198-W", "50508040W", "5050505W", "50508040-W"),
           pattern = "[0-9]{8}[A-Z]|[0-9]{8}[-][A-Z]")
[1] FALSE  TRUE FALSE  TRUE

Patrones

  • Contar patrones: con str_count() podemos contar cuantas veces aparece un mismo patrón
str_count(c("abcd defg", "ab defg", "ab cd"), pattern = "[a-z]{4}")
[1] 2 1 0

Patrones

  • Localizar posiciones: str_locate() nos permite localizar la primera posición en la que se produce un patrón. Con str_locate_all() obtenemos todos
str_locate(c("abcde abcd", "cba", "*a*"), pattern = "a")
     start end
[1,]     1   1
[2,]     3   3
[3,]     2   2
str_locate_all(c("abcde abcd", "cba", "*a*"), pattern = "a")
[[1]]
     start end
[1,]     1   1
[2,]     7   7

[[2]]
     start end
[1,]     3   3

[[3]]
     start end
[1,]     2   2

Patrones

  • Extraer patrones: con str_extract() podemos extraer patrones (con str_extract_all() todos ellos) de una cadena de texto.
str_extract(c("DNI: 5050W", "DNI: 50558040W, DNI: 50558080-W", "DNI: 50558080-W"),
            pattern = "[0-9]{8}[A-Z]|[0-9]{8}[-][A-Z]")
[1] NA           "50558040W"  "50558080-W"
str_extract_all(c("DNI: 5050W", "DNI: 50558040W, DNI: 50558080-W", "DNI: 50558080-W"),
            pattern = "[0-9]{8}[A-Z]|[0-9]{8}[-][A-Z]")
[[1]]
character(0)

[[2]]
[1] "50558040W"  "50558080-W"

[[3]]
[1] "50558080-W"

Patrones

  • Dividir: con str_split() podemos localizar un patrón y dividir la cadena de texto siempre que aparezca (con str_split_fixed() podemos dividir en un número concreto de trozos)
str_split(c("a-b-c", "ab-c-d-e"), pattern = "-")
[[1]]
[1] "a" "b" "c"

[[2]]
[1] "ab" "c"  "d"  "e" 
str_split_fixed(c("a-b-c", "ab-c-d-e"), pattern = "-", n = 2)
     [,1] [,2]   
[1,] "a"  "b-c"  
[2,] "ab" "c-d-e"

Si usamos boundary() como patrón podemos dividir en base a caracteres, frases, palabras, etc.

x <- "Esto es una frase. Y esto otra."
str_split(x, boundary("word"))
[[1]]
[1] "Esto"  "es"    "una"   "frase" "Y"     "esto"  "otra" 
str_split(x, boundary("sentence"))
[[1]]
[1] "Esto es una frase. " "Y esto otra."       

💻 Tu turno (textos)

El dataset será discursos (extraído de https://github.com/lirondos/discursos-de-navidad) con los discursos de navidad de los jefes de Estado (1946-2021).

load(file = "./datos/discursos.RData")

📝 Convierte todos los discurso a minúscula (y reemplaza la variable texto)

Código
# Convertimos a minúscula
discursos$texto <- str_to_lower(discursos$texto)

📝 Elimina los: “:”, “,”, “.”, “;”, “¡”, “!”, “¿” y “?”. Tras ello elimina espacios adelante y atrás, y en medio si existen solo deja uno (busca en las diapos y pdf del paquete). Como son caracteres especiales debes indicárselos con \\ delante

str_detect(c("hola", "hola. adios"), pattern = ".")
[1] TRUE TRUE
str_detect(c("hola", "hola. adios"), pattern = "\\.")
[1] FALSE  TRUE
Código
# Eliminamos los signos de puntuación
discursos$texto <-
  str_remove_all(discursos$texto, pattern = "\\:|\\,|\\.|\\;|\\¡|\\!|\\¿|\\?")

# Tras ello eliminamos espacios adelante, atrás y en medio solo dejamos uno
discursos$texto <- str_squish(discursos$texto)

📝 Crea una nueva variable long con la longitud de cada discurso

Código
# Eliminamos los signos de puntuación
discursos$long <- str_length(discursos$texto)

📝 Determina los 5 años con mayor longitud

Código
# 5 años con mayor longitud (usamos order para obtener índices)
discursos$year[order(discursos$long, decreasing = TRUE)[1:5]]

📝 Incorpora una nueva variable llamada spain que calcule el número de veces que se dice “españoles”, “españolas” o “españa” en el discurso. Determina los 5 años dónde menos se menten dichas palabras

Código
# Contamos
discursos$spain <- str_count(discursos$texto, pattern = "españoles|españolas|españa")

# Años con menos menciones
discursos$year[order(discursos$spain)[1:5]]

📝 De los 76 años calcula el número de discursos en los que las palabras “mujer” o “mujeres” se nombren más que las palabras “hombre” u “hombres”

Código
sum(str_count(discursos$texto, pattern = "mujer|mujeres") >
      str_count(discursos$texto, pattern = "hombre|hombres"))

🐣 Caso práctico I: textos

Para practicar textos y tibbles vamos a usar el dataset salto_longitud que tienes guardado en la carpeta de datos, que guarda cómo ha progresado el récord de salto de longitud masculino. Dicha tabla ha sido extraída directamente de la wikipedia https://en.wikipedia.org/wiki/Men%27s_long_jump_world_record_progression#Low_altitude_record_progression_1965%E2%80%931991

load(file = "./datos/salto_longitud.RData")

Aunque más adelante veremos como extraerlo, puedes ver debajo si quieres el código que se ha usado para extraer la tabla

Código
library(rvest)
library(tidyverse)
wiki_jump <- 'https://en.wikipedia.org/wiki/Men%27s_long_jump_world_record_progression'
long_jump <- read_html(wiki_jump)
salto_longitud <- html_table(html_node(long_jump, 'table'))

Intenta responder a las preguntas planteadas en el workbook

Clase 4: primeras tablas

Primeras bases de datos

Primera base de datos

Cuando analizamos datos solemos tener varias variables de cada individuo: necesitamos una «tabla» que las recopile. La opción más inmediata son las matrices: concatenación de variables del mismo tipo e igual longitud.

Imagina que tenemos estaturas y pesos de 4 personas. ¿Cómo crear un dataset con las dos variables?

La opción más habitual es usando cbind(): concatenamos (bind) vectores en forma de columnas (c)

estaturas <- c(150, 160, 170, 180)
pesos <- c(63, 70, 85, 95)
datos_matriz <- cbind(estaturas, pesos)
datos_matriz
     estaturas pesos
[1,]       150    63
[2,]       160    70
[3,]       170    85
[4,]       180    95

Primer intento: matrices

También podemos construir la matriz por filas con la función rbind() (concatenar - bind - por filas - rows), aunque lo recomendable es tener cada variable en columna e individuo en fila como luego veremos.

rbind(estaturas, pesos) # Construimos la matriz por filas
          [,1] [,2] [,3] [,4]
estaturas  150  160  170  180
pesos       63   70   85   95
  • Podemos «visualizar» la matriz con View(matriz).
  • Podemos comprobar las dimensiones con dim(), nrow() y ncol(): las matrices son un tipo de datos tabulados (organizados en filas y columnas)
dim(datos_matriz)
[1] 4 2
nrow(datos_matriz)
[1] 4
ncol(datos_matriz)
[1] 2

Primer intento: matrices

También podemos «darle vuelta» (matriz transpuesta) con t().

t(datos_matriz)
          [,1] [,2] [,3] [,4]
estaturas  150  160  170  180
pesos       63   70   85   95

Dado que ahora tenemos dos dimensiones en nuestros datos, para acceder a elementos con [] deberemos proporcionar dos índices separados por comas: índice de la fila y de la columna

datos_matriz[2, 1] # segunda fila, primera columna
estaturas 
      160 
datos_matriz[1, 2] # primera fila, segunda columna 
pesos 
   63 

Primer intento: matrices

En algunas casos querremos obtener los datos totales de un individuo (una fila concreta pero todas las columnas) o los valores de toda una variable para todos los individuos (una columna concreta pero todas las filas). Para ello dejaremos sin rellenar uno de los índices

datos_matriz[2, ] # segundo individuo
estaturas     pesos 
      160        70 
datos_matriz[, 1] # primera variable
[1] 150 160 170 180

Mucho de lo aprendido con vectores podemos hacerlo con matrices, así podemos por ejemplo acceder a varias filas y/o columnas haciendo uso de las secuencias de enteros 1:n

datos_matriz[c(1, 3), 1] # primera variable para el primer y tercer individuo
[1] 150 170

Primer intento: matrices

También podemos definir una matriz a partir de un vector numérico, reorganizando los valores en forma de matriz (sabiendo que los elementos se van colocando por columnas).

z <- matrix(1:9, ncol = 3) 
z
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

Incluso podemos definir una matriz de valores constantes, por ejemplo de ceros (para luego rellenar)

matrix(0, nrow = 2, ncol = 3)
     [,1] [,2] [,3]
[1,]    0    0    0
[2,]    0    0    0

Operaciones con matrices

Con las matrices sucede como con los vectores: cuando aplicamos una operación aritmética lo hacemos elemento a elemento

z/5
     [,1] [,2] [,3]
[1,]  0.2  0.8  1.4
[2,]  0.4  1.0  1.6
[3,]  0.6  1.2  1.8

Para realizar operaciones en un sentido matricial deberemos añadir %%, por ejemplo, para multiplicar matrices será %*%

z * t(z)
     [,1] [,2] [,3]
[1,]    1    8   21
[2,]    8   25   48
[3,]   21   48   81
z %*% t(z)
     [,1] [,2] [,3]
[1,]   66   78   90
[2,]   78   93  108
[3,]   90  108  126

Operaciones con matrices

También podemos realizar operaciones por columnas/filas sin recurrir a bucles con la función apply(), y le indicaremos como argumentos

  • la matriz
  • el sentido de la operación (MARGIN = 1 por filas, MARGIN = 2 por columnas)
  • la función a aplicar
  • argumentos extra que necesite la función

Por ejemplo, para aplicar una media a cada variable, será mean aplicada con MARGIN = 2 (misma función para cada columna)

# Media (mean) por columnas (MARGIN = 2)
apply(datos_matriz, MARGIN = 2, FUN = "mean")
estaturas     pesos 
   165.00     78.25 

💻 Tu turno (matrices)

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Modifica el código inferior para definir una matriz x de unos, de 3 filas y 7 columnas.

x <- matrix(0, nrow = 2, ncol = 3)
x
Código
x <- matrix(1, nrow = 3, ncol = 7)
x

📝 A la matriz anterior, suma un 1 a cada número de la matriz y divide el resultado entre 5. Tras ello calcula su transpuesta

Código
new_matrix <- (x + 1)/5
t(new_matrix)

📝 ¿Por qué el código inferior nos devuelve dicho mensaje de aviso?

matrix(1:15, nrow = 4)
Warning in matrix(1:15, nrow = 4): data length [15] is not a sub-multiple or
multiple of the number of rows [4]
     [,1] [,2] [,3] [,4]
[1,]    1    5    9   13
[2,]    2    6   10   14
[3,]    3    7   11   15
[4,]    4    8   12    1

📝 Define la matriz x <- matrix(1:12, nrow = 4). Tras ello obtén los datos del primer individuo, los datos de la tercera variable, y el elemento (4, 1).

Código
x <- matrix(1:12, nrow = 4)
x[1, ] # primera fila
x[, 3] # tercera columna
x[4, 1] # elemento (4, 1)

📝 Define una matriz de 2 variables y 3 individuos tal que cada variable capture la estatura y la edad 3 personas, de manera que la edad de la segunda persona sea desconocida (ausente). Tras ello calcula la media de cada variable (¡nos debe de volver un número!)

Código
datos <- cbind("edad" = c(20, NA, 25), "estatura" = c(160, 165, 170))
apply(datos, MARGIN = 2, FUN = "mean", na.rm = TRUE) # media por columnas

📝 ¿Por qué devuelve error el código inferior? ¿Qué está mal?

matriz <- cbind("edad" = c(15, 20, 25), "nombres" = c("javi", "sandra", "carlos"))
matriz
     edad nombres 
[1,] "15" "javi"  
[2,] "20" "sandra"
[3,] "25" "carlos"
matriz + 1
Error in matriz + 1: non-numeric argument to binary operator

Segundo intento: data.frame

Las matrices tienen el mismo problema que los vectores: si juntamos datos de distinto tipo, se perturba la integridad del dato ya que los convierte (fíjate en el código inferior: las edades y los TRUE/FALSE los ha convertido a texto)

edades <- c(14, 24, NA)
soltero <- c(TRUE, NA, FALSE)
nombres <- c("javi", "laura", "lucía")
matriz <- cbind(edades, soltero, nombres)
matriz
     edades soltero nombres
[1,] "14"   "TRUE"  "javi" 
[2,] "24"   NA      "laura"
[3,] NA     "FALSE" "lucía"

De hecho al no ser números ya no podemos realizar operaciones aritméticas

matriz + 1
Error in matriz + 1: non-numeric argument to binary operator

Segundo intento: data.frame

Para poder trabajar con variables de distinto tipo tenemos en R lo que se conoce como data.frame: concatenación de variables de igual longitud pero que pueden ser de tipo distinto.

tabla <- data.frame(edades, soltero, nombres)
class(tabla)
[1] "data.frame"
tabla
  edades soltero nombres
1     14    TRUE    javi
2     24      NA   laura
3     NA   FALSE   lucía

Segundo intento: data.frame

Dado que un data.frame es ya un intento de «base de datos» las variables no son meros vectores matemáticos: tienen un significado y podemos (debemos) ponerles nombres que describan su significado

library(lubridate)
tabla <-
  data.frame("edad" = edades, "estado" = soltero, "nombre" = nombres,
             "f_nacimiento" = as_date(c("1989-09-10", "1992-04-01", "1980-11-27")))
tabla
  edad estado nombre f_nacimiento
1   14   TRUE   javi   1989-09-10
2   24     NA  laura   1992-04-01
3   NA  FALSE  lucía   1980-11-27

Segundo intento: data.frame

¡TENEMOS NUESTRO PRIMER CONJUNTO DE DATOS! (estrictamente no podemos hablar de base de datos pero de momento como lo si fuesen). Puedes visualizarlo escribiendo su nombre en consola o con View(tabla)

Acceso a variables

Si queremos acceder a sus elementos, al ser de nuevo datos tabulados, podemos acceder como en las matrices (no recomendable): de nuevo tenemos dos índices (filas y columnas, dejando libre la que no usemos)

tabla[2, ]  # segunda fila (todas sus variables)
  edad estado nombre f_nacimiento
2   24     NA  laura   1992-04-01
tabla[, 3]  # tercera columna (de todos los individuos)
[1] "javi"  "laura" "lucía"
tabla[2, 1]  # primera característica de la segunda persona
[1] 24

Pero también tiene las ventajas de una «base» de datos : podemos aceder a las variables por su nombre (lo recomendable ya que las variables pueden cambiar de posición y ahora sí tienen un significado), poniendo el nombre de la tabla seguido del símbolo $ (con el tabulador, nos aparecerá un menú de columnas a elegir)

Funciones de consulta

  • names(): nos muestra los nombres de las variables
names(tabla)
[1] "edad"         "estado"       "nombre"       "f_nacimiento"
  • dim(): nos muestra las dimensiones (también nrow() y ncol())
dim(tabla)
[1] 3 4
  • Podemos acceder a las variables por su nombre
tabla[c(1, 3), "nombre"]
[1] "javi"  "lucía"
tabla$nombre[c(1, 3)]
[1] "javi"  "lucía"

Añadir variable

Si tenemos uno ya creado y queremos añadir una columna es tan simple como usar la función data.frame() que ya hemos visto para concatenar la columna. Vamos añadir por ejemplo una nueva variable, el número de hermanos de cada individuo.

# Añadimos una nueva columna con nº de hermanos/as
hermanos <- c(0, 2, 3)
tabla <- data.frame(tabla, "n_hermanos" = hermanos)
tabla
  edad estado nombre f_nacimiento n_hermanos
1   14   TRUE   javi   1989-09-10          0
2   24     NA  laura   1992-04-01          2
3   NA  FALSE  lucía   1980-11-27          3

Intento final: tibble

Las tablas en formato data.frame tienen algunas limitaciones. La principal es que no permite la recursividad: imagina que definimos una base de datos con estaturas y pesos, y queremos una tercera variable con el IMC

data.frame("estatura" = c(1.7, 1.8, 1.6), "peso" = c(80, 75, 70),
           "IMC" = peso / (estatura^2))
Error in data.frame(estatura = c(1.7, 1.8, 1.6), peso = c(80, 75, 70), : object 'peso' not found

En adelante usaremos el formato tibble (data.frame mejorado) del paquete {tibble}

library(tibble)
datos_tb <- 
  tibble("estatura" = c(1.7, 1.8, 1.6), "peso" = c(80, 75, 70), "IMC" = peso / (estatura^2))
class(datos_tb)
[1] "tbl_df"     "tbl"        "data.frame"
datos_tb
# A tibble: 3 × 3
  estatura  peso   IMC
     <dbl> <dbl> <dbl>
1      1.7    80  27.7
2      1.8    75  23.1
3      1.6    70  27.3

Intento final: tibble

datos_tb <-
  tibble("estatura" = c(1.7, 1.8, 1.6), "peso" = c(80, 75, 70), "IMC" = peso / (estatura^2))
datos_tb
# A tibble: 3 × 3
  estatura  peso   IMC
     <dbl> <dbl> <dbl>
1      1.7    80  27.7
2      1.8    75  23.1
3      1.6    70  27.3

Las tablas en formato tibble nos permitirá una gestión más ágil, eficiente y coherente de los datos, con 4 ventajas principales:

  • Metainformación: si te fijas en la cabecera, nos dice ya automáticamente el número de filas y columnas, y el tipo de cada variable
  • Recursividad: permite definir las variables secuencialmente (como hemos visto)

Intento final: tibble

  • Consistencia: si accedes a una columna que no existe avisa con un warning
datos_tb$invent
Warning: Unknown or uninitialised column: `invent`.
NULL
  • Por filas: crear por filas (copiar y pegar de una tabla) con tribble()
tribble(~colA, ~colB,
        "a",   1,
        "b",   2)
# A tibble: 2 × 2
  colA   colB
  <chr> <dbl>
1 a         1
2 b         2

Consejo

El paquete {datapasta} nos permite copiar y pegar tablas de páginas web y documentos sencillos

Recapitulando

  • Cada celda puede ser de un tipo diverso: números, texto, fechas, valores lógicos, etc
  • Un vector es una concatenación de celdas (las futuras columnas de nuestras tablas) –> En R por defecto las operaciones se hacen elemento a elemento
  • Una matriz nos permite concatenar variables del MISMO tipo y MISMA longitud –> datos tabulados
  • Un data.frame nos permite concatenar variables de DISTINTO tipo y MISMA longitud –> usaremos tibble como una opción mejorada de base de datos

💻 Tu turno (tb/df)

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Carga del paquete {datasets} el conjunto de datos airquality (variables de la calidad del aire de Nueva York desde mayo hasta septiembre de 1973). ¿Es el conjunto de datos airquality de tipo tibble? En caso negativo, conviértelo a tibble (busca en la documentación del paquete en https://tibble.tidyverse.org/index.html).

Código
library(tibble)
class(datasets::airquality)
airquality_tb <- as_tibble(datasets::airquality)

📝 Una vez convertido a tibble obtén el nombre de las variables y las dimensiones del conjunto de datos. ¿Cuántas variables hay? ¿Cuántos días se han medido?

Código
names(airquality_tb)
ncol(airquality_tb)
nrow(airquality_tb)

📝 Cambia el código inferior para filtrar solo los datos de la quinta observación

airquality_tb[Month == 8, ]
Código
airquality_tb[5, ]

📝 Cambia el código inferior para filtrar solo los datos del mes de agosto.

airquality_tb[Month == 8, ]
Código
airquality_tb[airquality_tb$Month == 8, ]

📝 Selecciona aquellos datos que no sean ni de julio ni de agosto.

Código
airquality_tb[airquality_tb$Month != 7 & airquality_tb$Month != 8, ]

# otra opción
airquality_tb[!(airquality_tb$Month %in% c(7, 8)), ]

📝 Modifica el siguiente código para quedarte solo con las variable de ozono y temperatura (sin importar qué posición ocupen)

airquality_tb[, 3]

📝 Selecciona los datos de temperatura y viento de agosto.

Código
airquality_tb[airquality_tb$Month == 8, c("Temp", "Wind")]

📝 Traduce a castellano el nombre de las variables.

Código
names(airquality_tb) <- c("ozono", "rad_solar", "viento", "temp", "mes", "dia") 

🐣 Caso práctico II: tibble

Del paquete {Biostatistics} usaremos el conunto de datos pinniped, que guarda los datos de peso de cuerpo y cerebro (desagregado por sexo y mono/poligamia) de 33 especies de mamíferos marinos.

Biostatistics::pinniped
                       Species Male_brain_g Female_brain_g Male_mass_Kg
1       Monachus schauinslandi        370.0             NA        173.0
2            Monachus monachus        480.0          480.0        260.0
3      Mirounga angustirostris        700.0          640.0       2275.0
4             Mirounga leonina       1431.3          898.8       3510.0
5       Leptonychotes weddelli        535.0          637.5        450.0
6            Ommatophoca rossi        425.0          530.0        153.8
7        Lobodon carcinophagus        578.2          538.8        220.5
8            Hydrurga leptonyx        765.0          660.0        324.0
9          Cystophora cristata        480.0          430.0        343.2
10         Erignathus barbatus           NA          460.0        312.5
11          Halichoerus grypus        342.5          272.5        233.0
12          Phoca groenlandica        297.5          252.5        145.0
13              Phoca fasciata        257.5          240.0         94.8
14                Phoca largha        257.5          250.0         97.0
15               Phoca caspica        165.0          160.0         70.5
16              Phoca sibirica        185.0          190.0         89.5
17               Phoca hispida        229.3          220.0         84.0
18              Phoca vitulina        362.3          265.0         97.1
19      Zalophus californianus        405.0          361.5        244.5
20          Eumetopias jubatus        747.5          575.0       1000.0
21              Otaria byronia        546.3          470.0        300.0
22            Neophoca cinerea        440.0          337.5        300.0
23          Phocarctos hookeri        417.5          370.0        364.0
24         Callorhinus ursinus        355.0          302.5        140.0
25     Arctocephalus townsendi           NA             NA        112.0
26     Arctocephalus philippii        415.0             NA        140.0
27 Arctocephalus galapagoensis        302.5          280.0         64.5
28     Arctocephalus australis        350.0          265.0         91.0
29      Arctocephalus forsteri        340.0          300.0        125.0
30       Arctocephalus gazella        360.0          320.0        155.0
31    Arctocephalus tropicalis        322.5          330.0        152.5
32      Arctocephalus pusillus        401.3          337.5        263.0
33           Odobenus rosmarus       1303.0         1340.5       1233.0
   Female_mass_Kg Mate_type
1           272.2      mono
2           275.0      mono
3           488.0      poly
4           565.7      poly
5           447.0      poly
6           164.0      mono
7           224.0      mono
8           367.0      mono
9           222.5      mono
10          326.0      mono
11          205.8      poly
12          139.0      mono
13           80.4      mono
14           71.3      mono
15           55.0      mono
16           85.0      mono
17           81.2      mono
18           85.2      mono
19           81.0      poly
20          287.5      poly
21          144.0      poly
22           78.6      poly
23          114.7      poly
24           33.3      poly
25           49.6      poly
26           48.1      poly
27           27.4      poly
28           48.5      poly
29           38.1      poly
30           45.0      poly
31           50.0      poly
32           64.1      poly
33          811.5      poly

Intenta responder a las preguntas planteadas en el workbook

Clase 5: entrega y Quarto

Comunicar: elaborar apuntes, diapositivas, etc

Comunicar: rmd y Quarto

Una de las principales fortalezas de R es la facilidad para generar informes, libros, webs, apuntes y hasta diapositivas (este mismo material por ejemplo). Para ello instalaremos antes

  • el paquete {rmarkdown} (para generar archivos .rmd)
install.packages("rmarkdown")
  • instalar Quarto (si ya conocías R, el «nuevo» .rmd ahora como .qmd)

Comunicar: rmd y Quarto

Hasta ahora solo hemos programado en scripts (archivos .R) dentro de proyectos, pero en muchas ocasiones no trabajaremos solos y necesitaremos comunicar los resultados en diferentes formatos:

  • apuntes (para nosotros mismos)
  • diapositivas
  • web
  • informes

Para todo ello usaremos Quarto (ver más en https://ivelasq.quarto.pub/intro-to-quarto/)

Comunicar: rmd y Quarto

Los archivos de extensión .qmd (o .rmd antes) nos permitirán fácilmente combinar:

  • Markdown: lenguaje tipado que nos permite crear contenido simple (tipo wordpress, con texto, negritas, cursivas, etc) con un diseño legible.
  • Matemáticas (latex): lenguaje para escribir notación matemática como \(x^2\) o \(\sqrt{y}\) o \(\int_{a}^{b} f(x) dx\)
  • Código y salidas: podremos no solo mostrar el paso final sino el código que has ido realizando (en R, Python, C++, Julia, …), con cajitas de código llamadas CHUNKS.
  • Imágenes, gráficas, tablas, estilos (css, js), etc.

Comunicar: rmd y Quarto

La principal ventaja de realizar este tipo de material en Quarto/Rmarkdown es que, al hacerlo desde RStudio, puedes generar un informe o una presentación sin salirte del entorno de programación en el que estás trabajando

De esta forma podrás analizar los datos, resumirlos y a la vez comunicarlos con la misma herramienta.

Recientemente el equipo de RStudio desarrolló Quarto, una versión mejorada de Rmarkdown (archivos .qmd), con un formato un poco más estético y simple. Tienes toda la documentación y ejemplos en https://quarto.org/

Usos de Quarto

Imágenes obtenidas de https://ivelasq.quarto.pub/intro-to-quarto/#/working-with-the-rstudio-visual-editor

Nuestro primer informe

Vamos a crear el primer fichero rmarkdown con Quarto con extensión .qmd. Para ello solo necesitaremos hacer click en

File << New File << Quarto Document

Nuestro primer informe

Tras hacerlo nos aparecerán varias opciones de formatos de salida:

  • archivo .pdf
  • archivo .html (recomendable): documento dinámico, permite la interacción con el usuario, como una «página web».
  • archivo .doc (nada recomendable)

De momento dejaremos marcado el formato HTML que viene por defecto, y escribiremos el título de nuestro documento. Tras ello tendremos nuestro archivo .qmd (ya no es un script .R como los que hemos abierto hasta ahora).

Nuestro primer informe

Deberías tener algo similar a la captura de la imagen con dos modos de edición: Source (con código, la opción recomendada hasta que lo domines) y Visual (más parecido a un blog)

Para ejecutar TODO el documento debes clickar Render on Save y darle a guardar.

Salida de Quarto

Deberías haber obtenido una salida en html similar a esta (y se te ha generado en tu ordenador un archivo html)

Editor: source vs visual

Como se indicaba, tienes dos formas de trabajar: con código puro y algo parecido a un Notion (blog)

Imagen obtenida de https://ivelasq.quarto.pub/intro-to-quarto/#/working-with-the-rstudio-visual-editor

Nuestro primer informe

Un fichero .qmd se divide básicamente en tres partes:

  • Cabecera: la parte que tienes al inicio entre ---.

  • Texto: que podremos formatear y mejorar con negritas (escrito como negritas, con doble astérisco al inicio y final), cursivas (cursivas, con barra baja al inicio y final) o destacar nombres de funciones o variables de R. Puedes añadir ecuaciones como \(x^2\) (he escrito $x^2$, entre dólares).

  • Código R

Cabecera de un qmd

La cabecera están en formato YAML y contiene los metadatos del documento

  • title y subtitle: el título/subtítulo del documento
  • author: autor del mismo
  • format: formato de salida (podremos personalizar)
    • theme: si tienes algún archivo de estilos
    • toc: si quieres índice o no
    • toc-location: posición del índice
    • toc-title: título del índice
  • editor: si estás en modo visual o source.
---
title: "prueba"
format:
  html:
editor: visual
---

Cabecera de un qmd

La cabecera están en formato YAML y contiene los metadatos del documento

  • title y subtitle: el título/subtítulo del documento
  • author: autor del mismo
  • format: formato de salida (podremos personalizar)
    • theme: si tienes algún archivo de estilos
    • toc: si quieres índice o no
    • toc-location: posición del índice
    • toc-title: título del índice
  • editor: si estás en modo visual o source.
---
title: "prueba"
author: "javier álvarez liébana"
format:
  html:
editor: visual
---

Cabecera de un qmd

La cabecera están en formato YAML y contiene los metadatos del documento

  • title y subtitle: el título/subtítulo del documento
  • author: autor del mismo
  • format: formato de salida (podremos personalizar)
    • theme: si tienes algún archivo de estilos
    • toc: si quieres índice o no
    • toc-location: posición del índice
    • toc-title: título del índice
  • editor: si estás en modo visual o source.
---
title: "prueba"
author: "javier álvarez liébana"
format:
  html:
    style: style.css
    toc: true
editor: visual
---

Cabecera de un qmd

La cabecera están en formato YAML y contiene los metadatos del documento

  • title y subtitle: el título/subtítulo del documento
  • author: autor del mismo
  • format: formato de salida (podremos personalizar)
    • theme: si tienes algún archivo de estilos
    • toc: si quieres índice o no
    • toc-location: posición del índice
    • toc-title: título del índice
  • editor: si estás en modo visual o source.
---
title: "prueba"
author: "javier álvarez liébana"
format:
  html:
    style: style.css
    toc: true
    toc-location: left
editor: visual
---

Cabecera de un qmd

La cabecera están en formato YAML y contiene los metadatos del documento

  • title y subtitle: el título/subtítulo del documento
  • author: autor del mismo
  • format: formato de salida (podremos personalizar)
    • theme: si tienes algún archivo de estilos
    • toc: si quieres índice o no
    • toc-location: posición del índice
    • toc-title: título del índice
  • editor: si estás en modo visual o source.
---
title: "prueba"
author: "javier álvarez liébana"
format:
  html:
    style: style.css
    toc: true
    toc-location: left
    toc-title: Índice
editor: visual
---

Texto de un qmd

Respecto a la escritura solo hay una cosa importante: salvo que indiquemos lo contrario, TODO lo que vamos a escribir es texto (normal). No código R.

Vamos a empezar escribiendo una sección al inicio (# Intro y detrás por ej. la frase

Este material ha sido diseñado por el profesor Javier Álvarez Liébana, docente en la Universidad Complutense de Madrid

Además al Running Code le añadiremos una almohadilla #: las almohadillas FUERA DE CHUNKS nos servirán para crear epígrafes (secciones) en el documento

Índice de un qmd

Para que el índice capture dichas secciones modificaremos la cabecera del archivo como se observa en la imagen (puedes cambiar la localización del índice y el título si quieres para probar).

Texto en un qmd

Vamos a personalizar un poco el texto haciendo lo siguiente:

  • Vamos a añadir negrita al nombre (poniendo ** al inicio y al final).

  • Vamos añadir cursiva a la palabra material (poniendo _ al inicio y al final).

  • Vamos añadir un enlace https://www.ucm.es, asociándolo al nombre de la Universidad. Para ello el título lo ponemos entre corchetes y justo detrás el enlace entre paréntesis [«Universidad Complutense de Madrid»](https://www.ucm.es)

Código en un qmd

Para añadir código R debemos crear nuestras cajas de código llamadas chunks: altos en el camino en nuestro texto markdown donde podremos incluir código de casi cualquier lenguaje (y sus salidas).

 

Para incluir uno deberá de ir encabezado de la siguiente forma tienes un atajo Command + Option + I (Mac) o Ctrl + Shift + I (Windows)

Código en un qmd

Dentro de dicha cajita (que tiene ahora otro color en el documento) escribiremos código R como lo veníamos haciendo hasta ahora en los scripts.

Vamos por ejemplo a definir dos variables y su suma de la siguiente manera, escribiendo dicho código en nuestro .qmd (dentro de ese chunk)

# Código R
x <- 1
y <- 2
x + y
[1] 3

Etiquetando chunks

Los chunks pueden tener un nombre o etiqueta, de forma que podamos referenciarlos de nuevo para no repetir código.

Ejecutando chunks

En cada chunk aparecen dos botones:

  • botón de play: activa la ejecución y salida de ese chunk particular (lo puedes visualizar dentro de tu propio RStudio)

  • botón de rebobinar: activa la ejecución y salida de todos los chunk hasta ese (sin llegar a él)

 

Además podemos incluir código R dentro de la línea de texto (en lugar de mostrar el texto x ejecuta el código R mostrando la variable).

Personalización de chunks

Los chunks podemos personalizarlos con opciones al inicio del chunk precedido de #|:

  • #| echo: false: ejecuta código y se muestra resultado pero no visualiza código en la salida.

  • #| include: false: ejecuta código pero no muestra resultado y no visualiza código en la salida.

  • #| eval: false: no ejecuta código, no muestra resultado pero sí visualiza código en la salida.

  • #| message: false: ejecuta código pero no muestra mensajes de salida.

  • #| warning: false: ejecuta código pero no muestra mensajes de warning.

  • #| error: true: ejecuta código y permite que haya errores mostrando el mensaje de error en la salida.

Estas opciones podemos aplicarlas chunk a chunk o fijar los parámetros de forma global con knitr::opts_chunk$set() al inicio del documento (dentro de un chunk).

Personalizando chunks

Si queremos que aplique la opción a todos los chunks por defecto debemos incluirlo al final de la cabecera, como opciones de ejecución

---
title: "¡Hola!"
format: html
editor: visual
execute:
  echo: false
---

Organizando qmd

Además de texto y código podemos introducir lo siguiente:

  • Ecuaciones: puedes añadir además ecuaciones como \(x^2\) (he escrito $x^2$, la ecuación entre dólares).

  • Listas: puedes itemizar elementos poniendo *

* Paso 1: ...

* Paso 2: ...

  • Cross-references: puedes etiquetar partes del documento (la etiqueta se construye con {#nombre-seccion}) y llamarlas luego con [Sección](@nombre-seccion)

Gráficas/imágenes en qmd

Por último, también podemos añadir pies de gráficas o imágenes añadiendo #| fig-cap: "..."

Fíjate que el caption está en el margen (por ejemplo). Puedes cambiarlo introduciendo ajustes en la cabecera (todo lo relativo a figuras empieza por fig-, y puedes ver las opciones tabulando). Tienes más información en https://quarto.org/

Añadir estilos

Por último puedes añadir un tema personalizado incluyendo un archivo de estilos (archivo en formato .scss o .css). Te he dejado uno en https://github.com/dadosdelaplace/docencia-R-master-bio-2324/tree/main/material.

Importante

El archivo de estilos debe estar en la misma carpeta que el archivo .qmd

Añadir estilos

También puedes hacerlo de manera sencilla añadiendo a los textos un poco de HTML. Por ejemplo, para personalizar el color de un texto va entre corchetes y justo tras el texto, entre llaves, las opciones de estilo

Esta palabra es [roja]{style="color:red;"} ...
... y esta [verde y en negrita]{style="color:green; font-weight: bold;"}

Esta palabra es roja

… y esta verde y en negrita

Revealjs

Puedes añadir algunas «animaciones» usando lo que se conoce como Revealjs (javascript), especifcándolo en la cabecera y usando bloques de dicho lenguaje delimitados por ::: al inicio y final, y la palabra de la «herramienta» a usar. Por ejemplo {.incremental} hace una transición de los elementos.

format:
  revealjs

 

::: {.incremental}
- Me
- llamo
- Javi
:::
  • Me
  • llamo
  • Javi

Bloques de llamada

También puedes usar los bloques de llamada que por defecto son note, tip, warning, caution e important (aunque los puedes crear y personalizar). Para ello basta con usar :::{.callout-tipo} y el tipo que quieras

:::{.callout-tip}

Note that there are five types of callouts, including: 
`note`, `tip`, `warning`, `caution`, and `important`.

:::

Consejo

Recuerda que los 5 tipos son note, tip, warning, caution e important.

Peligro

Úsalos con cabeza, a veces mucho recursos estético puede marear.

Múltiples columnas

Con :::: columns podemos definir una disposición de múltiples columnas donde cada una viene definida por ::: {.column width="65%"} cosa :::, indicando al lado del porcentaje cuanto quieres que ocupe cada columna (¡cuidado, no dejar espacios!)

:::: columns
::: {.column width="65%"}
Así se define un vector
:::
::: {.column width="35%"}
x <- c(1, 2, 3)
x
:::
::::

 

Así se define un vector

x <- c(1, 2, 3)
x
[1] 1 2 3

Pestañas

Con ::: panel-tabset ::: podemos también definir un panel de pestañas

### Ejercicios

Resuelve los siguientes ejercicios
::: panel-tabset

#### Ejercicio 1

Define un vector x con los primeros 3 números naturales

#### Ejercicio 2

Haz la suma del vector anterior
:::

Ejercicios

Resuelve los siguientes ejercicios

Define un vector x con los primeros 3 números naturales

Haz la suma del vector anterior

Código ajeno a R

Además {reticulate} nos permite crear chunks de python dentro de un Quarto en R (ver https://quarto.org/docs/computations/python.html para crear jupyter notebooks directamente desde Quarto)

# install.packages("reticulate")
library(reticulate)

install_python("3.9.12") # Instalar python en PC sino lo tienes

# Instalar paquetes de Python
reticulate::py_install("numpy")
reticulate::py_install("matplotlib")
import numpy as np
import matplotlib.pyplot as plt
r = np.arange(0, 2, 0.05)
theta = 2 * np.pi * r
fig, ax = plt.subplots(
  subplot_kw = {'projection': 'polar'} 
)
ax.plot(theta, r)
plt.show()

🐣 Caso práctico: simulacro

  1. Crea un documento .qmd en el que al menos la cabecera contenga
  • Título, autor y formato (html)
  • Índice con título y situado a la derecha
  1. Tras ello vuelve a ejercicios del tema 3 y estructura un documento donde cada ejercicio sea una subsección. En cada subsección pon el enunciado.

  2. Debajo de cada enunciado pon el chunk con el código correspondiente, así como comentarios de texto de la salida (con negritas y cursivas)

  3. Añade un último chunk en el que, dado un vector x <- 1:5, calcules su media, e incluye con $ $ la fórmula de la media aritmética e incrusta además una foto de la fórmula que encuentres por google.

  4. Renderiza el documento para obtener el html

Ejemplo de entrega

Vamos a realizar un pequeño simulacro antes de la entrega usando el dataset starwars del paquete {dplyr}

Ejemplo de entrega

library(dplyr)
starwars
# A tibble: 87 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

En él tenemos diferentes variables de los personajes de Star Wars, con características de su pelo, piel, altura, nombre, etc.

Ejemplo de entrega

Crea un documento .qmd con nombre, título, formato e índice. Cada ejercicio posterior será una subsección del documento. Ejecuta los chunks que consideres y comenta las salidas para responder a cada pregunta

Ejercicio 1. ¿Cuántos personajes hay guardados en la base de datos? ¿Cuántas características se han medido de cada uno?

Ejercicio 2. Extrae en dos variables distintas nombres y edades las variables correspondientes de la tabla. ¿De qué tipo es la variable nombre? ¿Y la variable birth_year?

Ejercicio 3. Obtén el vector de nombres de los personajes ordenados de mayores a jóvenes.

Ejemplo de entrega

Ejercicio 4. Busca ayuda de la función unique(). Úsala para saber que modalidades tiene la variable cualitativa correspondiente al color de ojos. ¿Cuántos distintos hay?

Ejercicio 5. ¿Existe ALGÚN valor ausente en la variable de color ojos?

Ejercicio 6. Calcula la media y desviación típica de las variables de estatura y peso (cuidado con los ausentes). Define un nuevo tibble con esas dos variables e incorpora una tercera variable que se llame “IMC” que calcule el índice de masa corporal. Incorpora con $ $ la fórmula usada para el IMC.

Entrega I (5%)

El día de la entrega tendrás subido una plantilla de entrega en formato .qmd en el campus.

  • Descomprime la carpeta (¡importante! si no descomprimes, aunque puedas editar el .qmd, no podrás generar el .html)

  • Edita la cabecera con tu nombre y DNI

  • Deberás rellenar cada chunk con el código que consideres (en algunos te he dejado pistas) y cambiar de #| eval: false a #| eval: true (si los quitas directamente, por defecto ya es true)

  • Deberás de comentar con texto normal lo que consideres para responder a las preguntas.

  • Será OBLIGATORIO subir el archivo .html generado (solo se corregirá dicho archivo) así que ve renderizando según rellenas el documento, no lo dejes para el final.

Clase 6: estructuras control

Estructuras condicionales y bucles

Estructuras de control

Una estructura de control se compone de una serie de comandos orientados a decidir el camino que tu código debe recorrer

  • Si se cumple la condición A, ¿qué sucede?

  • ¿Y si sucede B?

  • ¿Cómo puedo repetir una misma expresión (dependiendo de una variable)?

Si has programado antes, quizás te sea familiar las conocidas como estructuras condicionales tales como if (blabla) {...} else {...} o bucles for/while (a evitar siempre que podamos).

Estructura If

Una de las estructuras de control más famosas son las conocidas como estructuras condicionales if.

SI (IF) un conjunto de condiciones se cumple (TRUE), entonces ejecuta lo que haya dentro de las llaves

Por ejemplo, la estructura if (x == 1) { código A } lo que hará será ejecutar el código A entre llaves pero SOLO SI la condición entre paréntesis es cierta (solo si x es 1). En cualquier otro caso, no hará nada.

Por ejemplo, definamos un vector de edades de 8 personas

edad <- c(14, 17, 24, 56, 31, 20, 87, 73)
edad < 18
[1]  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE

Estructura If

Nuestra estructura condicional hará lo siguiente: si existe algún menor de edad, imprimirá por pantalla un mensaje.

if (any(edad < 18)) { 
  
  print("Existe alguna persona menor de edad")
  
}
[1] "Existe alguna persona menor de edad"

Estructura If

if (any(edad < 18)) { 
  
  print("Existe alguna persona menor de edad")
  
}

En caso de que las condiciones no sean ciertas dentro de if() (FALSE), no sucede nada

if (all(edad >= 18)) { 
  
  print("Todos son mayores de edad")
  
}

No obtenemos ningún mensaje porque la condición all(edad >= 18) no es TRUE, así que no ejecuta nada.

Estructura If-else

La estructura if (condicion) { código A } puede combinarse con un else { código B }: cuando la condición no está verificada, se ejecutará el código alternativo B dentro de else { }, permitiéndonos decidir que sucede cuando se cumple y cuando no.

Por ejemplo, if (x == 1) { código A } else { código B } ejecutará A si x es igual a 1 y B en cualquier otro caso.

if (all(edad >= 18)) { 
  
  print("Todos son mayores de edad")
  
} else {
  
  print("Existe alguna persona menor de edad")
}
[1] "Existe alguna persona menor de edad"

Estructura If-else

Esta estructura if - else puede ser anidada: imagina que queremos ejecutar un código si todos son menores; si no sucede, pero todos son mayores de 16, hacer otra cosa; en cualquier otra cosa, otra acción.

if (all(edad >= 18)) { 
  
  print("Todos son mayores de edad")
  
} else if (all(edad >= 16)) {
  
  print("Hay algún menor de edad pero todos con 16 años o más")
  
} else { print("Hay alguna persona con menos de 16 años") }
[1] "Hay alguna persona con menos de 16 años"

Truco

Puedes colapsar las estructuras haciendo click en la flecha a la izquierda que aparece en tu script.

If-else vectorizado

Esta estructura condicional se puede vectorizar (en una sola línea) con if_else() (del paquete {dplyr}), cuyos argumentos son

  • la condición a evaluar
  • lo que sucede cuando se cumple y cuando no
  • un argumento opcional para cuando la condición a evaluar es NA

Vamos a etiquetar sin son mayores/menores y un “desconocido” cuando no conocemos

library(dplyr)
edad <- c(NA, edad)
if_else(edad >= 18, "mayor", "menor", missing = "desconocido")
[1] "desconocido" "menor"       "menor"       "mayor"       "mayor"      
[6] "mayor"       "mayor"       "mayor"       "mayor"      

En R base existe ifelse(): no deja especificar que hacer con los ausentes pero permite especificar distintos tipos de datos en TRUE y en FALSE.

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 ¿Cuál es la salida del siguiente código?

if_else(sqrt(9) < 2, sqrt(9), 0)
Código
La salida es 0 ya que sqrt(9) es igual 3, y dado que no es menor que 2, devuelve el segundo argumento que es 0

📝 ¿Cuál es la salida del siguiente código?

x <- c(1, NA, -1, 9)
if_else(sqrt(x) < 2, 0, 1)
Código
La salida es el vector c(0, NA, NA, 1) ya que sqrt(1) sí es menor que 2, sqrt(9) no lo es, y tanto en el caso de sqrt(NA) (raíz de ausente) como sqrt(-1) (devuelve NaN, not a number), su raíz cuadrada no puede verificarse si es menor que 2 o no, así que la salida es NA.

📝 Modifica el código inferior para que, cuando no se pueda verificar si la raíz cuadrada de un número es menor que 2, devuelva -1

x <- c(1, NA, -1, 9)
if_else(sqrt(x) < 2, 0, 1)
Código
x <- c(1, NA, -1, 9)
if_else(sqrt(x) < 2, 0, 1, missing = -1)

📝 ¿Cuál es son los valores de x e y del código inferior para z <- 1, z <- -1 y z <- -5?

z <- -1
if (z > 0) {
  
  x <- z^3
  y <- -sqrt(z)
  
} else if (abs(z) < 2) {
  
  x <- z^4
  y <- sqrt(-z)
  
} else {
  
  x <- z/2
  y <- abs(z)
  
}
Código
En primero caso x = 1 e y = -1. En el segundo caso x = 1 e y = 1. En el tercer caso -2.5 y 5

📝 ¿Qué pasaría si ejecutamos el siguiente código? Spoiler: da error. ¿Por qué? ¿Cómo solucionarlo?

z <- c(-1, 1, 5)
if (z > 0) {
  
  x <- z^3
  y <- -sqrt(z)
  
} else if (abs(z) < 2) {
  
  x <- z^4
  y <- sqrt(-z)
  
} else {
  
  x <- z/2
  y <- abs(z)
  
}
Código
Da error ya que en los `if (condición) { } else { }` "clásicos" necesitamos que
la condición tenga longitud uno (un solo valor TRU/FALSE)
Código
# para arreglarlo podemos hacer un if_else vectorial
z <- c(-1, 1, -5)
library(dplyr)
x <- if_else(z > 0, z^3, if_else(abs(z) < 2, z^4, z/2))
y <- if_else(z > 0, -sqrt(z), if_else(abs(z) < 2, sqrt(-z), abs(z)))

📝 ¿Qué sucederá si ejecutamos el código inferior?

z <- "a"
if (z > 0) {
  
  x <- z^3
  y <- -sqrt(z)
  
} else if (abs(z) < 2) {
  
  x <- z^4
  y <- sqrt(-z)
  
} else {
  
  x <- z/2
  y <- abs(z)
  
}
Código
# dará error ya que no es un argumento numérico
Error in z^3 : non-numeric argument to binary operator

📝 Del paquete {lubridate}, la función hour() nos devuelve la hora de una fecha dada, y la función now() nos devuelve fecha y hora del momento actual. Con ambas funciones haz que se imprima por pantalla (cat()) “buenas noches” solo a partir de las 21 horas.

Código
# Cargamos librería
library(lubridate)

# Fecha-hora actual
fecha_actual <- now()

# Estructura if
if (hour(fecha_actual) > 21) {
  
  cat("Buenas noches") # print/cat dos formas de imprimir por pantalla
}

Bucles

Aunque en la mayoría de ocasiones se pueden reemplazar por otras estructuras más eficientes y legibles, es importante conocer una de las expresiones de control más famosas: los bucles.

  • for { }: permite repetir el mismo código en un número prefijado y conocido de veces.

  • while { }: permite repetir el mismo código pero en un número indeterminado de veces (hasta que una condición deje de cumplirse).

Bucles for

Un bucle for es una estructura que permite repetir un conjunto de órdenes un número finito, prefijado y conocido de veces dado un conjunto de índices.

Vamos a definir un vector x <- c(0, -7, 1, 4) y otra variable vacía y. Tras ello definiremos un bucle for con for () { }: dentro de los paréntesis indicaremos un índice y unos valores a recorrer, dentro de las llaves el código a ejecutar en cada iteración (en este caso, rellenar y como x + 1)

x <- c(0, -7, 1, 4)
y <- c()

Bucles for

Un bucle for es una estructura que permite repetir un conjunto de órdenes un número finito, prefijado y conocido de veces dado un conjunto de índices.

Vamos a definir un vector x <- c(0, -7, 1, 4) y otra variable vacía y. Tras ello definiremos un bucle for con for () { }: dentro de los paréntesis indicaremos un índice y unos valores a recorrer, dentro de las llaves el código a ejecutar en cada iteración (en este caso, rellenar y como x + 1)

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:4) {
  
}

Bucles for

Un bucle for es una estructura que permite repetir un conjunto de órdenes un número finito, prefijado y conocido de veces dado un conjunto de índices.

Vamos a definir un vector x <- c(0, -7, 1, 4) y otra variable vacía y. Tras ello definiremos un bucle for con for () { }: dentro de los paréntesis indicaremos un índice y unos valores a recorrer, dentro de las llaves el código a ejecutar en cada iteración (en este caso, rellenar y como x + 1)

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:4) {
  y[i] <- x[i] + 1
}

Bucles for

Fíjate que debido a que R funciona de manera vectorial por defecto, el bucle es lo mismo que hacer x + 1 directamente.

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:4) {
  y[i] <- x[i] + 1
}
y
[1]  1 -6  2  5
y2 <- x + 1
y2
[1]  1 -6  2  5

Bucles for

Otra opción habitual es indicar los índices de manera «automática»: desde el primero 1 hasta el último (que corresponde con la longitud de x length(x))

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:length(x)) {
  y[i] <- x[i] + 1
}
y
[1]  1 -6  2  5

Bucles for

Así la estructura general de un bucle for será siempre la siguiente

for (índice in conjunto) { 
  código (dependiente de i)
}

SIEMPRE sabemos cuántas iteraciones tenemos (tantas como elementos haya en el conjunto a indexar)

Evitando bucles

Como ya hemos aprendido con el paquete{microbenchmark} podemos chequear como los bucles suelen ser muy ineficientes (de ahí que debamos evitarlos en la mayoría de ocasiones

library(microbenchmark)
x <- 1:1000
microbenchmark(y <- x^2, 
               for (i in 1:100) { y[i] <- x[i]^2 },
               times = 500)
Unit: microseconds
                                    expr      min       lq        mean   median
                                y <- x^2    1.681    1.927    2.357992    2.009
 for (i in 1:100) {     y[i] <- x[i]^2 } 1257.101 1277.417 1343.424204 1287.667
       uq      max neval
    2.214   10.660   500
 1346.461 4092.497   500

Bucles for

Podemos ver otro ejemplo de bucle combinando números y textos: definimos un vector de edades y de nombres, e imprimimos el nombre y edad i-ésima.

nombres <- c("Javi", "Sandra", "Carlos", "Marcos", "Marta")
edades <- c(33, 27, 18, 43, 29)

for (i in 1:5) { 
  
  print(glue("{nombres[i]} tiene {edades[i]} años")) 
  
}
Javi tiene 33 años
Sandra tiene 27 años
Carlos tiene 18 años
Marcos tiene 43 años
Marta tiene 29 años

Bucles for

Aunque normalmente se suelen indexar con vectors numéricos, los bucles pueden ser indexados sobre cualquier estructura vectorial, da igual de que tipo sea el conjunto

library(stringr)
week_days <- c("monday", "tuesday", "wednesday", "thursday",
               "friday", "saturday", "sunday")

for (days in week_days) {
  
  print(str_to_upper(days))
}
[1] "MONDAY"
[1] "TUESDAY"
[1] "WEDNESDAY"
[1] "THURSDAY"
[1] "FRIDAY"
[1] "SATURDAY"
[1] "SUNDAY"

Bucles y condicionales

Vamos a combinar las estructuras condicionales y los bucles: usando el conjunto swiss del paquete {datasets}, vamos a asignar NA si los valores de fertilidad son mayores de 80.

for (i in 1:nrow(swiss)) {
  
  if (swiss$Fertility[i] > 80) { 
    
    swiss$Fertility[i] <- NA
    
  }
}

Esto es exactamente igual a un if_else() vectorizado

data("swiss")
swiss$Fertility <- if_else(swiss$Fertility > 80, NA, swiss$Fertility)

Bucles while

Otra forma de crear un bucle es con la estructura while { }, que nos ejecutará un bucle un número desconocido de veces, hasta que una condición deje de cumplirse (de hecho puede que nunca termine). Por ejemplo, vamos a inializar una variable ciclos <- 1, que incrementaremos en cada paso, y no saldremos del bucle hasta que ciclos > 4.

ciclos <- 1
while(ciclos <= 4) {
  
  print(glue("No todavía, vamos por el ciclo {ciclos}")) 
  ciclos <- ciclos + 1
  
}
No todavía, vamos por el ciclo 1
No todavía, vamos por el ciclo 2
No todavía, vamos por el ciclo 3
No todavía, vamos por el ciclo 4

Bucles while

Un bucle while será siempre como sigue

while(condición) {
  
  código a hacer mientras la condición sea TRUE
  # normalmente aquí se actualiza alguna variable
  
}

Bucles while

¿Qué sucede cuando la condición nunca es FALSE? Pruébalo tu mismo

while (1 > 0) {
  
  print("Presiona ESC para salir del bucle")
  
}

 

Cuidado

Un bucle while { } puede ser bastante «peligroso» sino controlamos bien cómo pararlo.

Bucles while

Contamos con dos palabras reservadas para abortar un bucle o forzar su avance:

  • break: permite abortar un bucle incluso si no se ha llegado a su final
for(i in 1:10) {
  if (i == 3) {
    
    break # si i = 3, abortamos bucle
    
  }
  print(i)
}
[1] 1
[1] 2

Bucles while

Contamos con dos palabras reservadas para abortar un bucle o forzar su avance:

  • next: fuerza un bucle a avanzar a la siguiente iteración
for(i in 1:5) {
  if (i == 3) {
    
    next # si i = 3, la obvia y continua al siguiente
    
  }
  print(i)
}
[1] 1
[1] 2
[1] 4
[1] 5

Bucles repeat

Aunque no es tan usado como las opciones anteriores, también contamos con repeat { } que ejecuta un bucle de manera infinita hasta que se indique abortar con un break

count <- 0
repeat { 
  
  count <- count + 1
  if (count >= 100) { break }
  
}
count
[1] 100

Replicate

Aunque no es formalmente un bucle, otra forma de repetir código un número de veces es hacer uso de replicate(): simplemente permite repetir lo mismo n veces

x <- 1:3
replicate(n = 3, x^2)
     [,1] [,2] [,3]
[1,]    1    1    1
[2,]    4    4    4
[3,]    9    9    9

Replicate

La función replicate() se suele usar para generar distintas repeticiones de elementos aleatorios. Por ejemplo, imaginemos que queremos generar 3 muestras de distribuciones normales, en la que cada muestra tendrá 7 elementos. Para generar una se usa rnorm(n = 7) (r de resample, norm de normal, y si no se dice nada es media 0 y desv 1).

replicate(n = 3, rnorm(n = 7))
           [,1]       [,2]        [,3]
[1,] -1.7305216 -0.4787108  0.08331489
[2,]  0.7445537 -2.3204100  0.61128734
[3,]  2.4410534  0.8691069  0.33036976
[4,] -0.6739057  0.7655178  0.42204789
[5,] -1.0057717  2.0289433 -0.78958629
[6,]  0.5981527  0.9064921 -0.11663416
[7,] -0.5509492 -0.6519952 -0.16331130

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Modifica el código inferior para que se imprima un mensaje por pantalla si y solo si todos los datos de airquality son con mes distinto a enero

library(datasets)
months <- airquality$Month

if (months == 2) {
  print("No hay datos de enero")
}
Código
library(datasets)
months <- airquality$Month

if (all(months != 1)) {
  print("No hay datos de enero")
}

📝 Modifica el código inferior para guardar en una variable llamada temp_alta un TRUE si alguno de los registros tiene una temperatura superior a 90 grados Farenheit y FALSE en cualquier otro caso

temp <- airquality$Temp

if (temp == 100) {
  print("Algunos de los registros tienen temperaturas superiores a 90 grados Farenheit")
}
Código
# Option 1
temp <- airquality$Temp
temp_alta <- FALSE
if (any(temp > 90)) {
   temp_alta <- TRUE
}

# Option 2
temp_alta <- any(airquality$Temp > 90)

📝 Modifica el código inferior para diseñar un bucle for de 5 iteraciones que solo recorra los primeros 5 impares (y en cada paso del bucle los imprima)

for (i in 1:5) {
  
  print(i)
}
Código
for (i in c(1, 3, 5, 7, 9)) {
  
  print(i)
}

📝 Modifica el código inferior para diseñar un bucle while que empiece con un contador count <- 1 y pare cuando llegue a 6

count <- 1
while (count == 2) {
  
  print(count)
}
Código
count <- 1
while (count < 6) {
  
  print(count)
  count <- count + 1
  
}

🐣 Caso práctico

Intenta responder a las preguntas planteadas en el workbook donde tendrás que diseñar algunos estudios de simulación haciendo uso de bucles y estructuras condicionales

Clase 7: funciones

¿Qué es una función? ¿Cómo se definen? Variables locales vs globales

Creando funciones

No solo podemos usar funciones predeterminadas que vienen ya cargadas en paquetes, además podemos crear nuestras propias funciones para automatizar tareas. ¿Cómo crear nuestra propia función? Veamos su esquema básico:

  • Nombre: por ejemplo name_fun (sin espacios ni caracteres extraños). Al nombre le asignamos la palabra reservada function().

  • Definir argumentos de entrada (dentro de function()).

  • Cuerpo de la función dentro de { }.

  • Finalizamos la función con los argumentos de salida con return().

name_fun <- function() {
  
}

Creando funciones

No solo podemos usar funciones predeterminadas que vienen ya cargadas en paquetes, además podemos crear nuestras propias funciones para automatizar tareas. ¿Cómo crear nuestra propia función? Veamos su esquema básico:

  • Nombre: por ejemplo name_fun (sin espacios ni caracteres extraños). Al nombre le asignamos la palabra reservada function().

  • Definir argumentos de entrada (dentro de function()).

  • Cuerpo de la función dentro de { }.

  • Finalizamos la función con los argumentos de salida con return().

name_fun <- function(arg1, arg2, ...) {
  
}

Creando funciones

No solo podemos usar funciones predeterminadas que vienen ya cargadas en paquetes, además podemos crear nuestras propias funciones para automatizar tareas. ¿Cómo crear nuestra propia función? Veamos su esquema básico:

  • Nombre: por ejemplo name_fun (sin espacios ni caracteres extraños). Al nombre le asignamos la palabra reservada function().

  • Definir argumentos de entrada (dentro de function()).

  • Cuerpo de la función dentro de { }.

  • Finalizamos la función con los argumentos de salida con return().

name_fun <- function(arg1, arg2, ...) {
  
  código a ejecutar
  
}

Creando funciones

No solo podemos usar funciones predeterminadas que vienen ya cargadas en paquetes, además podemos crear nuestras propias funciones para automatizar tareas. ¿Cómo crear nuestra propia función? Veamos su esquema básico:

  • Nombre: por ejemplo name_fun (sin espacios ni caracteres extraños). Al nombre le asignamos la palabra reservada function().

  • Definir argumentos de entrada (dentro de function()).

  • Cuerpo de la función dentro de { }.

  • Finalizamos la función con los argumentos de salida con return().

name_fun <- function(arg1, arg2, ...) {
  
  código a ejecutar
  
  return(var_salida)
  
}

Creando funciones

  • arg1, arg2, ...: serán los argumentos de entrada, los argumentos que toma la función para ejecutar el código que tiene dentro

  • código: líneas de código que queramos que ejecute la función.

  • return(var_salida): se introducirán los argumentos de salida.

name_fun <- function(arg1, arg2, ...) {
  
  # Código que queramos ejecutar
  código
  
  # Salida
  return(var_salida)
  
}

Importante

Todas las variables que definamos dentro de la función son variables LOCALES: solo existirán dentro de la función salvo que especifiquemos lo contrario.

Creando funciones

Veamos un ejemplo muy simple de función para calcular el área de un rectángulo.

Dado que el área de un rectángulo se calcula como el producto de sus lados, necesitaremos precisamente eso, sus lados: esos serán los argumentos de entrada y el valor a devolver será justo su área (\(lado_1 * lado_2\)).

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2) {
  
}

Creando funciones

Veamos un ejemplo muy simple de función para calcular el área de un rectángulo.

Dado que el área de un rectángulo se calcula como el producto de sus lados, necesitaremos precisamente eso, sus lados: esos serán los argumentos de entrada y el valor a devolver será justo su área (\(lado_1 * lado_2\)).

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2) {
  
  area <- lado_1 * lado_2
  
}

Creando funciones

Veamos un ejemplo muy simple de función para calcular el área de un rectángulo.

Dado que el área de un rectángulo se calcula como el producto de sus lados, necesitaremos precisamente eso, sus lados: esos serán los argumentos de entrada y el valor a devolver será justo su área (\(lado_1 * lado_2\)).

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2) {
  
  area <- lado_1 * lado_2
  return(area)
  
}

Uso de funciones

También podemos hacer una definición directa de las variables sin almacenar por el camino.

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2) {
  
  return(lado_1 * lado_2)
  
}

¿Cómo aplicar la función?

calcular_area(5, 3) # área de un rectángulo 5 x 3 
[1] 15
calcular_area(1, 5) # área de un rectángulo 1 x 5
[1] 5

Uso de funciones

Consejo

Aunque no sea necesario, es recomendable hacer explícita la llamada de los argumentos, especificando en el código qué valor es para cada argumento para que no dependa de su orden, haciendo el código más legible

calcular_area(lado_1 = 5, lado_2 = 3) # área de un rectángulo 5 x 3 
[1] 15
calcular_area(lado_2 = 3, lado_1 = 5) # área de un rectángulo 5 x 3 
[1] 15

Argumentos por defecto

Imagina ahora que nos damos cuenta que el 90% de las veces usamos dicha función para calcular por defecto el área de un cuadrado (es decir, solo necesitamos un lado). Para ello, podemos definir argumentos por defecto en la función: tomarán dicho valor salvo que le asignemos otro.

¿Por qué no asignar lado_2 = lado_1 por defecto, para ahorrar líneas de código y tiempo?

calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado que devolvemos
  return(area)
  
}

Argumentos por defecto

calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado que devolvemos
  return(area)
  
}

Ahora por defecto el segundo lado será igual al primero (si se lo añadimos usará ambos).

calcular_area(lado_1 = 5) # cuadrado
[1] 25
calcular_area(lado_1 = 5, lado_2 = 7) # rectángulo
[1] 35

Salida múltiple

Compliquemos un poco la función y añadamos en la salida los valores de cada lado, etiquetados como lado_1 y lado_2, empaquetando la salida en una vector.

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado
  return(c("area" = area, "lado_1" = lado_1, "lado_2" = lado_2))
  
}

Salida múltiple

Podemos complicar un poco más la salida añadiendo una cuarta variable que nos diga, en función de los argumentos, si rectángulo o cuadrado, teniendo que añadir en la salida una variable que de tipo caracter (o lógica).

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado
  return(c("area" = area, "lado_1" = lado_1, "lado_2" = lado_2,
           "tipo" = if_else(lado_1 == lado_2, "cuadrado", "rectángulo")))
  
}
calcular_area(5, 3)
        area       lado_1       lado_2         tipo 
        "15"          "5"          "3" "rectángulo" 

Problema: al intentar juntar números y texto, lo convierte todo a números. Podríamos guardarlo todo en un tibble() como hemos aprendido o en un objeto conocido en R como listas

Introducción a listas

Veamos un pequeño resumen de los datos que ya conocemos:

  • vectores: colección de elementos de igual tipo. Pueden ser números, caracteres o valores lógicos, entre otros.

  • matrices: colección BIDIMENSIONAL de elementos de igual tipo e igual longitud.

  • data.frame / tibble: colección BIDIMENSIONAL de elementos de igual longitud pero de cualquier tipo.

Las listas serán colecciones de variables de diferente tipo y diferente longitud, con estructuras totalmente heterógeneas (incluso una lista puede tener dentro a su vez otra lista).

Introducción a listas

Vamos a crear nuestra primera lista con list() con tres elementos: el nombre de nuestros padres/madres, nuestro lugar de nacimiento y edades de nuestros hermanos.

var_1 <- c("Paloma", "Gregorio")
var_2 <- "Madrid"
var_3 <- c(25, 30, 26)

lista <- list("progenitores" = var_1, "lugar_nac" = var_2, "edad_hermanos" = var_3)
lista
$progenitores
[1] "Paloma"   "Gregorio"

$lugar_nac
[1] "Madrid"

$edad_hermanos
[1] 25 30 26

Introducción a listas

length(lista)
[1] 3

Si observas el objeto que hemos definido como lista, su longitud del es de 3 ya que tenemos guardados tres elementos: un vector de caracteres (de longitud 2), un caracter (vector de longitud 1), y un vector de números (de longitud 3)

Tenemos guardados elementos de distinto tipo (algo que ya podíamos hacer) pero, además, de longitudes dispares.

dim(lista) # devolverá NULL al no tener dos dimensiones
NULL
class(lista) # de tipo lista
[1] "list"

Introducción a listas

Si los juntásemos con un tibble(), al tener distinta longitud, obtendríamos un error.

library(tibble)
tibble("progenitores" = var_1, "lugar_nac" = va_2, "edad_hermanos" = var_3)
Error in eval_tidy(xs[[j]], mask): object 'va_2' not found

Introducción a listas

  • Acceder por índice: con el operador [[i]] accedemos al elemento i-ésimo de la lista.
lista[[1]]
[1] "Paloma"   "Gregorio"
  • Acceder por nombre: con $nombre_elemento accedemos por su nombre.
lista$progenitores
[1] "Paloma"   "Gregorio"

En contraposición, el corchete simple nos permite acceder a varios elementos a la vez

# Varios elementos
lista[1:2]
$progenitores
[1] "Paloma"   "Gregorio"

$lugar_nac
[1] "Madrid"

Salida múltiple: listas

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado
  return(list("area" = area, "lado_1" = lado_1, "lado_2" = lado_2,
           "tipo" = if_else(lado_1 == lado_2, "cuadrado", "rectángulo")))
  
}
calcular_area(5, 3)
$area
[1] 15

$lado_1
[1] 5

$lado_2
[1] 3

$tipo
[1] "rectángulo"

Orden de los argumentos

Antes nos daba igual el orden de los argumentos pero ahora el orden de los argumentos de entrada importa, ya que en la salida incluimos lado_1 y lado_2.

Recomendación

Como se comentaba, altamente recomendable hacer la llamada a la función indicando explícitamente los argumentos para mejorar legibilidad e interpretabilidad.

# Equivalente a calcular_area(5, 3)
calcular_area(lado_1 = 5, lado_2 = 3)
$area
[1] 15

$lado_1
[1] 5

$lado_2
[1] 3

$tipo
[1] "rectángulo"

Funciones: generando conocimiento

Parece una tontería lo que hemos hecho pero hemos cruzado una frontera importante: hemos pasado de consumir conocimiento (código de otros paquetes, elaborado por otros/as), a generar conocimiento, creando nuestras propias funciones.

Las funciones van a ser claves en tu día a día ya que te permitirá automatizar código que vas a repetir una y otra vez: empaquetando ese código bajo un alias (nombre de la función) vas a poder usarlo una y otra vez sin necesidad de programarlo (por lo que hacer el doble de trabajo no implicará trabajar el doble)

Variables locales vs globales

Un aspecto importante sobre el que reflexionar con las funciones: ¿qué sucede si nombramos a una variable dentro de una función a la que se nos ha olvidado asignar un valor dentro de la misma?

Debemos ser cautos al usar funciones en R, ya que debido a la «regla lexicográfica», si una variable no se define dentro de la función, R buscará dicha variable en el entorno de variables.

x <- 1
funcion_ejemplo <- function() {
    
  print(x) # No devuelve nada, solo realiza la acción 
}
funcion_ejemplo()
[1] 1

Variables locales vs globales

Si una variable ya está definida fuera de la función (entorno global), y además es usada dentro de cambiando su valor, el valor solo cambia dentro pero no en el entorno global.

x <- 1
funcion_ejemplo <- function() {
    
  x <- 2
  print(x) # lo que vale dentro
}
# lo que vale dentro
funcion_ejemplo() #<<
[1] 2
# lo que vale fuera
print(x) #<<
[1] 1

Variables locales vs globales

Si queremos que además de cambiar localmente lo haga globalmente deberemos usar la doble asignación (<<-).

x <- 1
y <- 2
funcion_ejemplo <- function() {
  
  # no cambia globalmente, solo localmente
  x <- 3 
  # cambia globalmente
  y <<- 0 #<<
  
  print(x)
  print(y)
}

funcion_ejemplo() # lo que vale dentro
[1] 3
[1] 0
x # lo que vale fuera
[1] 1
y # lo que vale fuera
[1] 0

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Modifica el código inferior para definir una función llamada funcion_suma, de forma que dados dos elementos, devuelve su suma.

nombre <- function(x, y) {
  suma <- # código a ejecutar
  return()
}
# Aplicamos la función
suma(3, 7)
Código
funcion_suma <- function(x, y) {
  suma <- x + y
  return(suma)
}
funcion_suma(3, 7)

📝 Modifica el código inferior para definir una función llamada funcion_producto, de forma que dados dos elementos, devuelve su producto, pero que por defecto calcule el cuadrado

nombre <- function(x, y) {
  producto <- # código de la multiplicación
  return()
}
producto(3)
producto(3, -7)
Código
funcion_producto <- function(x, y = x) {
  producto <- x * y
  return(producto)
}
funcion_producto(3)
funcion_producto(3, -7)

📝 Define una función llamada igualdad_nombres que, dados dos nombres, nos diga si son iguales o no. Hazlo considerando importantes las mayúsculas, y sin que importen las mayúsculas. Usa el paquete {stringr}.

Código
# Distinguiendo mayúsculas
igualdad_nombres <- function(persona_1, persona_2) {
  return(persona_1 == persona_2)
}
igualdad_nombres("Javi", "javi")
igualdad_nombres("Javi", "Lucía")

# Sin importar mayúsculas
igualdad_nombres <- function(persona_1, persona_2) {
  return(str_to_upper(persona_1) == str_to_upper(persona_2))
  # otra opción
  return(str_equal(persona_1, persona_2, ignore_case = TRUE))
}
igualdad_nombres("Javi", "javi")
igualdad_nombres("Javi", "Lucía")

📝 Crea una función llamada calculo_IMC que, dados dos argumentos (peso y estatura en metros) y un nombre, devuelva una lista con el IMC (\(peso/(estatura_m^2)\)) y el nombre.

Código
calculo_IMC <- function(nombre, peso, estatura) {
  
  return(list("nombre" = nombre, "IMC" = peso/(estatura^2)))
}

📝 Repite el ejercicio anterior pero con otro argumento opcional que se llame unidades (por defecto, unidades = "metros"). Desarrolla la función de forma que haga lo correcto si unidades = "metros" y si unidades = "centímetros".

Código
calculo_IMC <- function(nombre, peso, estatura, unidades = "metros") {
  
  return(list("nombre" = nombre,
              "IMC" = peso/(if_else(unidades == "metros", estatura, estatura/100)^2)))
}

📝 Crea un tibble ficticio de 7 personas, con tres variables (inventa nombre, y simula peso, estatura en centímetros), y aplica la función definida de forma que obtengamos una cuarta columna con su IMC.

Código
datos <-
  tibble("nombres" = c("javi", "sandra", "laura",
                       "ana", "carlos", "leo", NA),
         "peso" = rnorm(n = 7, mean = 70, sd = 1),
         "estatura" = rnorm(n = 7, mean = 168, sd = 5))

datos |> 
  mutate(IMC = calculo_IMC(nombres, peso, estatura, unidades = "centímetros")$IMC)

📝 Crea una función llamada atajo que tenga dos argumentos numéricos x e y. Si ambos son iguales, debes devolver "iguales" y hacer que la función acaba automáticamente (piensa cuándo una función sale). OJO: x e y podrían ser vectores. Si son distintos (de igual de longitud) calcula la proporción de elementos diferentes. Si son distintos (por ser distinta longitud), devuelve los elementos que no sean comunes.

Código
atajo <- function(x, y) {
  
  if (all(x == y) & length(x) == length(y)) { return("iguales") }
  else {
   
    if (length(x) == length(y)) {
      
      n_diff <- sum(x != y) / length(x)
      return(n_diff)
      
    } else {
      
      diff_elem <- unique(c(setdiff(x, y), setdiff(y, x)))
      return(diff_elem)
    }
    
  }
}

🐣 Caso práctico

Para practicar con funciones vamos a crear un completo conversor de temperaturas que, dada una temperatura en Fahrenheit, Celsius o Kelvin, la convierta a cualquiera de las otras

Intenta responder a las preguntas planteadas en el workbook hasta construirlo.

Clase 8: tidydata

Nuestra base de datos: tibble. Tidydata: un multiverso de datos limpios

Previously, in Breaking Bad…

Nuestro formato final de base de datos será el objeto de tipo tibble, un data.frame mejorado

library(tibble)
tibble("estatura" = c(1.7, 1.8, 1.6), "peso" = c(80, 75, 70), "IMC" = peso / (estatura^2))
# A tibble: 3 × 3
  estatura  peso   IMC
     <dbl> <dbl> <dbl>
1      1.7    80  27.7
2      1.8    75  23.1
3      1.6    70  27.3
  • Metainformación: en la cabecera nos dice ya automáticamente el número de filas y columnas, y el tipo de cada variable

  • Recursividad: permite definir las variables secuencialmente (como hemos visto)

  • Consistencia: si accedes a una columna que no existe avisa con un warning

  • Por filas: permite crear por filas con tribble()

Previously, in Breaking Bad…

Para definir un tibble() nosotro mismos tenemos dos opciones:

  • Concatenando vectores que ya tengamos definidos, haciendo uso de la función tibble() del paquete {tibble} (ya incluido en {tidyverse})
estatura <- c(1.7, 1.8, 1.6)
peso <- c(80, 75, 70)
IMC <-  peso / (estatura^2)
tibble("estatura" = estatura, "peso" = peso,
       "IMC" = IMC)
# A tibble: 3 × 3
  estatura  peso   IMC
     <dbl> <dbl> <dbl>
1      1.7    80  27.7
2      1.8    75  23.1
3      1.6    70  27.3

Previously, in Breaking Bad…

  • Directamente en tibble proporcionando manualmente valores y nombres de variables
tibble("estatura" = c(1.7, 1.8, 1.6),
       "peso" = c(80, 75, 70),
       "IMC" = peso / (estatura^2))
# A tibble: 3 × 3
  estatura  peso   IMC
     <dbl> <dbl> <dbl>
1      1.7    80  27.7
2      1.8    75  23.1
3      1.6    70  27.3

R base vs Tidyverse

Hasta ahora todo lo que hemos hecho en R lo hemos realizado en el paradigma de programación conocido como R base. Y es que cuando R nació como lenguaje, muchos de los que programaban en él imitaron formas y metodologías heredadas de otros lenguajes, basado en el uso de

  • Bucles for

  • Bucles while

  • Estructuras if-else

Y aunque conocer dichas estructuras puede sernos en algunos casos interesantes, en la mayoría de ocasiones han quedado caducas y vamos a poder evitarlas (en especial los bucles) ya que R está especialmente diseñado para trabajar de manera funcional (en lugar de elemento a elemento).

¿Qué es tidyverse?

En ese contexto de programación funcional, hace una década nacía {tidyverse}, un «universo» de paquetes para garantizar un flujo de trabajo eficiente, coherente y lexicográficamente sencillo de entender, basado en la idea de que nuestros datos están limpios y ordenados (tidy)

¿Qué es tidyverse?

  • {lubridate} manejo de fechas
  • {rvest}: web scraping
  • {tidymodels}: modelización/predicción
  • {tibble}: optimizando data.frame
  • {tidyr}: limpieza de datos
  • {readr}: carga datos rectangulares (.csv), {readxl} para importar archivos .xls y .xlsx
  • {dplyr}: gramática para depurar
  • {stringr}: manejo de textos
  • {purrr}: manejo de listas
  • {forcats}: manejo de cualitativas
  • {ggplot2}: visualización de datos

¿Qué es tidyverse?

  • {lubridate} manejo de fechas
  • {rvest}: web scraping
  • {tidymodels}: modelización/predicción
  • {tibble}: optimizando data.frame
  • {tidyr}: limpieza de datos
  • {readr}: carga datos rectangulares (.csv), {readxl} para importar archivos .xls y .xlsx
  • {dplyr}: gramática para depurar
  • {stringr}: manejo de textos
  • {purrr}: manejo de listas
  • {forcats}: manejo de cualitativas
  • {ggplot2}: visualización de datos

Filosofía base: tidy data

Tidy datasets are all alike, but every messy dataset is messy in its own way (Hadley Wickham, Chief Scientist en RStudio)

TIDYVERSE

El universo de paquetes {tidyverse} se basa en la idea introducida por Hadley Wickham (el Dios al que rezamos) de estandarizar el formato de los datos para

  • sistematizar la depuración
  • hacer más sencillo su manipulación.
  • código legible

Reglas del tidy data

Lo primero por tanto será entender qué son los conjuntos tidydata ya que todo {tidyverse} se basa en que los datos están estandarizados.

  1. Cada variable en una única columna
  1. Cada individuo en una fila diferente
  1. Cada celda con un único valor
  1. Cada dataset en un tibble
  1. Si queremos cruzar múltiples tablas debemos tener una columna común

Tubería (pipe)

En {tidyverse} será clave el operador pipe (tubería) definido como |> (ctrl+shift+M): será una tubería que recorre los datos y los transforma.

En R base, si queremos aplicar tres funciones first(), second() y third() en orden, sería

third(second(first(datos)))

En {tidyverse} podremos leer de izquierda a derecha y separar los datos de las acciones

datos |> first() |> second() |> third()

Apunte importante

Desde la versión 4.1.0 de R disponemos de |>, un pipe nativo disponible fuera de tidyverse, sustituyendo al antiguo pipe %>% que dependía del paquete {magrittr} (bastante problemático).

Tubería (pipe)

La principal ventaja es que el código sea muy legible (casi literal) pudiendo hacer grandes operaciones con los datos con apenas código.

datos |>
  limpio(...) |>
  filtro(...) |>
  selecciono(...) |>
  ordeno(...) |>
  modifico(...) |>
  renombro(...) |>
  agrupo(...) |>
  cuento(...) |>
  resumo(...) |>
  pinto(...)

Datos SUCIOS: messy data

¿Pero qué aspecto tienen los datos no tidy? Vamos a cargar la tabla table4a del paquete {tidyr} (ya lo tenemos cargado del entorno tidyverse).

library(tidyr)
table4a
# A tibble: 3 × 3
  country     `1999` `2000`
  <chr>        <dbl>  <dbl>
1 Afghanistan    745   2666
2 Brazil       37737  80488
3 China       212258 213766

¿Qué puede estar fallando?

Pivotar: pivot_longer()

table4a
# A tibble: 3 × 3
  country     `1999` `2000`
  <chr>        <dbl>  <dbl>
1 Afghanistan    745   2666
2 Brazil       37737  80488
3 China       212258 213766

❎ Cada fila representa dos observaciones (1999 y 2000) → las columnas 1999 y 2000 en realidad deberían ser en sí valores de una variable y no nombres de columnas.

Incluiremos una nueva columna que nos guarde el año y otra que guarde el valor de la variable de interés en cada uno de esos años. Y lo haremos con la función pivot_longer(): pivotaremos la tabla a formato long:

table4a |> 
  pivot_longer(cols = c("1999", "2000"), names_to = "year", values_to = "cases")
# A tibble: 6 × 3
  country     year   cases
  <chr>       <chr>  <dbl>
1 Afghanistan 1999     745
2 Afghanistan 2000    2666
3 Brazil      1999   37737
4 Brazil      2000   80488
5 China       1999  212258
6 China       2000  213766

Pivotar: pivot_longer()

table4a |> 
  pivot_longer(cols = c("1999", "2000"),
               names_to = "year",
               values_to = "cases")
# A tibble: 6 × 3
  country     year   cases
  <chr>       <chr>  <dbl>
1 Afghanistan 1999     745
2 Afghanistan 2000    2666
3 Brazil      1999   37737
4 Brazil      2000   80488
5 China       1999  212258
6 China       2000  213766

  • cols: nombre de las variables a pivotar
  • names_to: nombre de la nueva variable a la quemandamos la cabecera de la tabla (los nombres).
  • values_to: nombre de la nueva variable a la que vamos a mandar los datos.

Datos SUCIOS: messy data

Veamos otro ejemplo con la tabla table2

table2
# A tibble: 12 × 4
   country      year type            count
   <chr>       <dbl> <chr>           <dbl>
 1 Afghanistan  1999 cases             745
 2 Afghanistan  1999 population   19987071
 3 Afghanistan  2000 cases            2666
 4 Afghanistan  2000 population   20595360
 5 Brazil       1999 cases           37737
 6 Brazil       1999 population  172006362
 7 Brazil       2000 cases           80488
 8 Brazil       2000 population  174504898
 9 China        1999 cases          212258
10 China        1999 population 1272915272
11 China        2000 cases          213766
12 China        2000 population 1280428583

¿Qué puede estar fallando?

Pivotar: pivot_wider()

# A tibble: 12 × 4
   country      year type            count
   <chr>       <dbl> <chr>           <dbl>
 1 Afghanistan  1999 cases             745
 2 Afghanistan  1999 population   19987071
 3 Afghanistan  2000 cases            2666
 4 Afghanistan  2000 population   20595360
 5 Brazil       1999 cases           37737
 6 Brazil       1999 population  172006362
 7 Brazil       2000 cases           80488
 8 Brazil       2000 population  174504898
 9 China        1999 cases          212258
10 China        1999 population 1272915272
11 China        2000 cases          213766
12 China        2000 population 1280428583

❎ Cada observación está dividido en dos filas → los registros con el mismo año deberían ser el mismo

Lo que haremos será lo opuesto: con pivot_wider() ensancharemos la tabla

table2 |>  pivot_wider(names_from = type, values_from = count)
# A tibble: 6 × 4
  country      year  cases population
  <chr>       <dbl>  <dbl>      <dbl>
1 Afghanistan  1999    745   19987071
2 Afghanistan  2000   2666   20595360
3 Brazil       1999  37737  172006362
4 Brazil       2000  80488  174504898
5 China        1999 212258 1272915272
6 China        2000 213766 1280428583

Datos SUCIOS: messy data

Veamos otro ejemplo con la tabla table3

table3
# A tibble: 6 × 3
  country      year rate             
  <chr>       <dbl> <chr>            
1 Afghanistan  1999 745/19987071     
2 Afghanistan  2000 2666/20595360    
3 Brazil       1999 37737/172006362  
4 Brazil       2000 80488/174504898  
5 China        1999 212258/1272915272
6 China        2000 213766/1280428583

¿Qué puede estar fallando?

Separar: separate()

table3
# A tibble: 6 × 3
  country      year rate             
  <chr>       <dbl> <chr>            
1 Afghanistan  1999 745/19987071     
2 Afghanistan  2000 2666/20595360    
3 Brazil       1999 37737/172006362  
4 Brazil       2000 80488/174504898  
5 China        1999 212258/1272915272
6 China        2000 213766/1280428583

❎ Cada celda contiene varios valores

Lo que haremos será hacer uso de la función separate() para mandar separar cada valor a una columna diferente.

table3 |> separate(rate, into = c("cases", "pop"))
# A tibble: 6 × 4
  country      year cases  pop       
  <chr>       <dbl> <chr>  <chr>     
1 Afghanistan  1999 745    19987071  
2 Afghanistan  2000 2666   20595360  
3 Brazil       1999 37737  172006362 
4 Brazil       2000 80488  174504898 
5 China        1999 212258 1272915272
6 China        2000 213766 1280428583

Separar: separate()

table3 |> separate(rate, into = c("cases", "pop"))
# A tibble: 6 × 4
  country      year cases  pop       
  <chr>       <dbl> <chr>  <chr>     
1 Afghanistan  1999 745    19987071  
2 Afghanistan  2000 2666   20595360  
3 Brazil       1999 37737  172006362 
4 Brazil       2000 80488  174504898 
5 China        1999 212258 1272915272
6 China        2000 213766 1280428583

Fíjate que los datos, aunque los ha separado, los ha mantenido como texto cuando en realidad deberían ser variables numéricas. Para ello podemos añadir el argumento opcional convert = TRUE

table3 |> separate(rate, into = c("cases", "pop"), convert = TRUE)
# A tibble: 6 × 4
  country      year  cases        pop
  <chr>       <dbl>  <int>      <int>
1 Afghanistan  1999    745   19987071
2 Afghanistan  2000   2666   20595360
3 Brazil       1999  37737  172006362
4 Brazil       2000  80488  174504898
5 China        1999 212258 1272915272
6 China        2000 213766 1280428583

Datos SUCIOS: messy data

Veamos el último ejemplo con la tabla table5

table5
# A tibble: 6 × 4
  country     century year  rate             
  <chr>       <chr>   <chr> <chr>            
1 Afghanistan 19      99    745/19987071     
2 Afghanistan 20      00    2666/20595360    
3 Brazil      19      99    37737/172006362  
4 Brazil      20      00    80488/174504898  
5 China       19      99    212258/1272915272
6 China       20      00    213766/1280428583

¿Qué puede estar fallando?

Unir unite()

table5
# A tibble: 6 × 4
  country     century year  rate             
  <chr>       <chr>   <chr> <chr>            
1 Afghanistan 19      99    745/19987071     
2 Afghanistan 20      00    2666/20595360    
3 Brazil      19      99    37737/172006362  
4 Brazil      20      00    80488/174504898  
5 China       19      99    212258/1272915272
6 China       20      00    213766/1280428583

❎ Tenemos mismos valores divididos en dos columnas

Usaremos unite() para unir los valores de siglo y año en una misma columna

table5 |> unite(col = year_completo, century, year, sep = "")
# A tibble: 6 × 3
  country     year_completo rate             
  <chr>       <chr>         <chr>            
1 Afghanistan 1999          745/19987071     
2 Afghanistan 2000          2666/20595360    
3 Brazil      1999          37737/172006362  
4 Brazil      2000          80488/174504898  
5 China       1999          212258/1272915272
6 China       2000          213766/1280428583

Ejemplo: relig_income

Vamos a realizar un ejemplo juntos con la tabla relig_income del paquete {tidyr}. Como se indica en la ayuda ? relig_income, la tabla representa la cantidad de personas que hay en cada tramo de ingresos anuales (20k = 20 000$) y en cada religión.

relig_income
# A tibble: 18 × 11
   religion `<$10k` `$10-20k` `$20-30k` `$30-40k` `$40-50k` `$50-75k` `$75-100k`
   <chr>      <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
 1 Agnostic      27        34        60        81        76       137        122
 2 Atheist       12        27        37        52        35        70         73
 3 Buddhist      27        21        30        34        33        58         62
 4 Catholic     418       617       732       670       638      1116        949
 5 Don’t k…      15        14        15        11        10        35         21
 6 Evangel…     575       869      1064       982       881      1486        949
 7 Hindu          1         9         7         9        11        34         47
 8 Histori…     228       244       236       238       197       223        131
 9 Jehovah…      20        27        24        24        21        30         15
10 Jewish        19        19        25        25        30        95         69
11 Mainlin…     289       495       619       655       651      1107        939
12 Mormon        29        40        48        51        56       112         85
13 Muslim         6         7         9        10         9        23         16
14 Orthodox      13        17        23        32        32        47         38
15 Other C…       9         7        11        13        13        14         18
16 Other F…      20        33        40        46        49        63         46
17 Other W…       5         2         3         4         2         7          3
18 Unaffil…     217       299       374       365       341       528        407
# ℹ 3 more variables: `$100-150k` <dbl>, `>150k` <dbl>,
#   `Don't know/refused` <dbl>

Ejemplo: relig_income

relig_income
# A tibble: 18 × 11
   religion `<$10k` `$10-20k` `$20-30k` `$30-40k` `$40-50k` `$50-75k` `$75-100k`
   <chr>      <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
 1 Agnostic      27        34        60        81        76       137        122
 2 Atheist       12        27        37        52        35        70         73
 3 Buddhist      27        21        30        34        33        58         62
 4 Catholic     418       617       732       670       638      1116        949
 5 Don’t k…      15        14        15        11        10        35         21
 6 Evangel…     575       869      1064       982       881      1486        949
 7 Hindu          1         9         7         9        11        34         47
 8 Histori…     228       244       236       238       197       223        131
 9 Jehovah…      20        27        24        24        21        30         15
10 Jewish        19        19        25        25        30        95         69
11 Mainlin…     289       495       619       655       651      1107        939
12 Mormon        29        40        48        51        56       112         85
13 Muslim         6         7         9        10         9        23         16
14 Orthodox      13        17        23        32        32        47         38
15 Other C…       9         7        11        13        13        14         18
16 Other F…      20        33        40        46        49        63         46
17 Other W…       5         2         3         4         2         7          3
18 Unaffil…     217       299       374       365       341       528        407
# ℹ 3 more variables: `$100-150k` <dbl>, `>150k` <dbl>,
#   `Don't know/refused` <dbl>

¿Es tidydata?

Ejemplo: relig_income

relig_income
# A tibble: 18 × 11
   religion `<$10k` `$10-20k` `$20-30k` `$30-40k` `$40-50k` `$50-75k` `$75-100k`
   <chr>      <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>      <dbl>
 1 Agnostic      27        34        60        81        76       137        122
 2 Atheist       12        27        37        52        35        70         73
 3 Buddhist      27        21        30        34        33        58         62
 4 Catholic     418       617       732       670       638      1116        949
 5 Don’t k…      15        14        15        11        10        35         21
 6 Evangel…     575       869      1064       982       881      1486        949
 7 Hindu          1         9         7         9        11        34         47
 8 Histori…     228       244       236       238       197       223        131
 9 Jehovah…      20        27        24        24        21        30         15
10 Jewish        19        19        25        25        30        95         69
11 Mainlin…     289       495       619       655       651      1107        939
12 Mormon        29        40        48        51        56       112         85
13 Muslim         6         7         9        10         9        23         16
14 Orthodox      13        17        23        32        32        47         38
15 Other C…       9         7        11        13        13        14         18
16 Other F…      20        33        40        46        49        63         46
17 Other W…       5         2         3         4         2         7          3
18 Unaffil…     217       299       374       365       341       528        407
# ℹ 3 more variables: `$100-150k` <dbl>, `>150k` <dbl>,
#   `Don't know/refused` <dbl>

No lo es ya que en realidad solo deberíamos tener una variable de ingresos y la tenemos dividida en 11: todas ellas es la misma variable solo que adopta un valor diferente. ¿Cómo convertirla a tidy data?

Ejemplo: relig_income

La idea es pivotar todas las columnas de ingresos para que acaben en una sola columna llamada income, y los valores (el número de personas) en otra llamada people (por ejemplo). La tabla la haremos más larga y menos ancha así que…

relig_tidy <-
  relig_income |>
  pivot_longer(cols = "<$10k":"Don't know/refused", names_to = "income",
               values_to = "people")
relig_tidy 
# A tibble: 180 × 3
   religion income             people
   <chr>    <chr>               <dbl>
 1 Agnostic <$10k                  27
 2 Agnostic $10-20k                34
 3 Agnostic $20-30k                60
 4 Agnostic $30-40k                81
 5 Agnostic $40-50k                76
 6 Agnostic $50-75k               137
 7 Agnostic $75-100k              122
 8 Agnostic $100-150k             109
 9 Agnostic >150k                  84
10 Agnostic Don't know/refused     96
# ℹ 170 more rows

Ejemplo: relig_income

Vamos a hilar más fino: ahora mismo en la variable income en realidad tenemos dos valores, el límite inferior y el superior de la renta. Vamos a separar dicha variable e ingresos en dos, llamadas income_inf y income_sup

relig_tidy 
# A tibble: 180 × 3
   religion income             people
   <chr>    <chr>               <dbl>
 1 Agnostic <$10k                  27
 2 Agnostic $10-20k                34
 3 Agnostic $20-30k                60
 4 Agnostic $30-40k                81
 5 Agnostic $40-50k                76
 6 Agnostic $50-75k               137
 7 Agnostic $75-100k              122
 8 Agnostic $100-150k             109
 9 Agnostic >150k                  84
10 Agnostic Don't know/refused     96
# ℹ 170 more rows

Ejemplo: relig_income

Vamos a hilar más fino: ahora mismo en la variable income en realidad tenemos dos valores, el límite inferior y el superior de la renta. Vamos a separar dicha variable e ingresos en dos, llamadas income_inf y income_sup

relig_tidy |>
  # Separamos por -
  separate(income, into = c("income_inf", "income_sup"), sep = "-")
# A tibble: 180 × 4
   religion income_inf         income_sup people
   <chr>    <chr>              <chr>       <dbl>
 1 Agnostic <$10k              <NA>           27
 2 Agnostic $10                20k            34
 3 Agnostic $20                30k            60
 4 Agnostic $30                40k            81
 5 Agnostic $40                50k            76
 6 Agnostic $50                75k           137
 7 Agnostic $75                100k          122
 8 Agnostic $100               150k          109
 9 Agnostic >150k              <NA>           84
10 Agnostic Don't know/refused <NA>           96
# ℹ 170 more rows

¿Está ya bien? Fíjate bien…

Ejemplo: relig_income

relig_tidy |>
  # Separamos por -
  separate(income, into = c("income_inf", "income_sup"), sep = "-")
# A tibble: 180 × 4
   religion income_inf         income_sup people
   <chr>    <chr>              <chr>       <dbl>
 1 Agnostic <$10k              <NA>           27
 2 Agnostic $10                20k            34
 3 Agnostic $20                30k            60
 4 Agnostic $30                40k            81
 5 Agnostic $40                50k            76
 6 Agnostic $50                75k           137
 7 Agnostic $75                100k          122
 8 Agnostic $100               150k          109
 9 Agnostic >150k              <NA>           84
10 Agnostic Don't know/refused <NA>           96
# ℹ 170 more rows

Si te fijas la primera columna el "$10k" debería ser una cota superior, no inferior. ¿Cómo indicarle que separe bien ese caso?

Ejemplo: relig_income

Le indicaremos que separe si encuentra "-" o "<" (usamos | para separar ambas opciones)

relig_tidy |>
  # Separamos por -
  separate(income, into = c("income_inf", "income_sup"), sep = "-|<")
# A tibble: 180 × 4
   religion income_inf           income_sup people
   <chr>    <chr>                <chr>       <dbl>
 1 Agnostic ""                   $10k           27
 2 Agnostic "$10"                20k            34
 3 Agnostic "$20"                30k            60
 4 Agnostic "$30"                40k            81
 5 Agnostic "$40"                50k            76
 6 Agnostic "$50"                75k           137
 7 Agnostic "$75"                100k          122
 8 Agnostic "$100"               150k          109
 9 Agnostic ">150k"              <NA>           84
10 Agnostic "Don't know/refused" <NA>           96
# ℹ 170 more rows

Ejemplo: relig_income

relig_tidy <-
  relig_tidy |>
  # Separamos por -
  separate(income, into = c("income_inf", "income_sup"), sep = "-|<")
relig_tidy
# A tibble: 180 × 4
   religion income_inf           income_sup people
   <chr>    <chr>                <chr>       <dbl>
 1 Agnostic ""                   $10k           27
 2 Agnostic "$10"                20k            34
 3 Agnostic "$20"                30k            60
 4 Agnostic "$30"                40k            81
 5 Agnostic "$40"                50k            76
 6 Agnostic "$50"                75k           137
 7 Agnostic "$75"                100k          122
 8 Agnostic "$100"               150k          109
 9 Agnostic ">150k"              <NA>           84
10 Agnostic "Don't know/refused" <NA>           96
# ℹ 170 more rows

Piensa ahora como podemos convertir los límites de ingresos a numéricas (eliminando símbolos, letras, etc)

Ejemplo: relig_income

Para ello usaremos el paquete {stringr} que hemos visto antes, en concreto la función str_remove_all() a la que le podemos pasar los caracteres que queremos eliminar (fíjate que $ al ser un caracter reservado en R hay que indicárselo con \\$)

relig_tidy$income_inf <-
  str_remove_all(relig_tidy$income_inf, "\\$|>|k")
relig_tidy$income_sup <-
  str_remove_all(relig_tidy$income_sup, "\\$|>|k")

relig_tidy
# A tibble: 180 × 4
   religion income_inf          income_sup people
   <chr>    <chr>               <chr>       <dbl>
 1 Agnostic ""                  10             27
 2 Agnostic "10"                20             34
 3 Agnostic "20"                30             60
 4 Agnostic "30"                40             81
 5 Agnostic "40"                50             76
 6 Agnostic "50"                75            137
 7 Agnostic "75"                100           122
 8 Agnostic "100"               150           109
 9 Agnostic "150"               <NA>           84
10 Agnostic "Don't now/refused" <NA>           96
# ℹ 170 more rows

Ejemplo: relig_income

Fíjate que tenemos "Don't now/refused". ¿Qué deberíamos tener?

Debería ser un dato ausente así que usaremos if_else(): si contiene dicha frase, NA, en caso contrario su valor (consejo: str_detect() para detectar patrones en textos, y evitar tener que escribir toda la palabra sin errores)

relig_tidy$income_inf <-
  if_else(str_detect(relig_tidy$income_inf, "refused"), NA, relig_tidy$income_inf)
relig_tidy$income_sup <-
  if_else(str_detect(relig_tidy$income_sup, "refused"), NA, relig_tidy$income_sup)
relig_tidy
# A tibble: 180 × 4
   religion income_inf income_sup people
   <chr>    <chr>      <chr>       <dbl>
 1 Agnostic ""         10             27
 2 Agnostic "10"       20             34
 3 Agnostic "20"       30             60
 4 Agnostic "30"       40             81
 5 Agnostic "40"       50             76
 6 Agnostic "50"       75            137
 7 Agnostic "75"       100           122
 8 Agnostic "100"      150           109
 9 Agnostic "150"      <NA>           84
10 Agnostic  <NA>      <NA>           96
# ℹ 170 more rows

Ejemplo: relig_income

relig_tidy
# A tibble: 180 × 4
   religion income_inf income_sup people
   <chr>    <chr>      <chr>       <dbl>
 1 Agnostic ""         10             27
 2 Agnostic "10"       20             34
 3 Agnostic "20"       30             60
 4 Agnostic "30"       40             81
 5 Agnostic "40"       50             76
 6 Agnostic "50"       75            137
 7 Agnostic "75"       100           122
 8 Agnostic "100"      150           109
 9 Agnostic "150"      <NA>           84
10 Agnostic  <NA>      <NA>           96
# ℹ 170 more rows

En la primera línea, ese "" también debería ser `NA``

relig_tidy$income_inf <-
  if_else(relig_tidy$income_inf == "", NA, relig_tidy$income_inf)
relig_tidy$income_suop <-
  if_else(relig_tidy$income_sup == "", NA, relig_tidy$income_sup)

Ejemplo: relig_income

relig_tidy
# A tibble: 180 × 5
   religion income_inf income_sup people income_suop
   <chr>    <chr>      <chr>       <dbl> <chr>      
 1 Agnostic <NA>       10             27 10         
 2 Agnostic 10         20             34 20         
 3 Agnostic 20         30             60 30         
 4 Agnostic 30         40             81 40         
 5 Agnostic 40         50             76 50         
 6 Agnostic 50         75            137 75         
 7 Agnostic 75         100           122 100        
 8 Agnostic 100        150           109 150        
 9 Agnostic 150        <NA>           84 <NA>       
10 Agnostic <NA>       <NA>           96 <NA>       
# ℹ 170 more rows

Además si te fijas los números son en realidad caracteres, así que vamos a convertirlos a números

Ejemplo: relig_income

Además si te fijas los números son en realidad caracteres, así que vamos a convertirlos a números

relig_tidy$income_inf <- as.numeric(relig_tidy$income_inf)
relig_tidy$income_sup <- as.numeric(relig_tidy$income_sup)
relig_tidy
# A tibble: 180 × 5
   religion income_inf income_sup people income_suop
   <chr>         <dbl>      <dbl>  <dbl> <chr>      
 1 Agnostic         NA         10     27 10         
 2 Agnostic         10         20     34 20         
 3 Agnostic         20         30     60 30         
 4 Agnostic         30         40     81 40         
 5 Agnostic         40         50     76 50         
 6 Agnostic         50         75    137 75         
 7 Agnostic         75        100    122 100        
 8 Agnostic        100        150    109 150        
 9 Agnostic        150         NA     84 <NA>       
10 Agnostic         NA         NA     96 <NA>       
# ℹ 170 more rows

Ejemplo: relig_income

¿Se te ocurre alguna forma de «cuantificar numéricamente» los valores ausentes que tenemos en este caso?

Si te fijas en realidad cuando hay ausente en el límite inferior en realidad podríamos poner un 0 (nadie puede ganar menos de eso) y cuando lo tenemos en el límite superior sería Inf

relig_tidy$income_inf <-
  if_else(is.na(relig_tidy$income_inf), 0, relig_tidy$income_inf)
relig_tidy$income_sup <-
  if_else(is.na(relig_tidy$income_sup), Inf, relig_tidy$income_sup)
relig_tidy
# A tibble: 180 × 5
   religion income_inf income_sup people income_suop
   <chr>         <dbl>      <dbl>  <dbl> <chr>      
 1 Agnostic          0         10     27 10         
 2 Agnostic         10         20     34 20         
 3 Agnostic         20         30     60 30         
 4 Agnostic         30         40     81 40         
 5 Agnostic         40         50     76 50         
 6 Agnostic         50         75    137 75         
 7 Agnostic         75        100    122 100        
 8 Agnostic        100        150    109 150        
 9 Agnostic        150        Inf     84 <NA>       
10 Agnostic          0        Inf     96 <NA>       
# ℹ 170 more rows

Ejemplo: relig_income

Aunque nos haya llevado un rato este es el código completo resumido

relig_tidy <-
  relig_income |>
  pivot_longer(cols = "<$10k":"Don't know/refused", names_to = "income",
               values_to = "people") |>
  separate(income, into = c("income_inf", "income_sup"), sep = "-|<")

relig_tidy$income_inf <- str_remove_all(relig_tidy$income_inf, "\\$|>|k")
relig_tidy$income_sup <- str_remove_all(relig_tidy$income_sup, "\\$|>|k")

relig_tidy$income_inf <-
  if_else(str_detect(relig_tidy$income_inf, "refused") |
            relig_tidy$income_inf == "", 0, as.numeric(relig_tidy$income_inf))
relig_tidy$income_sup <-
  if_else(str_detect(relig_tidy$income_sup, "refused") |
            relig_tidy$income_sup == "", Inf, as.numeric(relig_tidy$income_sup))

Ejemplo: relig_income

¿Por qué era importante tenerlo en tidydata? Lo veremos más adelante al visualizar los datos pero esto ya nos permite realizar filtros muy rápidos con muy poco código.

Por ejemplo: ¿cuántas personas agnósticas con ingresos superiores (o iguales) a 30 tenemos?

# una línea de código
sum(relig_tidy$people[relig_tidy$religion == "Agnostic" & relig_tidy$income_inf >= 30])
[1] 609

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Usa el dataset original relig_income y trata de responder a la última pregunta: ¿cuántas personas agnósticas con ingresos superiores (o iguales) a 30 tenemos? Compara el código a realizar cuando tenemos tidydata a cuando no. ¿Cuál es más legible si no supieses R? ¿Cuál tiene mayor probabilidad de error?

Código
sum(relig_income[relig_income$religion == "Agnostic",
             c("$30-40k", "$40-50k", "$50-75k", "$75-100k", "$100-150k", ">150k")])

📝 Usando relig_tidy determina quién tiene más ingresos medios, ¿católicos (Catholic) o agnósticos (Agnostic)? Crea antes una variable avg_income (ingresos medios por intervalo): si hay 5 personas entre 20 y 30, y 3 personas entre 30 y 50, la media sería \((25*5 + 40*3)/8\) (si es Inf por arriba, NA)

Código
relig_tidy$avg_income <- 
  if_else(is.infinite(relig_tidy$income_sup), NA, (relig_tidy$income_sup + relig_tidy$income_inf)/2)

# Agnosticos vs catolicos
sum((relig_tidy$avg_income[relig_tidy$religion == "Agnostic"] * relig_tidy$people[relig_tidy$religion == "Agnostic"]), na.rm = TRUE) /
  sum(relig_tidy$people[relig_tidy$religion == "Agnostic"], na.rm = TRUE)

sum((relig_tidy$avg_income[relig_tidy$religion == "Catholic"] * relig_tidy$people[relig_tidy$religion == "Catholic"]), na.rm = TRUE) /
  sum(relig_tidy$people[relig_tidy$religion == "Catholic"], na.rm = TRUE)

📝 Si debemos elegir budismo (Buddhist) e hinduismo (Hindu), ¿cuál de las dos es la religión mayoritaria entre los que ganan más de 50 000$ anuales?

Código
greatest_income <-
  relig_tidy[relig_tidy$income_inf >= 50 & relig_tidy$religion %in% c("Buddhist", "Hindu"), ]

sum(greatest_income$people[greatest_income$religion == "Buddhist"], na.rm = TRUE)
sum(greatest_income$people[greatest_income$religion == "Hindu"], na.rm = TRUE)

📝 Echa un vistazo a la tabla table4b del paquete {tidyr}. ¿Es tidydata? En caso negativo, ¿qué falla? ¿Cómo convertirla a tidy data en caso de que no lo sea ya?

Código
table4b |>
  pivot_longer(cols = "1999":"2000", names_to = "year",
               values_to = "cases")

📝 Echa un vistazo a la tabla billboard del paquete {tidyr}. ¿Es tidydata? En caso negativo, ¿qué falla? ¿Cómo convertirla a tidy data en caso de que no lo sea ya?

Código
billboard |>
  pivot_longer(cols = "wk1":"wk76",
               names_to = "week",
               names_prefix = "wk",
               values_to = "position",
               values_drop_na = TRUE)

🐣 Caso práctico

En el paquete {tidyr} contamos con el dataset who2 (dataset de la Organización Mundial de la Salud). Intenta responder a las preguntas planteadas en el workbook.

who2
# A tibble: 7,240 × 58
   country      year sp_m_014 sp_m_1524 sp_m_2534 sp_m_3544 sp_m_4554 sp_m_5564
   <chr>       <dbl>    <dbl>     <dbl>     <dbl>     <dbl>     <dbl>     <dbl>
 1 Afghanistan  1980       NA        NA        NA        NA        NA        NA
 2 Afghanistan  1981       NA        NA        NA        NA        NA        NA
 3 Afghanistan  1982       NA        NA        NA        NA        NA        NA
 4 Afghanistan  1983       NA        NA        NA        NA        NA        NA
 5 Afghanistan  1984       NA        NA        NA        NA        NA        NA
 6 Afghanistan  1985       NA        NA        NA        NA        NA        NA
 7 Afghanistan  1986       NA        NA        NA        NA        NA        NA
 8 Afghanistan  1987       NA        NA        NA        NA        NA        NA
 9 Afghanistan  1988       NA        NA        NA        NA        NA        NA
10 Afghanistan  1989       NA        NA        NA        NA        NA        NA
# ℹ 7,230 more rows
# ℹ 50 more variables: sp_m_65 <dbl>, sp_f_014 <dbl>, sp_f_1524 <dbl>,
#   sp_f_2534 <dbl>, sp_f_3544 <dbl>, sp_f_4554 <dbl>, sp_f_5564 <dbl>,
#   sp_f_65 <dbl>, sn_m_014 <dbl>, sn_m_1524 <dbl>, sn_m_2534 <dbl>,
#   sn_m_3544 <dbl>, sn_m_4554 <dbl>, sn_m_5564 <dbl>, sn_m_65 <dbl>,
#   sn_f_014 <dbl>, sn_f_1524 <dbl>, sn_f_2534 <dbl>, sn_f_3544 <dbl>,
#   sn_f_4554 <dbl>, sn_f_5564 <dbl>, sn_f_65 <dbl>, ep_m_014 <dbl>, …

Clase 9: entrega II

Ponderación: 15%

Instrucciones

El día de la entrega tendrás subido una plantilla de entrega en formato .qmd en el campus.

  • Descomprime la carpeta (¡importante! si no descomprimes, aunque puedas editar el .qmd, no podrás generar el .html)

  • Edita la cabecera con tu nombre y DNI

  • Deberás rellenar cada chunk con el código que consideres (en algunos te he dejado pistas) y cambiar de #| eval: false a #| eval: true (si los quitas directamente, por defecto ya es true)

  • Deberás de comentar con texto normal lo que consideres para responder a las preguntas.

  • Será OBLIGATORIO subir el archivo .html generado (solo se corregirá dicho archivo) así que ve renderizando según rellenas el documento, no lo dejes para el final.

Clase 10: tidyverse (filas)

Tidyverse: operaciones por filas

Resolución de entrega II

El nombre de las columnas codifica el sexo (H hombre, M mujer, NC no consta) y el grupo etario (0-9, 10-19, 20-29, 30-39, 40-49, 50-59, 60-69, 70-79, ≥80 años y NC no consta).

library(readr)
datos <- read_csv(file = "./datos/messy_covid_data.csv")
datos
# A tibble: 9,486 × 31
   provincia_iso fecha               `0-9_H` `10-19_H` `20-29_H` `30-39_H`
   <chr>         <dttm>                <dbl>     <dbl>     <dbl>     <dbl>
 1 A             2020-03-01 00:00:00       0         0         0         0
 2 AL            2020-03-01 00:00:00       0         0         0         0
 3 B             2020-03-01 00:00:00       0         0         0         0
 4 BA            2020-03-01 00:00:00       0         1         0         0
 5 BI            2020-03-01 00:00:00       0         0         0         0
 6 C             2020-03-01 00:00:00       0         0         0         0
 7 CA            2020-03-01 00:00:00       0         0         0         0
 8 CO            2020-03-01 00:00:00       0         0         0         0
 9 GI            2020-03-01 00:00:00       0         0         0         0
10 GR            2020-03-01 00:00:00       0         0         0         0
# ℹ 9,476 more rows
# ℹ 25 more variables: `40-49_H` <dbl>, `50-59_H` <dbl>, `60-69_H` <dbl>,
#   `70-79_H` <dbl>, `80-Inf_H` <dbl>, `NC-NC_H` <dbl>, `0-9_M` <dbl>,
#   `10-19_M` <dbl>, `20-29_M` <dbl>, `30-39_M` <dbl>, `40-49_M` <dbl>,
#   `50-59_M` <dbl>, `60-69_M` <dbl>, `70-79_M` <dbl>, `80-Inf_M` <dbl>,
#   `NC-NC_M` <dbl>, `0-9_NC` <dbl>, `10-19_NC` <dbl>, `20-29_NC` <dbl>,
#   `30-39_NC` <dbl>, `40-49_NC` <dbl>, `50-59_NC` <dbl>, `60-69_NC` <dbl>, …

Resolución de entrega II

Diseña un bucle que recorra todas las filas y, salvo en las dos primeras columnas, convierta cada 0 casos que encuentre en la fila por un NA

Necesitamos 2 bucles: uno que recorra filas (desde la primera hasta la última) y otro que recorra columnas (desde la tercera hasta la última), y dentro del bucle: si el elemento (i, j) es 0 (o ausente), lo pasamos a ausente; en caso contrario, lo que había guardado.

library(dplyr)
for (i in 1:nrow(datos)) {
  for (j in 3:ncol(datos)) {
  
    datos[i, j] <- if_else(datos[i, j] == 0, NA, datos[i, j], missing = NA)
    
  }
}

Resolución de entrega II

Razona por qué no es tidydata y conviérte a tidy data. Si te fijas hay muchísimas filas que nos sobran (ausentes) así que haz que al pivotar las elimine.

 

Es un pivot_longer() sin más: le indicamos que columnas no queremos pivotar, la variable a donde irán los nombres y cómo se llamará la variable de casos; le incluimos values_drop_na = TRUE para que elimine los ausentes (de ahí que hayamos convertido los 0 para hacerlo más sencillo aquí)

tidy_covid <-
  datos |> 
  pivot_longer(cols = -c("provincia_iso", "fecha"), names_to = "grupo", values_to = "casos",
               values_drop_na = TRUE)
tidy_covid
# A tibble: 36,842 × 4
   provincia_iso fecha               grupo    casos
   <chr>         <dttm>              <chr>    <dbl>
 1 B             2020-03-01 00:00:00 20-29_M      1
 2 BA            2020-03-01 00:00:00 10-19_H      1
 3 BA            2020-03-01 00:00:00 50-59_H      1
 4 GI            2020-03-01 00:00:00 80-Inf_M     1
 5 LE            2020-03-01 00:00:00 20-29_H      2
 6 LE            2020-03-01 00:00:00 60-69_H      1
 7 LE            2020-03-01 00:00:00 50-59_M      1
 8 M             2020-03-01 00:00:00 30-39_H      2
 9 M             2020-03-01 00:00:00 40-49_H      3
10 M             2020-03-01 00:00:00 50-59_H      2
# ℹ 36,832 more rows

Resolución de entrega II

Una de las columnas la tenemos codificada a veces como cosa-cosa_cosa, otras como 80-Inf_cosa, otras como NC-NC_cosa. Intenta separar dicha columna para generar tres columnas nuevas edad_inf, edad_sup y sexo de manera adecuada. Recuerda que NC es un ausente.

 

Si lo intentamos hacer sin procesar nada antes, vemos que los Inf y los NC figuran como tal, como cadena texto, lo que nos impide convertir a numérica el resto de la columna.

tidy_covid2 <-
  tidy_covid |> 
  separate(col = "grupo", into = c("edad_inf", "edad_sup", "sexo"), convert = TRUE)
tidy_covid2
# A tibble: 36,842 × 6
   provincia_iso fecha               edad_inf edad_sup sexo  casos
   <chr>         <dttm>              <chr>    <chr>    <chr> <dbl>
 1 B             2020-03-01 00:00:00 20       29       M         1
 2 BA            2020-03-01 00:00:00 10       19       H         1
 3 BA            2020-03-01 00:00:00 50       59       H         1
 4 GI            2020-03-01 00:00:00 80       Inf      M         1
 5 LE            2020-03-01 00:00:00 20       29       H         2
 6 LE            2020-03-01 00:00:00 60       69       H         1
 7 LE            2020-03-01 00:00:00 50       59       M         1
 8 M             2020-03-01 00:00:00 30       39       H         2
 9 M             2020-03-01 00:00:00 40       49       H         3
10 M             2020-03-01 00:00:00 50       59       H         2
# ℹ 36,832 more rows

Resolución de entrega II

Por eso antes vamos a reemplazar los "Inf" y "NC" por "NA", para que luego al convertirlos pueda pasarlos a NA numéricos

tidy_covid$grupo <- str_replace_all(tidy_covid$grupo, "Inf|NC", "NA")
tidy_covid2 <-
  tidy_covid |>
  separate(col = "grupo", into = c("edad_inf", "edad_sup", "sexo"), convert = TRUE)
tidy_covid2
# A tibble: 36,842 × 6
   provincia_iso fecha               edad_inf edad_sup sexo  casos
   <chr>         <dttm>                 <int>    <int> <chr> <dbl>
 1 B             2020-03-01 00:00:00       20       29 M         1
 2 BA            2020-03-01 00:00:00       10       19 H         1
 3 BA            2020-03-01 00:00:00       50       59 H         1
 4 GI            2020-03-01 00:00:00       80       NA M         1
 5 LE            2020-03-01 00:00:00       20       29 H         2
 6 LE            2020-03-01 00:00:00       60       69 H         1
 7 LE            2020-03-01 00:00:00       50       59 M         1
 8 M             2020-03-01 00:00:00       30       39 H         2
 9 M             2020-03-01 00:00:00       40       49 H         3
10 M             2020-03-01 00:00:00       50       59 H         2
# ℹ 36,832 more rows

Resolución de entrega II

Incorpora una nueva variable a la tabla que codifice el mes y el año (por ejemplo, cualquier día de enero de 2020 será algo similar a “1-2020” y cualquier día de febrero del 2021 será “2-2021”).

library(glue)
tidy_covid2$mes_year <- glue("{month(tidy_covid2$fecha)}-{year(tidy_covid2$fecha)}")
tidy_covid2
# A tibble: 36,842 × 7
   provincia_iso fecha               edad_inf edad_sup sexo  casos mes_year
   <chr>         <dttm>                 <int>    <int> <chr> <dbl> <glue>  
 1 B             2020-03-01 00:00:00       20       29 M         1 3-2020  
 2 BA            2020-03-01 00:00:00       10       19 H         1 3-2020  
 3 BA            2020-03-01 00:00:00       50       59 H         1 3-2020  
 4 GI            2020-03-01 00:00:00       80       NA M         1 3-2020  
 5 LE            2020-03-01 00:00:00       20       29 H         2 3-2020  
 6 LE            2020-03-01 00:00:00       60       69 H         1 3-2020  
 7 LE            2020-03-01 00:00:00       50       59 M         1 3-2020  
 8 M             2020-03-01 00:00:00       30       39 H         2 3-2020  
 9 M             2020-03-01 00:00:00       40       49 H         3 3-2020  
10 M             2020-03-01 00:00:00       50       59 H         2 3-2020  
# ℹ 36,832 more rows

Resolución de entrega II

Haciendo uso de esa variable de grupo mes_year y del vector de provincias permitidas que aparece debajo (busca en https://es.wikipedia.org/wiki/ISO_3166-2:ES#Provincias) obtén un resumen que nos devuelva en un tibble, por cada provincia permitida y cada mes-año, la media de casos (sin importar edad ni sexo)

 

Solo necesitamos recorrer cada provincia permitida y cada valor mes_year distinto, así que creamos ambos vectores. También creamos un dataset vacío resumen_mes_provincia donde, en cada iteración, pegaremos una fila con su resumen.

provincias_permitidas <- c("M", "B", "SE", "V", "Z")
for_values <- unique(tidy_covid2$mes_year)
resumen_mes_provincia <- tibble()

Resolución de entrega II

Una vez creados esos objetos, el bucle simplemente deberá recorrer cada valor de provincia y de mes-año: para el mes-año i y la provincia j, filtramos solo los casos para esos valores concretos (de todo el vector de casos nos quedamos con aquellos que cumplan dicha condición, eliminando también provincias ausentes). Tras filtrar guardamos la media ese filtro de casos

for (i in 1:length(for_values)) {
  for (j in 1:length(provincias_permitidas)) {
    
    casos_mes_provincia <-
      tidy_covid2$casos[!is.na(tidy_covid2$provincia_iso) &
                          tidy_covid2$provincia_iso == provincias_permitidas[j] &
                          tidy_covid2$mes_year == for_values[i]]
    
    resumen_mes_provincia <-
      rbind(resumen_mes_provincia, 
            tibble("mes_year" = for_values[i], "provincia_iso" = provincias_permitidas[j],
                   "casos" = mean(casos_mes_provincia, na.rm = TRUE)))
  }
}

Resolución de entrega II

Diseña una función resumen_por_fecha_provincia() que dada un vector de códigos ISO de provincias permitidas, y un tabla, nos devuelva la media de casos diarios que hubo cada mes-año (sin importar sexo ni edad) en las provincias permitidas. Es decir: debes “adaptar” el código anterior a una función.

 

Simplemente debes copiar y pegar el código de los dos anteriores (salvo el vector de provincias que ahora será un argumento), y adaptarlo a que las provincias están en prov_iso y la tabla en datos.

Resolución de entrega II

resumen_por_fecha_provincia <- function(prov_iso, datos) {
  
  datos$mes_year <- glue("{month(datos$fecha)}-{year(datos$fecha)}")
  for_values <- unique(datos$mes_year)
  resumen_mes_provincia <- tibble()
  
  for (i in 1:length(for_values)) {
    for (j in 1:length(prov_iso)) {
      
      casos_mes_provincia <-
        datos$casos[!is.na(datos$provincia_iso) &
                            datos$provincia_iso == prov_iso[j] &
                            datos$mes_year == for_values[i]]
      
      resumen_mes_provincia <-
        rbind(resumen_mes_provincia, 
              tibble("mes_year" = for_values[i], "provincia_iso" = prov_iso[j],
                     "casos" = mean(casos_mes_provincia, na.rm = TRUE)))
    }
  }

    return(resumen_mes_provincia)
}
resumen_por_fecha_provincia(c("M", "B", "SE", "V", "Z"), tidy_covid2)
# A tibble: 30 × 3
   mes_year provincia_iso casos
   <chr>    <chr>         <dbl>
 1 3-2020   M             79.3 
 2 3-2020   B             47.2 
 3 3-2020   SE             5.66
 4 3-2020   V              9.65
 5 3-2020   Z              6.63
 6 4-2020   M             48.0 
 7 4-2020   B             41.4 
 8 4-2020   SE             3.02
 9 4-2020   V              5.78
10 4-2020   Z              5.42
# ℹ 20 more rows

¿Qué es tidyverse?

  • {tibble}: optimizando data.frame
  • {tidyr}: limpieza de datos
  • {readr}: carga datos rectangulares (.csv)
  • {dplyr}: gramática para depurar
  • {stringr}: manejo de textos
  • {ggplot2}: visualización de datos
  • {tidymodels}: modelización/predicción

Preprocesamiento: dplyr

Dentro de {tidyverse} usaremos el paquete {dplyr} para el preprocesamiento y depuración de los datos.

datos |>
  limpio(...) |>
  filtro(...) |>
  selecciono(...) |>
  ordeno(...) |>
  modifico(...) |>
  renombro(...) |>
  agrupo(...) |>
  cuento(...) |>
  resumo(...) |>
  pinto(...)

La idea es que el código sea legible, como si fuese una lista de instrucciones que al leerla nos diga de manera muy evidente lo que está haciendo.

Hipótesis: tidydata

Toda la depuración que vamos a realizar es sobre la hipótesis de que nuestros datos están en tidydata

Recuerda que en {tidyverse} será clave el operador pipe (tubería) definido como |> (ctrl+shift+M): será una tubería que recorre los datos y los transforma.

Vamos a practicar con el dataset starwars del paquete cargado {dplyr}

library(tidyverse)
starwars

Muestreo

Una de las operaciones más comunes es lo que se conoce en estadística como muestreo: una selección o filtrado de registros (una submuestra)

  • No aleatorio (por cuotas): en base a condiciones lógicas sobre los registros (filter())
  • No aleatorio (intencional/discreccional): en base a posición (slice())
  • Aleatorio simple (slice_sample())
  • Aleatorio estratificado (group_by() + slice_sample())

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

El más simple es cuando filtramos registros en base a alguna condición lógica: con filter() se seleccionarán solo individuos que cumplan ciertas condiciones (muestreo no aleatorio por condiciones)

  • ==, !=: igual o distinto que (|> filter(variable == "a"))
  • >, <: mayor o menor que (|> filter(variable < 3))
  • >=, <=: mayor o igual o menor o igual que (|> filter(variable >= 5))
  • %in%: valores pertenencen a un listado de opciones (|> filter(variable %in% c("azul", "verde")))
  • between(variable, val1, val2): si los valores (continuos) caen dentro de un rango de valores (|> filter(between(variable, 160, 180)))

Filtrar filas: filter()

Dichas condiciones lógicas las podemos combinar de diferentes maneras (y, o, o excluyente)

Importante

Recuerda que dentro de filter() debe ir siempre algo que devuelva un vector de valores lógicos.

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

¿Cómo harías para… filtrar los personajes de ojos marrones?

¿Qué tipo de variable es? –> La variable eye_color es cualitativa así que está representada por textos

starwars |>
  filter(eye_color == "brown")
# A tibble: 21 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Leia Or…    150  49   brown      light      brown           19   fema… femin…
 2 Biggs D…    183  84   black      light      brown           24   male  mascu…
 3 Han Solo    180  80   brown      fair       brown           29   male  mascu…
 4 Yoda         66  17   white      green      brown          896   male  mascu…
 5 Boba Fe…    183  78.2 black      fair       brown           31.5 male  mascu…
 6 Lando C…    177  79   black      dark       brown           31   male  mascu…
 7 Arvel C…     NA  NA   brown      fair       brown           NA   male  mascu…
 8 Wicket …     88  20   brown      brown      brown            8   male  mascu…
 9 Padmé A…    185  45   brown      light      brown           46   fema… femin…
10 Quarsh …    183  NA   black      dark       brown           62   male  mascu…
# ℹ 11 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

¿Cómo harías para… filtrar los personajes que no tienen ojos marrones?

starwars |>
  filter(eye_color != "brown")
# A tibble: 66 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 6 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 7 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 8 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
 9 Anakin …    188    84 blond      fair       blue            41.9 male  mascu…
10 Wilhuff…    180    NA auburn, g… fair       blue            64   male  mascu…
# ℹ 56 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

¿Cómo harías para … filtrar los personajes que tengan los ojos marrones o azules?

starwars |>
  filter(eye_color %in% c("blue", "brown"))
# A tibble: 40 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 3 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 4 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 5 Biggs D…    183    84 black      light      brown           24   male  mascu…
 6 Anakin …    188    84 blond      fair       blue            41.9 male  mascu…
 7 Wilhuff…    180    NA auburn, g… fair       blue            64   male  mascu…
 8 Chewbac…    228   112 brown      unknown    blue           200   male  mascu…
 9 Han Solo    180    80 brown      fair       brown           29   male  mascu…
10 Jek Ton…    180   110 brown      fair       blue            NA   <NA>  <NA>  
# ℹ 30 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

Fíjate que %in% es equivalente a concatenar varios == con una conjunción o (|)

starwars |>
  filter(eye_color == "blue" | eye_color == "brown")
# A tibble: 40 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 3 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 4 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 5 Biggs D…    183    84 black      light      brown           24   male  mascu…
 6 Anakin …    188    84 blond      fair       blue            41.9 male  mascu…
 7 Wilhuff…    180    NA auburn, g… fair       blue            64   male  mascu…
 8 Chewbac…    228   112 brown      unknown    blue           200   male  mascu…
 9 Han Solo    180    80 brown      fair       brown           29   male  mascu…
10 Jek Ton…    180   110 brown      fair       blue            NA   <NA>  <NA>  
# ℹ 30 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

¿Cómo harías para … filtrar los personajes que midan entre 120 y 160 cm?

¿Qué tipo de variable es? –> La variable height es cuantitativa continua así que deberemos filtrar por rangos de valores (intervalos) –> usaremos between()

starwars |>
  filter(between(height, 120, 160))
# A tibble: 6 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Leia Org…    150    49 brown      light      brown             19 fema… femin…
2 Mon Moth…    150    NA auburn     fair       blue              48 fema… femin…
3 Nien Nunb    160    68 none       grey       black             NA male  mascu…
4 Watto        137    NA black      blue, grey yellow            NA male  mascu…
5 Gasgano      122    NA none       white, bl… black             NA male  mascu…
6 Cordé        157    NA brown      light      brown             NA <NA>  <NA>  
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

¿Cómo harías… filtrar los personajes que tengan ojos y no sean humanos?

starwars |>
  filter(eye_color == "brown" & species != "Human")
# A tibble: 3 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Yoda          66    17 white      green      brown            896 male  mascu…
2 Wicket S…     88    20 brown      brown      brown              8 male  mascu…
3 Eeth Koth    171    NA black      brown      brown             NA male  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Filtrar filas: filter()

datos |>
  filtro(condicion)
starwars |>
  filter(condicion)

¿Cómo harías… filtrar los personajes que tengan ojos y no sean humanos, o que tengan más de 60 años? Piénsalo bien: los paréntesis son importantes: no es lo mismo \((a+b)*c\) que \(a+(b*c)\)

starwars |>
  filter((eye_color == "brown" & species != "Human") | birth_year > 60)
# A tibble: 18 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 C-3PO       167    75 <NA>       gold       yellow           112 none  mascu…
 2 Wilhuff…    180    NA auburn, g… fair       blue              64 male  mascu…
 3 Chewbac…    228   112 brown      unknown    blue             200 male  mascu…
 4 Jabba D…    175  1358 <NA>       green-tan… orange           600 herm… mascu…
 5 Yoda         66    17 white      green      brown            896 male  mascu…
 6 Palpati…    170    75 grey       pale       yellow            82 male  mascu…
 7 Wicket …     88    20 brown      brown      brown              8 male  mascu…
 8 Qui-Gon…    193    89 brown      fair       blue              92 male  mascu…
 9 Finis V…    170    NA blond      fair       blue              91 male  mascu…
10 Quarsh …    183    NA black      dark       brown             62 male  mascu…
11 Shmi Sk…    163    NA black      fair       brown             72 fema… femin…
12 Mace Wi…    188    84 none       dark       brown             72 male  mascu…
13 Ki-Adi-…    198    82 white      pale       yellow            92 male  mascu…
14 Eeth Ko…    171    NA black      brown      brown             NA male  mascu…
15 Cliegg …    183    NA brown      fair       blue              82 male  mascu…
16 Dooku       193    80 white      fair       brown            102 male  mascu…
17 Bail Pr…    191    NA black      tan        brown             67 male  mascu…
18 Jango F…    183    79 black      tan        brown             66 male  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Eliminar ausentes: drop_na()

datos |>
  retirar_ausentes(var1, var2, ...)
starwars |>
  drop_na(var1, var2, ...)

Hay un filtro especial para una de las operaciones más habituales en depuración: retirar los ausentes. Para ello podemos usar dentro de un filtro is.na(), que nos devuelve TRUE/FALSE en función de si es ausente, o bien …

Usar drop_na(): si no indicamos variable, elimina registros con ausente en cualquier variable. Más adelante veremos como imputar esos ausentes

starwars |>
  drop_na(mass, height)
# A tibble: 7 × 4
  name                mass height hair_color 
  <chr>              <dbl>  <int> <chr>      
1 Luke Skywalker        77    172 blond      
2 C-3PO                 75    167 <NA>       
3 R2-D2                 32     96 <NA>       
4 Darth Vader          136    202 none       
5 Leia Organa           49    150 brown      
6 Owen Lars            120    178 brown, grey
7 Beru Whitesun Lars    75    165 brown      
starwars |>
  drop_na()
# A tibble: 7 × 4
  name                mass height hair_color   
  <chr>              <dbl>  <int> <chr>        
1 Luke Skywalker        77    172 blond        
2 Darth Vader          136    202 none         
3 Leia Organa           49    150 brown        
4 Owen Lars            120    178 brown, grey  
5 Beru Whitesun Lars    75    165 brown        
6 Biggs Darklighter     84    183 black        
7 Obi-Wan Kenobi        77    182 auburn, white

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Selecciona del conjunto de starwars solo los personajes que sean androides o cuyo valor en species sea desconocido

Código
starwars |>
  filter(species == "Droid" | is.na(species))

📝 Selecciona del conjunto de starwars solo los personajes cuyo peso esté entre 65 y 90 kg.

Código
starwars |> filter(between(mass, 65, 90))

📝 Tras limpiar de ausentes en todas las variables, selecciona del conjunto de starwars solo los personajes que sean humanos y que vengan de Tatooine

Código
starwars |>
  drop_na() |> 
  filter(species == "Human" & homeworld == "Tatooine")

📝 Selecciona del conjunto original de starwars los personajes no humanos, male en el sexo y que midan entre 120 y 170 cm, o los personajes con ojos marrones o rojos.

Código
starwars |>
  filter((species != "Human" & sex == "male" &
            between(height, 120, 170)) |
           eye_color %in% c("brown", "red"))

📝 Busca información en la ayuda de la función str_detect() del paquete {stringr} (cargado en {tidyverse}). Consejo: prueba antes las funciones que vayas a usar con algún vector de prueba para poder comprobar su funcionamiento. Tras saber lo que hace, filtra solo aquellos personajes con apellido Skywalker

Código
starwars |> filter(str_detect(name, "Skywalker"))

Rebanadas de datos: slice()

datos |> rebanadas(posiciones)
starwars |> slice(posiciones)

A veces nos puede interesar realizar un muestreo no aleatorio discreccional, o lo que es lo mismo, filtrar por posición: con slice(posiciones) podremos seleccionar filas concretas pasando como argumento un vector de índices

# fila 1
starwars |>
  slice(1)
# A tibble: 1 × 4
  name           height  mass hair_color
  <chr>           <int> <dbl> <chr>     
1 Luke Skywalker    172    77 blond     
# filas de la 7 a la 9
starwars |>
  slice(7:9)
# A tibble: 3 × 4
  name               height  mass hair_color
  <chr>               <int> <dbl> <chr>     
1 Beru Whitesun Lars    165    75 brown     
2 R5-D4                  97    32 <NA>      
3 Biggs Darklighter     183    84 black     
# filas 2, 7, 10 y 31
starwars |>
  slice(c(2, 7, 10, 31))
# A tibble: 4 × 8
  name             height  mass hair_color skin_color eye_color birth_year sex  
  <chr>             <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr>
1 C-3PO               167    75 <NA>       gold       yellow           112 none 
2 Beru Whitesun L…    165    75 brown      light      blue              47 fema…
3 Obi-Wan Kenobi      182    77 auburn, w… fair       blue-gray         57 male 
4 Qui-Gon Jinn        193    89 brown      fair       blue              92 male 

Rebanadas de datos: slice()

datos |>
  rebanadas(posiciones)
starwars |>
  slice(posiciones)

Disponemos de opciones por defecto:

  • con slice_head(n = ...) y slice_tail(n = ...) podemos obtener la cabecera y cola de la tabla
starwars |> slice_head(n = 2)
# A tibble: 2 × 4
  name           height  mass hair_color
  <chr>           <int> <dbl> <chr>     
1 Luke Skywalker    172    77 blond     
2 C-3PO             167    75 <NA>      
starwars |> slice_tail(n = 2)
# A tibble: 2 × 4
  name           height  mass hair_color
  <chr>           <int> <dbl> <chr>     
1 BB8                NA    NA none      
2 Captain Phasma     NA    NA none      

Rebanadas de datos: slice()

datos |>
  rebanadas(posiciones)
starwars |>
  slice(posiciones)

Disponemos de opciones por defecto:

  • con slice_max() y slice_min() obtenemos la filas con menor/mayor valor de una variable (si empate, todas salvo que with_ties = FALSE) que indicamos en order_by = ...
starwars |> slice_min(mass, n = 2)
# A tibble: 2 × 4
  name         height  mass hair_color
  <chr>         <int> <dbl> <chr>     
1 Ratts Tyerel     79    15 none      
2 Yoda             66    17 white     
starwars |> slice_max(height, n = 2)
# A tibble: 2 × 4
  name        height  mass hair_color
  <chr>        <int> <dbl> <chr>     
1 Yarael Poof    264    NA none      
2 Tarfful        234   136 brown     

Aleatorio: slice_sample()

datos |>
  rebanadas_aleatorias(posiciones)
starwars |>
  slice_sample(posiciones)

El conocido como muestreo aleatorio simple se basa en seleccionar individuos aleatoriamente, de forma que cada uno tenga ciertas probabilidades de ser seleccionado. Con slice_sample(n = ...) podemos extraer n registros aleatoriamente (a priori equiprobables).

starwars |> slice_sample(n = 2)
# A tibble: 2 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Mas Amed…    196    NA none       blue       blue              NA male  mascu…
2 Shaak Ti     178    57 none       red, blue… black             NA fema… femin…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Importante…

«Aleatorio» no implica equiprobable: es igual de aleatorio un dado normal que uno trucado. No hay cosas «más aleatorias» que otras, simplemente tienen subyacente distintas leyes de probabilidad.

Aleatorio: slice_sample()

datos |>
  rebanadas_aleatorias(posiciones)
starwars |>
  slice_sample(posiciones)

También podremos indicarle la proporción de datos a samplear (en lugar del número) y si queremos que sea con reemplazamiento (que se puedan repetir).

# 5% de registros aleatorios con reemplazamiento
starwars |> 
  slice_sample(prop = 0.05, replace = TRUE)
# A tibble: 4 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Obi-Wan …    182    77 auburn, w… fair       blue-gray         57 male  mascu…
2 Chewbacca    228   112 brown      unknown    blue             200 male  mascu…
3 Grievous     216   159 none       brown, wh… green, y…         NA male  mascu…
4 Finn          NA    NA black      dark       dark              NA male  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Aleatorio: slice_sample()

datos |>
  rebanadas_aleatorias(posiciones)
starwars |>
  slice_sample(posiciones)

Como decíamos, «aleatorio» no es igual que «equiprobable», así que podemos pasarle un vector de probabilidades. Por ejemplo, vamos a forzar que sea muy improbable sacar una fila que no sean las dos primeras

starwars |>
  slice_sample(n = 2, weight_by = c(0.495, 0.495, rep(0.01/85, 85)))
# A tibble: 2 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 C-3PO        167    75 <NA>       gold       yellow           112 none  mascu…
2 Luke Sky…    172    77 blond      fair       blue              19 male  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>
starwars |>
  slice_sample(n = 2, weight_by = c(0.495, 0.495, rep(0.01/85, 85)))
# A tibble: 2 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 C-3PO        167    75 <NA>       gold       yellow           112 none  mascu…
2 Luke Sky…    172    77 blond      fair       blue              19 male  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Paréntesis: sample()

La función slice_sample() es simplemente una integración de {tidyverse} de la función básica de R conocida como sample() que nos permite muestrear elementos

Por ejemplo, vamos a muestrear 10 tiradas de un dado, indicándole

  • soporte de nuestra variable aleatorio (valores permitidos en x)
  • tamaño muestral (size)
  • reemplazamiento (si TRUE entonces pueden salir repetidas, como en el caso del dado)
sample(x = 1:6, size = 10, replace = TRUE)
 [1] 4 4 4 1 1 1 6 6 4 6

Paréntesis: sample()

La opción anterior lo que genera son sucesos de una variable aleatoria equiprobable pero al igual que antes, podemos asignarle un vector de probabilidades o función de masa concreta con el argumento prob = ...

sample(x = 1:6, size = 50, replace = TRUE,
       prob = c(0.5, 0.2, 0.1, 0.1, 0.05, 0.05))
 [1] 1 4 3 1 1 2 1 5 1 1 4 1 4 2 1 1 5 1 2 1 1 1 1 1 1 3 1 2 4 2 1 1 2 4 3 1 2 2
[39] 3 1 2 1 2 2 1 1 1 2 3 2

Paréntesis: sample()

¿Cómo harías el siguiente enunciado?

 

Supongamos que en una ciudad se han estudiado episodios de gripe estacional. Sean las variables aleatorias \(X_m\) y \(X_p\) tal que \(X_m=1\) si la madre tiene gripe, \(X_m=0\) si la madre no tiene gripe, \(X_p=1\) si el padre tiene gripe y \(X_p=0\) si el padre no tiene gripe. El modelo teórico asociado a este tipo de epidemias indica que la distribución conjunta viene dada por \(P(X_m = 1, X_p=1)=0.02\), \(P(X_m = 1, X_p=0)=0.08\), \(P(X_m = 1, X_p=0)=0.1\) y \(P(X_m = 0, X_p=0)=0.8\)

Genera una muestra de tamaño \(n = 1000\) (soporte "10", "01", "00" y "11") haciendo uso de runif() y haciendo uso de sample()

Reordenar filas: arrange()

datos |> ordenar(var1, var2, ...)
starwars |> arrange(var1, var2, ...)

También podemos ordenar filas en función de alguna variable con arrange()

starwars |> arrange(mass)
# A tibble: 5 × 6
  name                  height  mass hair_color skin_color  eye_color
  <chr>                  <int> <dbl> <chr>      <chr>       <chr>    
1 Ratts Tyerel              79    15 none       grey, blue  unknown  
2 Yoda                      66    17 white      green       brown    
3 Wicket Systri Warrick     88    20 brown      brown       brown    
4 R2-D2                     96    32 <NA>       white, blue red      
5 R5-D4                     97    32 <NA>       white, red  red      

Por defecto de menor a mayor pero podemos invertir el orden con desc()

starwars |> arrange(desc(height))
# A tibble: 5 × 3
  name         height  mass
  <chr>         <int> <dbl>
1 Yarael Poof     264    NA
2 Tarfful         234   136
3 Lama Su         229    88
4 Chewbacca       228   112
5 Roos Tarpals    224    82
starwars |> arrange(mass, desc(height))
# A tibble: 5 × 3
  name                  height  mass
  <chr>                  <int> <dbl>
1 Ratts Tyerel              79    15
2 Yoda                      66    17
3 Wicket Systri Warrick     88    20
4 R5-D4                     97    32
5 R2-D2                     96    32

Eliminar duplicados: distinct()

datos |> sin_duplicados(var1, var2, ...)
starwars |> distinct(var1, var2, ...)

Muchas veces necesitaremos asegurarnos que no hay duplicados en alguna variable (DNI) y podemos eliminar filas duplicadas con distinct().

starwars |> distinct(sex)
# A tibble: 5 × 1
  sex           
  <chr>         
1 male          
2 none          
3 female        
4 hermaphroditic
5 <NA>          

Para mantener todas las columnas de la tabla usaremos .keep_all = TRUE.

starwars |> distinct(sex, .keep_all = TRUE)
# A tibble: 3 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Luke Sky…    172    77 blond      fair       blue              19 male  mascu…
2 C-3PO        167    75 <NA>       gold       yellow           112 none  mascu…
3 Leia Org…    150    49 brown      light      brown             19 fema… femin…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Añadir filas: bind_rows()

tibble1 |> encuadernar_filas(tibble2)
tibble1 |> bind_rows(tibble2)

Por último, podemos concatenar nuevas filas con bind_rows() con las nuevas observaciones en tabla (si no cuadran columnas rellena con ausentes)

datos <-
  tibble("nombre" = c("javi", "laura"), "edad" = c(33, 50))
datos
# A tibble: 2 × 2
  nombre  edad
  <chr>  <dbl>
1 javi      33
2 laura     50
datos |> bind_rows(tibble("nombre" = c("carlos", NA), "cp" = c(28045, 28019)))
# A tibble: 4 × 3
  nombre  edad    cp
  <chr>  <dbl> <dbl>
1 javi      33    NA
2 laura     50    NA
3 carlos    NA 28045
4 <NA>      NA 28019

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Selecciona solo los personajes que sean humanos y de ojos marrones, para después ordernarlos en altura descendente y peso ascendente.

Código
starwars |>
  filter(eye_color == "brown" & species == "Human") |> 
  arrange(height, desc(mass))

📝 Extrae 3 registros aleatoriamente.

Código
starwars |> slice_sample(n = 3)

📝 Extrae el 10% de los registros aleatoriamente.

Código
starwars |> slice_sample(prop = 0.1)

📝 Extrae aleatoriamente 10 personajes pero de forma que la probabilidad de que salga cada uno sea proporcional a su peso (más pesados, más probable)

Código
starwars |>
  drop_na(mass) |> 
  slice_sample(n = 10, weight_by = mass)

📝 Selecciona los 3 personajes más mayores.

Código
starwars |> slice_max(birth_year, n = 3)

📝 Para saber que valores únicos hay en el color de pelo, elimina duplicados de la variable hair_color, eliminando antes los ausentes de dicha variable.

Código
starwars |>
  drop_na(hair_color) |> 
  distinct(hair_color)

📝 De los personajes que son humanos y miden más de 160 cm, elimina duplicados en color de ojos, elimina ausentes en peso, selecciona los 3 más altos, y orden de mayor a menor peso. Devuelve la tabla.

Código
starwars |>
  filter(species == "Human" & height > 160) |> 
  distinct(eye_color, .keep_all = TRUE) |> 
  drop_na(mass) |> 
  slice_max(height, n = 3) |> 
  arrange(desc(mass))

Resumen

La clave de {tidyverse} es la legibilidad: es importantísimo que el código se entienda, por nuestro yo el futuro pero también por la transparencia algorítmica hacia los demás

Por ejemplo: quitaremos ausentes de la variable peso, filtraremos los personajes humanos y altura superior a 140cm, sin duplicados en el color de pelo, extrayendo los 5 más altos y obteniendo 2 personajes aleatorios finalmente.

starwars |>
  elimino_ausentes(peso) |> 
  filtro(especie humana Y altura > 140 cm) |> 
  sin_duplicados(color de pelo) |>
  rebanadas_max(peso, n = 5) |> 
  rebanadas_aleatorias(n = 2)
starwars |>
  drop_na(mass) |> 
  filter(species == "Human" & height > 140) |> 
  distinct(hair_color, .keep_all = TRUE) |>
  slice_max(mass, n = 5) |> 
  slice_sample(n = 2)

Resumen

La clave de {tidyverse} es la legibilidad: es importantísimo que el código se entienda, por nuestro yo el futuro pero también por la transparencia algorítmica hacia los demás

Por ejemplo: quitaremos ausentes de la variable peso, filtraremos los personajes humanos y altura superior a 140cm, sin duplicados en el color de pelo, extrayendo los 5 más altos y obteniendo 2 personajes aleatorios finalmente.

starwars |>
  elimino_ausentes(peso) |> 
  filtro(especie humana Y altura > 140 cm) |> 
  sin_duplicados(color de pelo) |>
  rebanadas_max(peso, n = 5) |> 
  rebanadas_aleatorias(n = 2)
starwars |>
  drop_na(mass) |> 
  filter(species == "Human" & height > 140) |> 
  distinct(hair_color, .keep_all = TRUE) |>
  slice_max(mass, n = 5) |> 
  slice_sample(n = 2)

Resumen

La clave de {tidyverse} es la legibilidad: es importantísimo que el código se entienda, por nuestro yo el futuro pero también por la transparencia algorítmica hacia los demás

Por ejemplo: quitaremos ausentes de la variable peso, filtraremos los personajes humanos y altura superior a 140cm, sin duplicados en el color de pelo, extrayendo los 5 más altos y obteniendo 2 personajes aleatorios finalmente.

starwars |>
  elimino_ausentes(peso) |> 
  filtro(especie humana Y altura > 140 cm) |> 
  sin_duplicados(color de pelo) |>
  rebanadas_max(peso, n = 5) |> 
  rebanadas_aleatorias(n = 2)
starwars |>
  drop_na(mass) |> 
  filter(species == "Human" & height > 140) |> 
  distinct(hair_color, .keep_all = TRUE) |>
  slice_max(mass, n = 5) |> 
  slice_sample(n = 2)

Resumen

La clave de {tidyverse} es la legibilidad: es importantísimo que el código se entienda, por nuestro yo el futuro pero también por la transparencia algorítmica hacia los demás

Por ejemplo: quitaremos ausentes de la variable peso, filtraremos los personajes humanos y altura superior a 140cm, sin duplicados en el color de pelo, extrayendo los 5 más altos y obteniendo 2 personajes aleatorios finalmente.

starwars |>
  elimino_ausentes(peso) |> 
  filtro(especie humana Y altura > 140 cm) |> 
  sin_duplicados(color de pelo) |>
  rebanadas_max(peso, n = 5) |> 
  rebanadas_aleatorias(n = 2)
starwars |>
  drop_na(mass) |> 
  filter(species == "Human" & height > 140) |> 
  distinct(hair_color, .keep_all = TRUE) |>
  slice_max(mass, n = 5) |> 
  slice_sample(n = 2)

Resumen

La clave de {tidyverse} es la legibilidad: es importantísimo que el código se entienda, por nuestro yo el futuro pero también por la transparencia algorítmica hacia los demás

Por ejemplo: quitaremos ausentes de la variable peso, filtraremos los personajes humanos y altura superior a 140cm, sin duplicados en el color de pelo, extrayendo los 5 más altos y obteniendo 2 personajes aleatorios finalmente.

starwars |>
  elimino_ausentes(peso) |> 
  filtro(especie humana Y altura > 140 cm) |> 
  sin_duplicados(color de pelo) |>
  rebanadas_max(peso, n = 5) |> 
  rebanadas_aleatorias(n = 2)
starwars |>
  drop_na(mass) |> 
  filter(species == "Human" & height > 140) |> 
  distinct(hair_color, .keep_all = TRUE) |>
  slice_max(mass, n = 5) |> 
  slice_sample(n = 2)

🐣 Caso práctico

Vamos a volver aun viejo conocido: en el paquete {datasets} (ya instalado por defecto) teníamos diversos conjuntos de datos y uno de ellos era airquality con el que ya trabajamos. Los datos capturan medidas diarias (n = 153 observaciones) de la calidad del aire en Nueva York, de mayo a septiembre de 1973.

En ese momento lo trabajamos desde la perspectiva de R base y extrayendo algunas variables del mismo. El objetivo ahora será trabajarlo desde la perspectiva de {tidyverse} fijándonos en las diferencias de una y otra forma.

library(datasets)
airquality

Intenta responder a las preguntas planteadas en el workbook.

Clase 11: tidyverse (columnas)

Tidyverse: operaciones por columnas

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

Hasta ahora todas las operaciones realizadas (aunque usásemos info de columnas) eran por filas. En elc aso de columnas, la acción más sencilla es seleccionar variables por nombre con select(), dando como argumentos los nombres de columnas sin comillas.

starwars |> select(name, hair_color)
# A tibble: 87 × 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  
 7 Beru Whitesun Lars brown        
 8 R5-D4              <NA>         
 9 Biggs Darklighter  black        
10 Obi-Wan Kenobi     auburn, white
# ℹ 77 more rows

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

La función select() nos permite seleccionar varias variables a la vez, incluso concatenando sus nombres como si fuesen índices numéricos

starwars |> select(name:eye_color) 
# A tibble: 4 × 6
  name           height  mass hair_color skin_color  eye_color
  <chr>           <int> <dbl> <chr>      <chr>       <chr>    
1 Luke Skywalker    172    77 blond      fair        blue     
2 C-3PO             167    75 <NA>       gold        yellow   
3 R2-D2              96    32 <NA>       white, blue red      
4 Darth Vader       202   136 none       white       yellow   

Y podemos deseleccionar columnas con - delante

starwars |>  select(-mass, -(eye_color:starships))
# A tibble: 4 × 4
  name           height hair_color skin_color 
  <chr>           <int> <chr>      <chr>      
1 Luke Skywalker    172 blond      fair       
2 C-3PO             167 <NA>       gold       
3 R2-D2              96 <NA>       white, blue
4 Darth Vader       202 none       white      

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

Tenemos además palabras reservadas: everything() todas las variables

starwars |> select(mass, homeworld, everything())
# A tibble: 4 × 14
   mass homeworld name   height hair_color skin_color eye_color birth_year sex  
  <dbl> <chr>     <chr>   <int> <chr>      <chr>      <chr>          <dbl> <chr>
1    77 Tatooine  Luke …    172 blond      fair       blue            19   male 
2    75 Tatooine  C-3PO     167 <NA>       gold       yellow         112   none 
3    32 Naboo     R2-D2      96 <NA>       white, bl… red             33   none 
4   136 Tatooine  Darth…    202 none       white      yellow          41.9 male 
# ℹ 5 more variables: gender <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

…y last_col() para referirnos a la última columna.

starwars |> select(name:mass, homeworld, last_col())
# A tibble: 4 × 5
  name           height  mass homeworld starships
  <chr>           <int> <dbl> <chr>     <list>   
1 Luke Skywalker    172    77 Tatooine  <chr [2]>
2 C-3PO             167    75 Tatooine  <chr [0]>
3 R2-D2              96    32 Naboo     <chr [0]>
4 Darth Vader       202   136 Tatooine  <chr [1]>

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

También podemos jugar con patrones en el nombre, aquellas que comiencen por un prefijo (starts_with()), terminen con un sufijo (ends_with()), contengan un texto (contains()) o cumplan una expresión regular (matches()).

# variables cuyo nombre acaba en "color" y contengan sexo o género
starwars |> select(ends_with("color"), matches("sex|gender"))
# A tibble: 87 × 5
   hair_color    skin_color  eye_color sex    gender   
   <chr>         <chr>       <chr>     <chr>  <chr>    
 1 blond         fair        blue      male   masculine
 2 <NA>          gold        yellow    none   masculine
 3 <NA>          white, blue red       none   masculine
 4 none          white       yellow    male   masculine
 5 brown         light       brown     female feminine 
 6 brown, grey   light       blue      male   masculine
 7 brown         light       blue      female feminine 
 8 <NA>          white, red  red       none   masculine
 9 black         light       brown     male   masculine
10 auburn, white fair        blue-gray male   masculine
# ℹ 77 more rows

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

Incluso podemos seleccionar por rango numérico si tenemos variables con un prefijo y números.

datos <-
  tibble("semana1" = c(115, 141, 232), "semana2" = c(7, NA, 17),
         "semana3" = c(95, 162, NA), "semana4" = c(11, 19, 15),
         "semana5" = c(NA, 262, 190), "semana6" = c(21, 15, 23))

Con num_range() podemos seleccionar con un prefijo y una secuencia numérica.

datos |> select(num_range("semana", 1:4))
# A tibble: 3 × 4
  semana1 semana2 semana3 semana4
    <dbl>   <dbl>   <dbl>   <dbl>
1     115       7      95      11
2     141      NA     162      19
3     232      17      NA      15

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

Por último, podemos seleccionar columnas por tipo de dato haciendo uso de where() y dentro una función que devuelva un valor lógico de tipo de dato.

# Solo columnas numéricas o de texto
starwars |> select(where(is.numeric) | where(is.character))
# A tibble: 87 × 11
   height  mass birth_year name     hair_color skin_color eye_color sex   gender
    <int> <dbl>      <dbl> <chr>    <chr>      <chr>      <chr>     <chr> <chr> 
 1    172    77       19   Luke Sk… blond      fair       blue      male  mascu…
 2    167    75      112   C-3PO    <NA>       gold       yellow    none  mascu…
 3     96    32       33   R2-D2    <NA>       white, bl… red       none  mascu…
 4    202   136       41.9 Darth V… none       white      yellow    male  mascu…
 5    150    49       19   Leia Or… brown      light      brown     fema… femin…
 6    178   120       52   Owen La… brown, gr… light      blue      male  mascu…
 7    165    75       47   Beru Wh… brown      light      blue      fema… femin…
 8     97    32       NA   R5-D4    <NA>       white, red red       none  mascu…
 9    183    84       24   Biggs D… black      light      brown     male  mascu…
10    182    77       57   Obi-Wan… auburn, w… fair       blue-gray male  mascu…
# ℹ 77 more rows
# ℹ 2 more variables: homeworld <chr>, species <chr>

Mover columnas: relocate()

datos |>
  recolocar(var1, despues_de = var2)
starwars |>
  relocate(var1, .after = var2)

Para facilitar la recolocación de variables tenemos una función para ello, relocate(), indicándole en .after o .before detrás o delante de qué columnas queremos moverlas.

starwars |> relocate(species, .before = name)
# A tibble: 87 × 14
   species name    height  mass hair_color skin_color eye_color birth_year sex  
   <chr>   <chr>    <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr>
 1 Human   Luke S…    172    77 blond      fair       blue            19   male 
 2 Droid   C-3PO      167    75 <NA>       gold       yellow         112   none 
 3 Droid   R2-D2       96    32 <NA>       white, bl… red             33   none 
 4 Human   Darth …    202   136 none       white      yellow          41.9 male 
 5 Human   Leia O…    150    49 brown      light      brown           19   fema…
 6 Human   Owen L…    178   120 brown, gr… light      blue            52   male 
 7 Human   Beru W…    165    75 brown      light      blue            47   fema…
 8 Droid   R5-D4       97    32 <NA>       white, red red             NA   none 
 9 Human   Biggs …    183    84 black      light      brown           24   male 
10 Human   Obi-Wa…    182    77 auburn, w… fair       blue-gray       57   male 
# ℹ 77 more rows
# ℹ 5 more variables: gender <chr>, homeworld <chr>, films <list>,
#   vehicles <list>, starships <list>

Renombrar: rename()

datos |> renombrar(nuevo = antiguo)
starwars |> rename(nuevo = antiguo)

A veces también podemos querer modificar la «metainformación» de los datos, renombrando columnas. Para ello usaremos de rename() poniendo primero el nombre nuevo y luego el antiguo.

starwars |> rename(nombre = name, altura = height, peso = mass)
# A tibble: 87 × 14
   nombre   altura  peso hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Extraer columnas: pull()

datos |> retirar(var)
starwars |> pull(var)

Si observas la salida de los select() sigue siendo una tabla tibble, ya que nos preserva la naturaleza de nuestros datos.

starwars |> select(name)
# A tibble: 87 × 1
   name              
   <chr>             
 1 Luke Skywalker    
 2 C-3PO             
 3 R2-D2             
 4 Darth Vader       
 5 Leia Organa       
 6 Owen Lars         
 7 Beru Whitesun Lars
 8 R5-D4             
 9 Biggs Darklighter 
10 Obi-Wan Kenobi    
# ℹ 77 more rows

Extraer columnas: pull()

datos |> retirar(var)
starwars |> pull(var)

A veces no querremos dicha estructura sino extraer literalmente la columna en un VECTOR, algo que podemos hacer con pull()

starwars |> pull(name)
 [1] "Luke Skywalker"        "C-3PO"                 "R2-D2"                
 [4] "Darth Vader"           "Leia Organa"           "Owen Lars"            
 [7] "Beru Whitesun Lars"    "R5-D4"                 "Biggs Darklighter"    
[10] "Obi-Wan Kenobi"        "Anakin Skywalker"      "Wilhuff Tarkin"       
[13] "Chewbacca"             "Han Solo"              "Greedo"               
[16] "Jabba Desilijic Tiure" "Wedge Antilles"        "Jek Tono Porkins"     
[19] "Yoda"                  "Palpatine"             "Boba Fett"            
[22] "IG-88"                 "Bossk"                 "Lando Calrissian"     
[25] "Lobot"                 "Ackbar"                "Mon Mothma"           
[28] "Arvel Crynyd"          "Wicket Systri Warrick" "Nien Nunb"            
[31] "Qui-Gon Jinn"          "Nute Gunray"           "Finis Valorum"        
[34] "Padmé Amidala"         "Jar Jar Binks"         "Roos Tarpals"         
[37] "Rugor Nass"            "Ric Olié"              "Watto"                
[40] "Sebulba"               "Quarsh Panaka"         "Shmi Skywalker"       
[43] "Darth Maul"            "Bib Fortuna"           "Ayla Secura"          
[46] "Ratts Tyerel"          "Dud Bolt"              "Gasgano"              
[49] "Ben Quadinaros"        "Mace Windu"            "Ki-Adi-Mundi"         
[52] "Kit Fisto"             "Eeth Koth"             "Adi Gallia"           
[55] "Saesee Tiin"           "Yarael Poof"           "Plo Koon"             
[58] "Mas Amedda"            "Gregar Typho"          "Cordé"                
[61] "Cliegg Lars"           "Poggle the Lesser"     "Luminara Unduli"      
[64] "Barriss Offee"         "Dormé"                 "Dooku"                
[67] "Bail Prestor Organa"   "Jango Fett"            "Zam Wesell"           
[70] "Dexter Jettster"       "Lama Su"               "Taun We"              
[73] "Jocasta Nu"            "R4-P17"                "Wat Tambor"           
[76] "San Hill"              "Shaak Ti"              "Grievous"             
[79] "Tarfful"               "Raymus Antilles"       "Sly Moore"            
[82] "Tion Medon"            "Finn"                  "Rey"                  
[85] "Poe Dameron"           "BB8"                   "Captain Phasma"       

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Filtra el conjunto de personajes y quédate solo con aquellos que en la variable height no tengan un dato ausente. Con los datos obtenidos del filtro anterior, selecciona solo las variables name, height, así como todas aquellas variables que CONTENGAN la palabra color en su nombre.

Código
starwars_2 <-
  starwars |> 
  drop_na(height) |> 
  select(name, height, contains("color"))

📝 Con los datos obtenidos del ejercicio anterior, traduce el nombre de las columnas a castellano.

Código
starwars_2 |> 
  rename(nombre = name, altura = height, color_pelo = hair_color,
         color_piel = skin_color, color_ojos = eye_color)

📝 Con los datos obtenidos del ejercicio 1, coloca la variable de color de pelo justo detrás de la variable de nombres.

Código
starwars_2 |>
  relocate(hair_color, .after = name)

📝 Con los datos obtenidos del ejercicio 1, comprueba cuántas modalidades únicas hay en la variable de color de pelo (sin usar unique()).

Código
starwars_2 |>
  distinct(hair_color) |> 
  nrow()

📝 Del conjunto de datos originales, elimina las columnas de tipo lista, y tras ello elimina duplicados en la variable eye_color. Tras eliminar duplicados extrae dicha columna en un vector.

Código
starwars |> 
  select(-where(is.list)) |> 
  distinct(eye_color, .keep_all = TRUE) |> 
  pull(eye_color)

📝 Del conjunto de datos original de starwars, con solo los personajes cuya altura es conocida, extrae en un vector con dicha variable.

Código
starwars |> 
  drop_na(height) |> 
  pull(height)

📝 Tras obtener el vector del ejercicio anterior, usa dicho vector para realizar un muestreo aleatorio del 50% de los datos de manera que la probabilidad de cada personaje de ser elegido sea inversamente proporcional a su altura (más bajitos, más opciones).

Código
heights <-
  starwars |> 
  drop_na(height) |> 
  pull(height)
  
starwars |> 
  drop_na(height) |> 
  slice_sample(prop = 0.5, weight_by = 1/heights)

Modificar columnas: mutate()

datos |> modificar(nueva = funcion())
starwars |> mutate(nueva = funcion())

En muchas ocasiones querremos modificar o crear variables con mutate().

Vamos a crear por ejemplo una nueva variable height_m con la altura en metros.

starwars |> mutate(height_m = height / 100)
# A tibble: 87 × 15
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 6 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>, height_m <dbl>

Modificar columnas: mutate()

datos |> modificar(nueva = funcion())
starwars |> mutate(nueva = funcion())

Además con los argumentos opcionales podemos recolocar la columna modificada

starwars |> 
  mutate(height_m = height / 100,
         IMC = mass / (height_m^2), .before = name)
# A tibble: 87 × 16
   height_m   IMC name   height  mass hair_color skin_color eye_color birth_year
      <dbl> <dbl> <chr>   <int> <dbl> <chr>      <chr>      <chr>          <dbl>
 1     1.72  26.0 Luke …    172    77 blond      fair       blue            19  
 2     1.67  26.9 C-3PO     167    75 <NA>       gold       yellow         112  
 3     0.96  34.7 R2-D2      96    32 <NA>       white, bl… red             33  
 4     2.02  33.3 Darth…    202   136 none       white      yellow          41.9
 5     1.5   21.8 Leia …    150    49 brown      light      brown           19  
 6     1.78  37.9 Owen …    178   120 brown, gr… light      blue            52  
 7     1.65  27.5 Beru …    165    75 brown      light      blue            47  
 8     0.97  34.0 R5-D4      97    32 <NA>       white, red red             NA  
 9     1.83  25.1 Biggs…    183    84 black      light      brown           24  
10     1.82  23.2 Obi-W…    182    77 auburn, w… fair       blue-gray       57  
# ℹ 77 more rows
# ℹ 7 more variables: sex <chr>, gender <chr>, homeworld <chr>, species <chr>,
#   films <list>, vehicles <list>, starships <list>

Modificar columnas: mutate()

datos |> modificar(nueva = funcion())
starwars |> mutate(nueva = funcion())

Importante…

Cuando aplicamos mutate(), debemos de acordarnos que las operaciones se realizan de manera vectorial, elemento a elemento, por lo que la función que usemos dentro debe devolver un vector de igual longitud. En caso contrario, devolverá una constante

starwars |> 
  mutate(constante = mean(mass, na.rm = TRUE), .before = name)
# A tibble: 87 × 15
   constante name  height  mass hair_color skin_color eye_color birth_year sex  
       <dbl> <chr>  <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr>
 1      97.3 Luke…    172    77 blond      fair       blue            19   male 
 2      97.3 C-3PO    167    75 <NA>       gold       yellow         112   none 
 3      97.3 R2-D2     96    32 <NA>       white, bl… red             33   none 
 4      97.3 Dart…    202   136 none       white      yellow          41.9 male 
 5      97.3 Leia…    150    49 brown      light      brown           19   fema…
 6      97.3 Owen…    178   120 brown, gr… light      blue            52   male 
 7      97.3 Beru…    165    75 brown      light      blue            47   fema…
 8      97.3 R5-D4     97    32 <NA>       white, red red             NA   none 
 9      97.3 Bigg…    183    84 black      light      brown           24   male 
10      97.3 Obi-…    182    77 auburn, w… fair       blue-gray       57   male 
# ℹ 77 more rows
# ℹ 6 more variables: gender <chr>, homeworld <chr>, species <chr>,
#   films <list>, vehicles <list>, starships <list>

Recategorizar: if_else()

También podemos combinar mutate() con la expresión de control if_else() para recategorizar la variable: si se cumple una condición, hace una cosa, en caso contrario otra.

starwars |> 
  mutate(human = if_else(species == "Human", "Human", "Not Human"),
         .after = name) |> 
  select(name:mass)
# A tibble: 87 × 4
   name               human     height  mass
   <chr>              <chr>      <int> <dbl>
 1 Luke Skywalker     Human        172    77
 2 C-3PO              Not Human    167    75
 3 R2-D2              Not Human     96    32
 4 Darth Vader        Human        202   136
 5 Leia Organa        Human        150    49
 6 Owen Lars          Human        178   120
 7 Beru Whitesun Lars Human        165    75
 8 R5-D4              Not Human     97    32
 9 Biggs Darklighter  Human        183    84
10 Obi-Wan Kenobi     Human        182    77
# ℹ 77 more rows

Recategorizar: case_when()

Para recategorizaciones más complejas tenemos case_when(), por ejemplo, para crear una categoría de los personajes en función de su altura.

starwars |> 
  drop_na(height) |> 
  mutate(altura = case_when(height < 120 ~ "enanos",
                            height < 160 ~ "bajito",
                            height < 180 ~ "normal",
                            height < 200 ~ "alto",
                            TRUE ~ "gigante"), .before = name)
# A tibble: 81 × 15
   altura  name    height  mass hair_color skin_color eye_color birth_year sex  
   <chr>   <chr>    <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr>
 1 normal  Luke S…    172    77 blond      fair       blue            19   male 
 2 normal  C-3PO      167    75 <NA>       gold       yellow         112   none 
 3 enanos  R2-D2       96    32 <NA>       white, bl… red             33   none 
 4 gigante Darth …    202   136 none       white      yellow          41.9 male 
 5 bajito  Leia O…    150    49 brown      light      brown           19   fema…
 6 normal  Owen L…    178   120 brown, gr… light      blue            52   male 
 7 normal  Beru W…    165    75 brown      light      blue            47   fema…
 8 enanos  R5-D4       97    32 <NA>       white, red red             NA   none 
 9 alto    Biggs …    183    84 black      light      brown           24   male 
10 alto    Obi-Wa…    182    77 auburn, w… fair       blue-gray       57   male 
# ℹ 71 more rows
# ℹ 6 more variables: gender <chr>, homeworld <chr>, species <chr>,
#   films <list>, vehicles <list>, starships <list>

Datos anidados

También podemos anidar o incrustar datasets uno dentro de otro. Imagina que tenemos un dataset de variables x e y con 2 registros, otro con las mismas variables pero solo un registro y otro de 3 registros.

data_1 <- tibble("x" = c(0, 2), "y" = c(-1, NA))
data_2 <- tibble("x" = c(NA), "y" = c(5))
data_3 <- tibble("x" = c(-2, 6, 7), "y" = c(1.5, NA, -2))

Hasta ahora la única manera que sabemos de juntar los 3 datasets es haciendo uso de bind_rows() (por cierto, si usas el argumento .id = "nombre_variable" podemos hacer que se añade sola una nueva variable que nos diga a que dataset pertenecía cada fila

data <- bind_rows(data_1, data_2, data_3, .id = "dataset")
data
# A tibble: 6 × 3
  dataset     x     y
  <chr>   <dbl> <dbl>
1 1           0  -1  
2 1           2  NA  
3 2          NA   5  
4 3          -2   1.5
5 3           6  NA  
6 3           7  -2  

Datos anidados

data <- bind_rows(data_1, data_2, data_3, .id = "dataset")
data
# A tibble: 6 × 3
  dataset     x     y
  <chr>   <dbl> <dbl>
1 1           0  -1  
2 1           2  NA  
3 2          NA   5  
4 3          -2   1.5
5 3           6  NA  
6 3           7  -2  

Sin embargo en muchas ocasiones lo que querremos es tener los 3 en un mismo objeto PERO cada dataset por su cuenta: un objeto (una lista) que almacene los 3 datasets separados entre sí.

Para ello usaremos la función nest() indicando que variables comunes forman los datasets (en este caso x e y)

data_nest <-
  data |>
  nest(data = c(x, y))
data_nest
# A tibble: 3 × 2
  dataset data            
  <chr>   <list>          
1 1       <tibble [2 × 2]>
2 2       <tibble [1 × 2]>
3 3       <tibble [3 × 2]>

Datos anidados

data_nest
# A tibble: 3 × 2
  dataset data            
  <chr>   <list>          
1 1       <tibble [2 × 2]>
2 2       <tibble [1 × 2]>
3 3       <tibble [3 × 2]>

Fíjate que ahora data es una lista ya que cada dataset almacenado podría tener diferentes longitudes

Datos anidados

Para desanidar podemos usar unnest() indicando la columna que contiene los datasets

data_nest |> unnest(cols = c(data))
# A tibble: 6 × 3
  dataset     x     y
  <chr>   <dbl> <dbl>
1 1           0  -1  
2 1           2  NA  
3 2          NA   5  
4 3          -2   1.5
5 3           6  NA  
6 3           7  -2  

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Selecciona solo las variables nombre, altura y así como todas aquellas variables relacionadas con el color, a la vez que te quedas solo con aquellos que no tengan ausente en la altura.

Código
starwars |> 
  select(name, height, contains("color")) |> 
  drop_na(height)

📝 Con los datos obtenidos del ejercicio anterior, traduce el nombre de las columnas a castellano.

Código
starwars |> 
  select(name, height, contains("color")) |> 
  drop_na(height) |> 
  rename(nombre = name, altura = height,
         color_pelo = eye_color, color_piel = skin_color,
         color_pelo = hair_color)

📝 Con los datos obtenidos del ejercicio anterior, coloca la variable de color de pelo justo detrás de la variable de nombres.

Código
starwars |>
  select(name, height, contains("color")) |> 
  drop_na(height) |> 
  rename(nombre = name, altura = height,
         color_pelo = eye_color, color_piel = skin_color,
         color_pelo = hair_color) |> 
  relocate(color_pelo, .after = nombre)

📝 Con los datos originales, comprueba cuántas modalidades únicas hay en la variable de color de pelo.

Código
starwars |> 
  distinct(hair_color) |> 
  nrow()

📝 Del dataset original, selecciona solo las variables numéricas y de tipo texto. Tras ello define una nueva variable llamada under_18 que nos recategorice la variable de edad: TRUE si es menor de edad y FALSE en caso contrario

Código
starwars |> 
  select(where(is.numeric) | where(is.character)) |> 
  mutate(under_18 = birth_year < 18)

📝 Del dataset original, crea una nueva columna llamada auburn (cobrizo/caoba) que nos diga TRUE si el color de pelo contiene dicha palabra y FALSE en caso contrario (reminder str_detect()).

Código
starwars |> 
  mutate(auburn = str_detect(hair_color, "auburn"))

📝 Del dataset original, incluye una columna que calcule el IMC. Tras ello, crea una nueva variable que valga NA si no es humano, delgadez por debajo de 18, normal entre 18 y 30, sobrepeso por encima de 30.

Código
starwars |> 
  mutate(IMC = mass / ((height/100)^2),
         IMC_recat = case_when(species != "Human" ~ NA,
                               IMC < 18 ~ "delgadez",
                               IMC < 30 ~ "normal",
                               TRUE ~ "sobrepeso"),
         .after = name)

🐣 Caso práctico I: simulación

Haciendo uso de todo lo aprendido, vamos a proceder a crear una tabla con datos de bebés de tamaño n = 20 en donde simulemos el sexo de los bebés y su peso

  1. Crea un tibble con dos columnas, una llamada id_bebe y otra llamada sexo. En el primer caso debe ir de 1 a 20. En el segundo caso, simula su sexo de manera que haya un 0.5 de probabilidad de chico y 0.5 de chica.

  2. Conocido el sexo, crea una tercera columna llamada peso en la que simules dicho valor. Supondremos que para los chicos el peso sigue una distribución \(N(\mu = 3.266kg, \sigma = 0.514)\) y que para las chicas sigue una distribución \(N(\mu = 3.155kg, \sigma = 0.495)\).

 

Puedes ver la solución en el workbook

🐣 Caso práctico II: Taylor Swift

Vamos a volver al análisis de Taylor Swift pero esta vez desde una perspectiva tidyverse

library(taylor)
taylor_album_songs
# A tibble: 5 × 29
  album_name   ep    album_release track_number track_name      artist featuring
  <chr>        <lgl> <date>               <int> <chr>           <chr>  <chr>    
1 Taylor Swift FALSE 2006-10-24               1 Tim McGraw      Taylo… <NA>     
2 Taylor Swift FALSE 2006-10-24               2 Picture To Burn Taylo… <NA>     
3 Taylor Swift FALSE 2006-10-24               3 Teardrops On M… Taylo… <NA>     
4 Taylor Swift FALSE 2006-10-24               4 A Place In Thi… Taylo… <NA>     
5 Taylor Swift FALSE 2006-10-24               5 Cold As You     Taylo… <NA>     
# ℹ 22 more variables: bonus_track <lgl>, promotional_release <date>,
#   single_release <date>, track_release <date>, danceability <dbl>,
#   energy <dbl>, key <int>, loudness <dbl>, mode <int>, speechiness <dbl>,
#   acousticness <dbl>, instrumentalness <dbl>, liveness <dbl>, valence <dbl>,
#   tempo <dbl>, time_signature <int>, duration_ms <int>, explicit <lgl>,
#   key_name <chr>, mode_name <chr>, key_mode <chr>, lyrics <list>

Intenta responder a las preguntas planteadas en el workbook

🐣 Caso práctico III: el señor de los anillos

Para practicar algunas funciones de {dplyr} vamos a usar datos de las películas de la trilogía de El Señor de los Anillos. Los datos los cargaremos directamente desde la web (Github en este caso), sin pasar por el ordenador antes, simplemente indicando como ruta la web donde está el archivo

🐣 Caso práctico III: el señor de los anillos

library(readr)
lotr_1 <-
  read_csv(file = "https://raw.githubusercontent.com/jennybc/lotr-tidy/master/data/The_Fellowship_Of_The_Ring.csv")
lotr_2 <-
  read_csv(file = "https://raw.githubusercontent.com/jennybc/lotr-tidy/master/data/The_Two_Towers.csv")
lotr_3 <-
  read_csv(file = "https://raw.githubusercontent.com/jennybc/lotr-tidy/master/data/The_Return_Of_The_King.csv")

Intenta responder a las preguntas planteadas en el workbook

Clase 12: tidyverse (resúmenes)

Summarise y group_by(). Contar y resumir.

Contar: count()

datos |> contar(var1, var2)
starwars |> count(var1, var2)

Hasta ahora solo hemos transformado o consultado los datos pero no hemos generado estadísticas. Empecemos por lo sencillo: ¿cómo contar (frecuencias)?

Cuando lo usamos en solitario count() nos devolverá simplemente el número de registros , pero cuando lo usamos con variables count() calcula lo que se conoce como frecuencias: número de elementos de cada modalidad.

starwars |> count(sex)
# A tibble: 5 × 2
  sex                n
  <chr>          <int>
1 female            16
2 hermaphroditic     1
3 male              60
4 none               6
5 <NA>               4

Contar: count()

datos |> contar(var1, var2)
starwars |> count(var1, var2)

Además si pasamos varias variables nos calcula lo que se conoce como una tabla de contigencia. Con sort = TRUE nos devolverá el conteo ordenado (más frecuentes primero).

starwars |> count(sex, gender, sort = TRUE)
# A tibble: 6 × 3
  sex            gender        n
  <chr>          <chr>     <int>
1 male           masculine    60
2 female         feminine     16
3 none           masculine     5
4 <NA>           <NA>          4
5 hermaphroditic masculine     1
6 none           feminine      1

Agrupar: group_by()

datos |>
  agrupar(var1, var2) |> 
  accion() |> 
  desagrupar()
starwars |>
  group_by(var1, var2) |> 
  accion() |> 
  ungroup()

Una de las funciones más potentes a combinar con las acciones vistas es group_by(), que nos permitirá agrupar nuestros registros previamente

starwars |> 
  group_by(sex) |>
  count() |>
  ungroup()
# A tibble: 5 × 2
  sex                n
  <chr>          <int>
1 female            16
2 hermaphroditic     1
3 male              60
4 none               6
5 <NA>               4

Agrupar: group_by()

datos |>
  agrupar(var1, var2) |> 
  accion() |> 
  desagrupar()
starwars |>
  group_by(var1, var2) |> 
  accion() |> 
  ungroup()

Cuando apliquemos group_by() es importante entender que NO MODIFICA los datos, sino que nos crea una variable de grupo (subtablas por cada grupo) que modificará las acciones futuras: las operaciones se aplicarán a cada subtabla por separado

Por ejemplo, imaginemos que queremos extraer el personaje más alto con slice_max().

starwars |> slice_max(height)
# A tibble: 1 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Yarael P…    264    NA none       white      yellow            NA male  mascu…
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Agrupar: group_by()

datos |>
  agrupar(var1, var2) |> 
  accion() |> 
  desagrupar()
starwars |>
  group_by(var1, var2) |> 
  accion() |> 
  ungroup()

¿Y si queremos extraer el personaje más alto pero…de cada uno de los sexos?

starwars |>
  group_by(sex) |> 
  slice_max(height) |> 
  ungroup()
# A tibble: 5 × 14
  name      height  mass hair_color skin_color eye_color birth_year sex   gender
  <chr>      <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
1 Taun We      213    NA none       grey       black             NA fema… femin…
2 Jabba De…    175  1358 <NA>       green-tan… orange           600 herm… mascu…
3 Yarael P…    264    NA none       white      yellow            NA male  mascu…
4 IG-88        200   140 none       metal      red               15 none  mascu…
5 Gregar T…    185    85 black      dark       brown             NA <NA>  <NA>  
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Agrupar: group_by()

datos |>
  agrupar(var1, var2) |> 
  accion() |> 
  desagrupar()
starwars |>
  group_by(var1, var2) |> 
  accion() |> 
  ungroup()

La web https://tidydatatutor.com/ permite visualizar las operaciones de {tidyverse} (con el pipe antiguo)

Agrupar: group_by()

datos |>
  agrupar(var1, var2) |> 
  accion() |>
  desagrupar()
starwars |>
  group_by(var1, var2) |> 
  accion() |>
  ungroup()

Importante

Recuerda siempre hacer ungroup para eliminar la variable de grupo creada

En la nueva versión de {dplyr} ahora se permite incluir la variable de grupo en la llamada a muchas funciones con el argumento by = ... o .by = ...

starwars |> slice_max(height, by = sex)
# A tibble: 5 × 6
  name                  height  mass hair_color skin_color       eye_color
  <chr>                  <int> <dbl> <chr>      <chr>            <chr>    
1 Yarael Poof              264    NA none       white            yellow   
2 IG-88                    200   140 none       metal            red      
3 Taun We                  213    NA none       grey             black    
4 Jabba Desilijic Tiure    175  1358 <NA>       green-tan, brown orange   
5 Gregar Typho             185    85 black      dark             brown    

Fila-a-fila: rowwise()

Una opción muy útil usada antes de una operación también es rowwise(): toda operación que venga después se aplicará en cada fila por separado. Por ejemplo, vamos a definir un conjunto dummy de notas.

notas <- tibble("mates" = c(7.5, 8, 9.1, 3),
                "lengua" = c(8, 6, 6.5, 9.2))

Si aplicamos la media directamente el valor será idéntico ya que nos ha hecho la media global, pero nos gustaría sacar una media por registro. Para eso usaremos rowwise()

notas |> 
  rowwise() |> 
  mutate(media_curso = mean(c(mates, lengua)))
# A tibble: 4 × 3
# Rowwise: 
  mates lengua media_curso
  <dbl>  <dbl>       <dbl>
1   7.5    8          7.75
2   8      6          7   
3   9.1    6.5        7.8 
4   3      9.2        6.1 

Resumir: summarise()

datos |> resumir()
starwars |> summarise()

Por último tenemos summarise(), que nos permitirá sacar resúmenes estadísticos. Por ejemplo, vamos a calcular la media de las alturas.

starwars |> 
  drop_na(height) |> 
  summarise(media_altura = mean(height))
# A tibble: 1 × 1
  media_altura
         <dbl>
1         175.

Cuidado

Fíjate que mutate() devuelve tantas filas como registros originales, mientras que con summarise() calcula un nuevo dataset de resumen, solo incluyendo aquello que esté indicado.

Resumir: summarise()

datos |> resumir()
starwars |> summarise()

Si además esto lo combinamos con la agrupación de group_by() o .by = ..., en pocas líneas de código puedes obtener estadísticas desagreagadas

starwars |> 
  drop_na(sex, height, mass) |> 
  summarise(media_altura = mean(height),
            media_peso = mean(mass),
            .by = sex)
# A tibble: 4 × 3
  sex            media_altura media_peso
  <chr>                 <dbl>      <dbl>
1 male                   178.       80.2
2 none                   140        69.8
3 female                 172.       54.7
4 hermaphroditic         175      1358  

Resumir: reframe()

datos |> resumir()
starwars |> reframe()

En el nuevo {dplyr} han incluido reframe() para evitar problemas de summarise() cuando devolvemos más de un valor por variable.

starwars |>
  drop_na(mass) |>
  summarise(quantile(mass))
Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
dplyr 1.1.0.
ℹ Please use `reframe()` instead.
ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
  always returns an ungrouped data frame and adjust accordingly.
# A tibble: 5 × 1
  `quantile(mass)`
             <dbl>
1             15  
2             55.6
3             79  
4             84.5
5           1358  
starwars |>
  drop_na(mass) |>
  reframe(quantile(mass))
# A tibble: 5 × 1
  `quantile(mass)`
             <dbl>
1             15  
2             55.6
3             79  
4             84.5
5           1358  

Selectores: across()

Un truco es hacer uso de selectores across() y where(). El primero nos permite actuar sobre varias columnas por nombre (con mutate() o summarise())

starwars |> summarise(medias = across(height:mass, mean, na.rm = TRUE), .by = sex)
# A tibble: 5 × 2
  sex            medias$height  $mass
  <chr>                  <dbl>  <dbl>
1 male                    179.   80.2
2 none                    131.   69.8
3 female                  172.   54.7
4 hermaphroditic          175  1358  
5 <NA>                    175    81  

El segundo, where(), nos permite hacer lo mismo pero seleccionando por tipo.

starwars |> 
  summarise(across(where(is.numeric), mean, na.rm = TRUE), .by = c(sex, gender))
# A tibble: 6 × 5
  sex            gender    height   mass birth_year
  <chr>          <chr>      <dbl>  <dbl>      <dbl>
1 male           masculine   179.   80.2       84.8
2 none           masculine   140    69.8       53.3
3 female         feminine    172.   54.7       47.2
4 hermaphroditic masculine   175  1358        600  
5 <NA>           <NA>        175    81        NaN  
6 none           feminine     96   NaN        NaN  

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Calcula cuántos personajes hay de cada especie, ordenados de más a menor frecuencia.

Código
starwars |> count(species, sort = TRUE)

📝 Tras eliminar ausentes en las variables de peso y estatura, añade una nueva variable que nos calcule el IMC de cada personaje, y determina el IMC medio de nuestros personajes desagregada por sexo

Código
starwars |>
  drop_na(mass, height) |> 
  mutate(IMC = mass / ((height/100)^2)) |> 
  summarise(IMC_medio = mean(IMC), .by = sex)

📝 Obtén el personaje más joven por cada sexo.

Código
starwars |>
  slice_min(birth_year, by = sex)

📝 Obtén la edad del personaje más joven y más viejo de cada sexo.

Código
starwars |>
  drop_na(birth_year) |>
  summarise(min(birth_year), max(birth_year), .by = sex)

📝 Determina la cantidad de personajes en cada década (echa un vistazo a round(), primero sin desagregar y luego desagregado por sexo.

Código
starwars |>
  count(birth_decade = round(birth_year, -1))

🐣 Caso práctico I: billboard

Vamos a hacer un repaso de lo aprendido en {tidyverse} con la tabla billboard del paquete {tidyr}. El dataset representa algo parecido a Los 40 principales (pero versión americana y un top 100 en lugar de 40): para cada artista y canción se guarda la fecha en la que entró en el ranking, y la posición que ocupaba en el ranking en cada una de las semanas (wk1, wk2, …)

billboard
# A tibble: 317 × 8
   artist         track               date.entered   wk1   wk2   wk3   wk4   wk5
   <chr>          <chr>               <date>       <dbl> <dbl> <dbl> <dbl> <dbl>
 1 2 Pac          Baby Don't Cry (Ke… 2000-02-26      87    82    72    77    87
 2 2Ge+her        The Hardest Part O… 2000-09-02      91    87    92    NA    NA
 3 3 Doors Down   Kryptonite          2000-04-08      81    70    68    67    66
 4 3 Doors Down   Loser               2000-10-21      76    76    72    69    67
 5 504 Boyz       Wobble Wobble       2000-04-15      57    34    25    17    17
 6 98^0           Give Me Just One N… 2000-08-19      51    39    34    26    26
 7 A*Teens        Dancing Queen       2000-07-08      97    97    96    95   100
 8 Aaliyah        I Don't Wanna       2000-01-29      84    62    51    41    38
 9 Aaliyah        Try Again           2000-03-18      59    53    38    28    21
10 Adams, Yolanda Open My Heart       2000-08-26      76    76    74    69    68
# ℹ 307 more rows

Intenta responder a las preguntas planteadas en el workbook

🐣 Caso práctico II: fútbol

Vamos a seguir practicando lo aprendido en {tidyverse} con el fichero futbol.csv, donde tenemos datos de los jugadores de las 5 principales ligas de futbol masculinas, desde 2005 hasta 2019, recopilando diferentes estadísticas. Los datos se han extraído directamente haciendo uso del paquete {worldfootballR}, que nos permite extraer datos de https://www.fbref.com

datos <- read_csv(file = "./datos/futbol.csv")
datos
# A tibble: 40,393 × 16
   season team    league  player     country position date_birth minutes_playing
    <dbl> <chr>   <chr>   <chr>      <chr>   <chr>         <dbl>           <dbl>
 1   2005 Ajaccio Ligue 1 Djamel Ab… ALG     MF             1986              30
 2   2005 Ajaccio Ligue 1 Gaspar Az… POR     DF             1975            1224
 3   2005 Ajaccio Ligue 1 Yacine Be… ALG     MF             1981             140
 4   2005 Ajaccio Ligue 1 Nicolas B… FRA     MF             1976             892
 5   2005 Ajaccio Ligue 1 Marcelinh… BRA     MF             1971             704
 6   2005 Ajaccio Ligue 1 Cyril Cha… FRA     FW             1979            1308
 7   2005 Ajaccio Ligue 1 Xavier Co… FRA     DF             1974            2734
 8   2005 Ajaccio Ligue 1 Renaud Co… FRA     MF             1980             896
 9   2005 Ajaccio Ligue 1 Yohan Dem… FRA     DF,MF          1978            2658
10   2005 Ajaccio Ligue 1 Christoph… FRA     DF             1973              74
# ℹ 40,383 more rows
# ℹ 8 more variables: minutes_per_match_playing <dbl>, goals <dbl>,
#   assist <dbl>, goals_minus_pk <dbl>, pk <dbl>, pk_attemp <dbl>,
#   yellow_card <dbl>, red_card <dbl>

🐣 Caso práctico II: fútbol

Las variables capturan la siguiente información:

  • season, team, league: temporada, equipo y liga
  • player, country, position, date_birth: nombre del jugador, país, posición y año de nacimiento.
  • minutes_playing, matches: minutos totales jugados y partidos jugados en media (es decir, cuantos partidos de 90 minutos ha jugado con los minutos jugados).
  • goals, assist: goles y asistencias totales
  • pk, pk_attemp, goals_minus_pk: penalties marcados, penalties tirados y goles marcados sin contar los penalties.
  • yellow_card, red_card: tarjetas amarillas/rojas.

Importante: la variable matches debes crearla tú haciendo uso de minutes_playing (por ejemplo, si ha jugado 315 minutos, cuentan como 3.5 partidos)

🐣 Caso práctico II: fútbol

datos
# A tibble: 40,393 × 16
   season team    league  player     country position date_birth minutes_playing
    <dbl> <chr>   <chr>   <chr>      <chr>   <chr>         <dbl>           <dbl>
 1   2005 Ajaccio Ligue 1 Djamel Ab… ALG     MF             1986              30
 2   2005 Ajaccio Ligue 1 Gaspar Az… POR     DF             1975            1224
 3   2005 Ajaccio Ligue 1 Yacine Be… ALG     MF             1981             140
 4   2005 Ajaccio Ligue 1 Nicolas B… FRA     MF             1976             892
 5   2005 Ajaccio Ligue 1 Marcelinh… BRA     MF             1971             704
 6   2005 Ajaccio Ligue 1 Cyril Cha… FRA     FW             1979            1308
 7   2005 Ajaccio Ligue 1 Xavier Co… FRA     DF             1974            2734
 8   2005 Ajaccio Ligue 1 Renaud Co… FRA     MF             1980             896
 9   2005 Ajaccio Ligue 1 Yohan Dem… FRA     DF,MF          1978            2658
10   2005 Ajaccio Ligue 1 Christoph… FRA     DF             1973              74
# ℹ 40,383 more rows
# ℹ 8 more variables: minutes_per_match_playing <dbl>, goals <dbl>,
#   assist <dbl>, goals_minus_pk <dbl>, pk <dbl>, pk_attemp <dbl>,
#   yellow_card <dbl>, red_card <dbl>

 

Intenta responder a las preguntas planteadas en el workbook

🐣 Caso práctico III: discursos

Volvamos al dataset discursos donde teníamos guardados los discursos navideños de los «jefes» de Estado desde 1946 hasta 2021.

load(file = "./datos/discursos.RData")

Piensa como responder a las mismas preguntas que hicimos en R base pero en modo tidyverse en el workbook

🐣 Caso práctico IV: covid

Vamos a volver al dataset de covid de la entrega II pero esta vez desde una perspectiva tidyverse

library(readr)
datos <- read_csv(file = "./datos/messy_covid_data.csv")
datos
# A tibble: 5 × 31
  provincia_iso fecha               `0-9_H` `10-19_H` `20-29_H` `30-39_H`
  <chr>         <dttm>                <dbl>     <dbl>     <dbl>     <dbl>
1 A             2020-03-01 00:00:00       0         0         0         0
2 AL            2020-03-01 00:00:00       0         0         0         0
3 B             2020-03-01 00:00:00       0         0         0         0
4 BA            2020-03-01 00:00:00       0         1         0         0
5 BI            2020-03-01 00:00:00       0         0         0         0
# ℹ 25 more variables: `40-49_H` <dbl>, `50-59_H` <dbl>, `60-69_H` <dbl>,
#   `70-79_H` <dbl>, `80-Inf_H` <dbl>, `NC-NC_H` <dbl>, `0-9_M` <dbl>,
#   `10-19_M` <dbl>, `20-29_M` <dbl>, `30-39_M` <dbl>, `40-49_M` <dbl>,
#   `50-59_M` <dbl>, `60-69_M` <dbl>, `70-79_M` <dbl>, `80-Inf_M` <dbl>,
#   `NC-NC_M` <dbl>, `0-9_NC` <dbl>, `10-19_NC` <dbl>, `20-29_NC` <dbl>,
#   `30-39_NC` <dbl>, `40-49_NC` <dbl>, `50-59_NC` <dbl>, `60-69_NC` <dbl>,
#   `70-79_NC` <dbl>, `80-Inf_NC` <dbl>

Piensa como responder a las mismas preguntas que hicimos en R base pero en modo tidyverse en el workbook

Clase 13: importar y joins

¿Cómo cruzar dos tablas? ¿Cómo importar y exportar?

Relacionando datos

Al trabajar con datos no siempre tendremos la información en una sola tabla y a veces nos interesará cruzar la información de distintas fuentes.

Para ello usaremos un clásico de todo lenguaje que maneja datos: los famosos join, que nos permitirán cruzar una o varias tablas, haciendo uso de una columna identificadora de cada una de ellas (por ejemplo, imagina que cruzamos datos de hacienda y de antecedentes penales, haciendo join por la columna DNI).

Relacionando datos

La estructura básica es la siguiente:

tabla_1 |>
  xxx_join(tabla_2, by = id)

Vamos a probar los distintos joins con un ejemplo sencillo

tb_1 <- tibble("key" = 1:3, "val_x" = c("x1", "x2", "x3"))
tb_2 <- tibble("key" = c(1, 2, 4), "val_y" = c("y1", "y2", "y3"))
tb_1
# A tibble: 3 × 2
    key val_x
  <int> <chr>
1     1 x1   
2     2 x2   
3     3 x3   
tb_2
# A tibble: 3 × 2
    key val_y
  <dbl> <chr>
1     1 y1   
2     2 y2   
3     4 y3   

Left join

  • left_join(): mantiene todos los registros de la primera tabla, y busca cuales tienen id también en la segunda (en caso de no tenerlo se rellena con NA los campos de la 2ª tabla).

En nuestra caso queremos incorporar a tb_1 la información de tb_2, identificando los registros por la columna key (by = "key", la columna por la que tiene que cruzar)

tb_1  |> 
  left_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 

Left join

tb_1 |> 
  left_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 

Fíjate que los registros de la primera cuya key no ha encontrado en la segunda les ha dado el valor de ausente.

Right join

  • right_join(): mantiene todos los registros de la segunda tabla, y busca cuales tienen id también en la primera.

Vamos ahora a incorporar a tb_2 la información de tb_1, identificando los registros por la columna key (by = "key")

tb_1 |> 
  right_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     4 <NA>  y3   

Right join

tb_1 |> 
  right_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     4 <NA>  y3   

Fíjate que ahora los registros de la segunda cuya key no ha encontrado en la primera son los que les ha dado el valor de ausente.

Claves y sufijos

Las columnas clave que usaremos para el cruce no siempre se llamarán igual.

tb_1 <- tibble("key_1" = 1:3, "val_x" = c("x1", "x2", "x3"))
tb_2 <- tibble("key_2" = c(1, 2, 4), "val_y" = c("y1", "y2", "y3"))
  • by = c("key_2" = "key_2"): le indicaremos en qué columna de cada tabla están las claves por las que vamos a cruzar.
# Left
tb_1 |> 
  left_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 3 × 3
  key_1 val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 
# Right
tb_1  |> 
  right_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 3 × 3
  key_1 val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     4 <NA>  y3   

Claves y sufijos

Además podemos cruzar por varias columnas a la vez (interpretará como igual registro aquel que tenga el conjunto de claves igual), con by = c("var1_t1" = "var1_t2", "var2_t1" = "var2_t2", ...). Modifiquemos el ejemplo anterior

tb_1 <- tibble("k_11" = 1:3, "k_12" = c("a", "b", "c"),  "val_x" = c("x1", "x2", "x3"))
tb_2 <- tibble("k_21" = c(1, 2, 4), "k_22" = c("a", "b", "e"), "val_y" = c("y1", "y2", "y3"))
# Left
tb_1 |> 
  left_join(tb_2,
            by = c("k_11" = "k_21", "k_12" = "k_22"))
# A tibble: 3 × 4
   k_11 k_12  val_x val_y
  <dbl> <chr> <chr> <chr>
1     1 a     x1    y1   
2     2 b     x2    y2   
3     3 c     x3    <NA> 

Claves y sufijos

También podría suceder que al cruzar dos tablas, haya columnas de valores que se llamen igual

tb_1 <- tibble("key_1" = 1:3, "val" = c("x1", "x2", "x3"))
tb_2 <- tibble("key_2" = c(1, 2, 4), "val" = c("y1", "y2", "y3"))
# Left
tb_1 |> 
  left_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 3 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 

Fíjate que por defecto nos añade los sufijos .x y .y para indicarnos de que tabla vienen.

Claves y sufijos

Dicho sufijo podemos especificárselo en el argumento opcional suffix = ..., que nos permita distinguir las variables de una tabla y de otra.

# Left
tb_1 |>
  left_join(tb_2, by = c("key_1" = "key_2"), suffix = c("_tabla1", "_tabla2"))
# A tibble: 3 × 3
  key_1 val_tabla1 val_tabla2
  <dbl> <chr>      <chr>     
1     1 x1         y1        
2     2 x2         y2        
3     3 x3         <NA>      

Full join

  • full_join(): mantiene todos los registros de ambas tablas.

Los dos anteriores casos forman lo que se conoce como outer joins: cruces donde se mantienen observaciones que salgan en al menos una tabla. El tercer outer join es el conocido como full_join() que nos mantendrá las observaciones de ambas tablas, añadiendo las filas que no casen con la otra tabla.

tb_1 |> 
  full_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 4 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 
4     4 <NA>  y3   

Inner join

  • inner_join(): solo sobreviven los registros cuyo id esté en ambas tablas.

Frente a los outer join está lo que se conoce como inner join, con inner_join(): un cruce en el que solo se mantienen las observaciones que salgan en ambas tablas, solo mantiene aquellos registros matcheados.

tb_1 |> 
  inner_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 2 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   

Inner join

Fíjate que en términos de registros, inner_join si es conmutativa, nos da igual el orden de las tablas: lo único que cambia es el orden de las columnas que añade.

tb_1 |> 
  inner_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 2 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
tb_2 |> 
  inner_join(tb_1, by = c("key_2" = "key_1"))
# A tibble: 2 × 3
  key_2 val.x val.y
  <dbl> <chr> <chr>
1     1 y1    x1   
2     2 y2    x2   

Anti/semi join

Por último tenemos dos herramientas interesantes para filtrar (no cruzar) registros: semi_join() y anti_join(). El semi join nos deja en la primera tabla los registros que cuya clave está también en la segunda (como un inner join pero sin añadir la info de la segunda tabla). Y el segundo, los anti join, hace justo lo contrario (aquellos que no están).

# semijoin
tb_1 |> 
  semi_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 2 × 2
  key_1 val  
  <int> <chr>
1     1 x1   
2     2 x2   
# antijoin
tb_1 |> 
  anti_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 1 × 2
  key_1 val  
  <int> <chr>
1     3 x3   

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

Para los ejercicios usaremos las tablas disponibles en el paquete {nycflights13} (echa un vistazo antes)

library(nycflights13)
  • airlines: nombre de aerolíneas (con su abreviatura).
  • airports: datos de aeropuertos (nombres, longitud, latitud, altitud, etc).
  • flights: datos de vuelos.
  • planes: datos de los aviones.
  • weather: datos meteorológicos horarios de las estaciones LGA, JFK y EWR.

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Del paquete {nycflights13} cruza la tabla flights con airlines. Queremos mantener todos los registros de vuelos, añadiendo la información de las aerolíneas a dicha tabla.

Código
flights_airlines <-
  flights |> 
  left_join(airlines, by = "carrier")
flights_airlines

📝 A la tabla obtenida del cruce del apartado anterior, cruza después con los datos de los aviones en planes, pero incluyendo solo aquellos vuelos de los que tengamos información de sus aviones (y viceversa).

Código
flights_airlines_planes <- 
  flights_airlines |> 
  inner_join(planes, by = "tailnum")
flights_airlines_planes

📝 Repite el ejercicio anterior pero conservando ambas variables year (en una es el año del vuelo, en la otra es el año de construcción del avión), y distinguiéndolas entre sí

Código
flights_airlines_planes <- 
  flights_airlines |> 
  inner_join(planes, by = "tailnum",
             suffix = c("_flight", "_build_aircraft"))
flights_airlines_planes

📝 Al cruce obtenido del ejercicio anterior incluye la longitud y latitud de los aeropuertos en airports, distinguiendo entre la latitud/longitud del aeropuerto en destino y en origen.

Código
flights_airlines_planes %>%
  left_join(airports %>% select(faa, lat, lon),
            by = c("origin" = "faa")) |> 
  rename(lat_origin = lat, lon_origin = lon) |> 
  left_join(airports %>% select(faa, lat, lon),
            by = c("dest" = "faa")) |> 
  rename(lat_dest = lat, lon_dest = lon)

📝 Filtra de airports solo aquellos aeropuertos de los que salgan vuelos. Repite el proceso filtrado solo aquellos a los que lleguen vuelos

Código
airports |> 
  semi_join(flights, by = c("faa" = "origin"))
airports |> 
  semi_join(flights, by = c("faa" = "dest"))

📝 ¿De cuántos vuelos no disponemos información del avión? Elimina antes los vuelos que no tengan identificar (diferente a NA) del avión

Código
flights |> 
  drop_na(tailnum) |>
  anti_join(planes, by = "tailnum") |>
  count(tailnum, sort = TRUE) # de mayor a menor ya de paso

🐣 Caso práctico I: Beatles

Vamos a empezar a practicar joins sencillos con la tabla band_members y band_instruments ya incluidos en el paquete {dplyr}.

library(dplyr)
band_members
# A tibble: 3 × 2
  name  band   
  <chr> <chr>  
1 Mick  Stones 
2 John  Beatles
3 Paul  Beatles
band_instruments
# A tibble: 3 × 2
  name  plays 
  <chr> <chr> 
1 John  guitar
2 Paul  bass  
3 Keith guitar

 

Intenta responder a las preguntas planteadas en el workbook

Importar/exportar

Hasta ahora sólo hemos utilizado datos ya cargados en paquetes, pero muchas veces necesitaremos importar datos externamente. Una de las principales fortalezas de R es que podemos importar datos muy fácilmente en diferentes formatos:

  • Formatos nativos de R: archivos .rda, .RData y .rds

  • Rectangular data (datos tabulados): archivos .csv y .tsv

  • Datos sin tabular: archivos .txt.

  • Datos en excel: archivos .xls y.xlsx.

  • Datos desde SAS/Stata/SPSS: archivos .sas7bdat, .sav y .dat.

  • Datos desde Google Drive

  • Datos desde API’s: aemet, catastro, censo, spotify, etc.

Formatos nativos

Los ficheros más sencillos para importar a R (y que suelen ocupar menos espacio en disco) son sus propias extensiones nativas: ficheros en formatos .RData, .rda y .rds. Para cargar los primeros basta con utilizar la función load() proporcionándole la ruta del fichero.

  • Archivos RData: vamos a importar el archivo world_bank_pop.RData que incluye la tabla world_bank_pop
load("./datos/world_bank_pop.RData")
world_bank_pop
# A tibble: 1,064 × 20
   country indicator      `2000`  `2001`  `2002`  `2003`  `2004`  `2005`  `2006`
   <chr>   <chr>           <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
 1 ABW     SP.URB.TOTL    4.16e4 4.20e+4 4.22e+4 4.23e+4 4.23e+4 4.24e+4 4.26e+4
 2 ABW     SP.URB.GROW    1.66e0 9.56e-1 4.01e-1 1.97e-1 9.46e-2 1.94e-1 3.67e-1
 3 ABW     SP.POP.TOTL    8.91e4 9.07e+4 9.18e+4 9.27e+4 9.35e+4 9.45e+4 9.56e+4
 4 ABW     SP.POP.GROW    2.54e0 1.77e+0 1.19e+0 9.97e-1 9.01e-1 1.00e+0 1.18e+0
 5 AFE     SP.URB.TOTL    1.16e8 1.20e+8 1.24e+8 1.29e+8 1.34e+8 1.39e+8 1.44e+8
 6 AFE     SP.URB.GROW    3.60e0 3.66e+0 3.72e+0 3.71e+0 3.74e+0 3.81e+0 3.81e+0
 7 AFE     SP.POP.TOTL    4.02e8 4.12e+8 4.23e+8 4.34e+8 4.45e+8 4.57e+8 4.70e+8
 8 AFE     SP.POP.GROW    2.58e0 2.59e+0 2.61e+0 2.62e+0 2.64e+0 2.67e+0 2.70e+0
 9 AFG     SP.URB.TOTL    4.31e6 4.36e+6 4.67e+6 5.06e+6 5.30e+6 5.54e+6 5.83e+6
10 AFG     SP.URB.GROW    1.86e0 1.15e+0 6.86e+0 7.95e+0 4.59e+0 4.47e+0 5.03e+0
# ℹ 1,054 more rows
# ℹ 11 more variables: `2007` <dbl>, `2008` <dbl>, `2009` <dbl>, `2010` <dbl>,
#   `2011` <dbl>, `2012` <dbl>, `2013` <dbl>, `2014` <dbl>, `2015` <dbl>,
#   `2016` <dbl>, `2017` <dbl>

Formatos nativos

  • Archivos .rda: vamos a importar el dataset airquality desde airquality.rda
load("./datos/airquality.rda")
airquality |> as_tibble()
# A tibble: 153 × 6
   Ozone Solar.R  Wind  Temp Month   Day
   <int>   <int> <dbl> <int> <int> <int>
 1    41     190   7.4    67     5     1
 2    36     118   8      72     5     2
 3    12     149  12.6    74     5     3
 4    18     313  11.5    62     5     4
 5    NA      NA  14.3    56     5     5
 6    28      NA  14.9    66     5     6
 7    23     299   8.6    65     5     7
 8    19      99  13.8    59     5     8
 9     8      19  20.1    61     5     9
10    NA     194   8.6    69     5    10
# ℹ 143 more rows

Formatos nativos

Tenga en cuenta que los archivos cargados con load() se cargan automáticamente en el entorno (con el nombre guardado originalmente), y no sólo se pueden cargar conjuntos de datos: load() nos permite cargar múltiples objetos (no sólo datos tabulares).

Los archivos nativos .rda y .RData son una forma adecuada de guardar el entorno.

load(file = "./datos/multiple_objects.rda")

Formatos nativos

  • Archivos .rds: para este tipo debemos utilizar readRDS(), y necesitamos incorporar un argumento file con la ruta. En este caso vamos a importar datos de cáncer de pulmón del North Central Cancer Treatment Group. Observe que ahora los archivos .rds incorporar solo una tabla, no un objeto en general
lung_cancer <-
  readRDS(file = "./datos/NCCTG_lung_cancer.rds") |>
  as_tibble()
lung_cancer
# A tibble: 228 × 10
    inst  time status   age   sex ph.ecog ph.karno pat.karno meal.cal wt.loss
   <dbl> <dbl>  <dbl> <dbl> <dbl>   <dbl>    <dbl>     <dbl>    <dbl>   <dbl>
 1     3   306      2    74     1       1       90       100     1175      NA
 2     3   455      2    68     1       0       90        90     1225      15
 3     3  1010      1    56     1       0       90        90       NA      15
 4     5   210      2    57     1       1       90        60     1150      11
 5     1   883      2    60     1       0      100        90       NA       0
 6    12  1022      1    74     1       1       50        80      513       0
 7     7   310      2    68     2       2       70        60      384      10
 8    11   361      2    71     2       2       60        80      538       1
 9     1   218      2    53     1       1       70        80      825      16
10     7   166      2    61     1       2       70        70      271      34
# ℹ 218 more rows

Importante

Las [rutas]{.hl-yellow deben ser siempre sin espacios, ñ, ni acentos.

Datos tabulados: readr

El paquete {readr} dentro del entorno {tidyverse} contiene varias funciones útiles para cargar datos rectangulares (sin formatear pero tabulados).

  • read_csv(): archivos .csv variables separadas por comas
  • read_csv2(): variables separadas por punto y coma
  • read_tsv(): variables separadas por tabuladores.
  • read_table(): variables separadas por espacios.
  • read_delim(): función generar con opción de especificar el delimitador.

Todos ellos necesitan como argumento la ruta del fichero más otros opcionales (saltar cabecera o no, decimales, etc). Ver más en https://readr.tidyverse.org/

Datos tabulados (.csv, .tsv)

La principal ventaja de {readr} es que automatiza el formato para pasar de un fichero plano (sin formato) a un tibble (en filas y columnas, con formato).

  • Archivo .csv: con read_csv() cargaremos archivos separados por comas, pasando como argumento la ruta en file = .... Vamos a importar el conjunto de datos chickens.csv (sobre pollos de dibujos animados, por qué no). Si nos fijamos en la salida nos da el tipo de variables.
library(readr)
chickens <- read_csv(file = "./datos/chickens.csv")
chickens
# A tibble: 5 × 4
  chicken                 sex     eggs_laid motto                               
  <chr>                   <chr>       <dbl> <chr>                               
1 Foghorn Leghorn         rooster         0 That's a joke, ah say, that's a jok…
2 Chicken Little          hen             3 The sky is falling!                 
3 Ginger                  hen            12 Listen. We'll either die free chick…
4 Camilla the Chicken     hen             7 Bawk, buck, ba-gawk.                
5 Ernie The Giant Chicken rooster         0 Put Captain Solo in the cargo hold. 

Datos tabulados (.csv, .tsv)

El formato de la variable se hará normalmente automáticamente por read_csv(), y podemos consultarlo con spec().

spec(chickens)
cols(
  chicken = col_character(),
  sex = col_character(),
  eggs_laid = col_double(),
  motto = col_character()
)

Datos tabulados (.csv, .tsv)

Aunque normalmente lo hace bien automáticamente podemos especificar el formato explícitamente en col_types = lista() (en formato lista, con col_xxx() para cada tipo de variable, por ejemplo eggs_laid se importará como carácter).

chickens <-
  read_csv(file = "./datos/chickens.csv",
           col_types = list(col_character(), col_character(),
                            col_character(), col_character()))
chickens
# A tibble: 5 × 4
  chicken                 sex     eggs_laid motto                               
  <chr>                   <chr>   <chr>     <chr>                               
1 Foghorn Leghorn         rooster 0         That's a joke, ah say, that's a jok…
2 Chicken Little          hen     3         The sky is falling!                 
3 Ginger                  hen     12        Listen. We'll either die free chick…
4 Camilla the Chicken     hen     7         Bawk, buck, ba-gawk.                
5 Ernie The Giant Chicken rooster 0         Put Captain Solo in the cargo hold. 

Datos tabulados (.csv, .tsv)

Incluso podemos indicar que variables queremos seleccionar (sin ocupar memoria), indicándolo en col_select = ... (en formato lista, con col_select = ...).

chickens <-
  read_csv(file = "./datos/chickens.csv",
           col_select = c(chicken, sex, eggs_laid))
chickens
# A tibble: 5 × 3
  chicken                 sex     eggs_laid
  <chr>                   <chr>       <dbl>
1 Foghorn Leghorn         rooster         0
2 Chicken Little          hen             3
3 Ginger                  hen            12
4 Camilla the Chicken     hen             7
5 Ernie The Giant Chicken rooster         0

Datos tabulados (.txt)

¿Qué ocurre cuando el separador no es correcto?

Si usamos read_csv() espera que el separador entre columnas sea una coma pero, como puedes ver con el siguiente .txt, lo interpreta todo como una sola columna: no tiene coma y no sabe dónde separar

datos_txt <- read_csv(file = "./datos/massey-rating.txt")
dim(datos_txt)
[1] 10  1
as_tibble(datos_txt)
# A tibble: 10 × 1
   `UCC PAY LAZ KPK  RT   COF BIH DII ENG ACU Rank Team            Conf`
   <chr>                                                                
 1 1   1   1   1   1     1   1   1   1   1    1 Ohio St          B10    
 2 2   2   2   2   2     2   2   2   4   2    2 Oregon           P12    
 3 3   4   3   4   3     4   3   4   2   3    3 Alabama          SEC    
 4 4   3   4   3   4     3   5   3   3   4    4 TCU              B12    
 5 6   6   6   5   5     7   6   5   6  11    5 Michigan St      B10    
 6 7   7   7   6   7     6  11   8   7   8    6 Georgia          SEC    
 7 5   5   5   7   6     8   4   6   5   5    7 Florida St       ACC    
 8 8   8   9   9  10     5   7   7  10   7    8 Baylor           B12    
 9 9  11   8  13  11    11  12   9  14   9    9 Georgia Tech     ACC    
10 13  10  13  11   8     9  10  11   9  10   10 Mississippi      SEC   

Datos tabulados (.txt)

Para ello tenemos.

  • read_csv2() cuando el separador es punto y coma, read_tsv() cuando es un tabulador y read_table() cuando es un espacio.

  • read_delim() en general.

datos_txt <- read_table(file = "./datos/massey-rating.txt")
as_tibble(datos_txt)
# A tibble: 10 × 13
     UCC   PAY   LAZ   KPK    RT   COF   BIH   DII   ENG   ACU  Rank Team  Conf 
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
 1     1     1     1     1     1     1     1     1     1     1     1 Ohio  St   
 2     2     2     2     2     2     2     2     2     4     2     2 Oreg… P12  
 3     3     4     3     4     3     4     3     4     2     3     3 Alab… SEC  
 4     4     3     4     3     4     3     5     3     3     4     4 TCU   B12  
 5     6     6     6     5     5     7     6     5     6    11     5 Mich… St   
 6     7     7     7     6     7     6    11     8     7     8     6 Geor… SEC  
 7     5     5     5     7     6     8     4     6     5     5     7 Flor… St   
 8     8     8     9     9    10     5     7     7    10     7     8 Bayl… B12  
 9     9    11     8    13    11    11    12     9    14     9     9 Geor… Tech 
10    13    10    13    11     8     9    10    11     9    10    10 Miss… SEC  

Datos Excel (.xls, .xlsx)

Otro paquete de importación clave será el paquete {readxl} para importar datos desde Excel. Tres funciones serán clave:

  • read_xls() específica para .xls, read_xlsx() específica para .xlsx.
  • read_excel(): tanto para .xls como para .xlsx.

Vamos a importar deaths.xlsx con los registros de defunciones de famosos.

library(readxl)
deaths <- read_xlsx(path = "./datos/deaths.xlsx")
deaths
# A tibble: 18 × 6
   `Lots of people`             ...2                     ...3  ...4  ...5  ...6 
   <chr>                        <chr>                    <chr> <chr> <chr> <chr>
 1 simply cannot resist writing <NA>                     <NA>  <NA>  <NA>  some…
 2 at                           the                      top   <NA>  of    thei…
 3 or                           merging                  <NA>  <NA>  <NA>  cells
 4 Name                         Profession               Age   Has … Date… Date…
 5 David Bowie                  musician                 69    TRUE  17175 42379
 6 Carrie Fisher                actor                    60    TRUE  20749 42731
 7 Chuck Berry                  musician                 90    TRUE  9788  42812
 8 Bill Paxton                  actor                    61    TRUE  20226 42791
 9 Prince                       musician                 57    TRUE  21343 42481
10 Alan Rickman                 actor                    69    FALSE 16854 42383
11 Florence Henderson           actor                    82    TRUE  12464 42698
12 Harper Lee                   author                   89    FALSE 9615  42419
13 Zsa Zsa Gábor                actor                    99    TRUE  6247  42722
14 George Michael               musician                 53    FALSE 23187 42729
15 Some                         <NA>                     <NA>  <NA>  <NA>  <NA> 
16 <NA>                         also like to write stuff <NA>  <NA>  <NA>  <NA> 
17 <NA>                         <NA>                     at t… bott… <NA>  <NA> 
18 <NA>                         <NA>                     <NA>  <NA>  <NA>  too! 

Datos Excel (.xls, .xlsx)

deaths |> slice(1:6)
# A tibble: 6 × 6
  `Lots of people`             ...2       ...3  ...4     ...5          ...6     
  <chr>                        <chr>      <chr> <chr>    <chr>         <chr>    
1 simply cannot resist writing <NA>       <NA>  <NA>     <NA>          some not…
2 at                           the        top   <NA>     of            their sp…
3 or                           merging    <NA>  <NA>     <NA>          cells    
4 Name                         Profession Age   Has kids Date of birth Date of …
5 David Bowie                  musician   69    TRUE     17175         42379    
6 Carrie Fisher                actor      60    TRUE     20749         42731    

Una desgracia muy común es que haya algún tipo de comentario o texto al principio del fichero, teniendo que saltar esas filas.

Datos Excel (.xls, .xlsx)

Podemos saltar estas filas directamente en la carga con skip = ... (indicando el número de filas a saltar).

deaths <- read_xlsx(path = "./datos/deaths.xlsx", skip = 4)
deaths
# A tibble: 14 × 6
   Name          Profession Age   `Has kids` `Date of birth`     `Date of death`
   <chr>         <chr>      <chr> <chr>      <dttm>              <chr>          
 1 David Bowie   musician   69    TRUE       1947-01-08 00:00:00 42379          
 2 Carrie Fisher actor      60    TRUE       1956-10-21 00:00:00 42731          
 3 Chuck Berry   musician   90    TRUE       1926-10-18 00:00:00 42812          
 4 Bill Paxton   actor      61    TRUE       1955-05-17 00:00:00 42791          
 5 Prince        musician   57    TRUE       1958-06-07 00:00:00 42481          
 6 Alan Rickman  actor      69    FALSE      1946-02-21 00:00:00 42383          
 7 Florence Hen… actor      82    TRUE       1934-02-14 00:00:00 42698          
 8 Harper Lee    author     89    FALSE      1926-04-28 00:00:00 42419          
 9 Zsa Zsa Gábor actor      99    TRUE       1917-02-06 00:00:00 42722          
10 George Micha… musician   53    FALSE      1963-06-25 00:00:00 42729          
11 Some          <NA>       <NA>  <NA>       NA                  <NA>           
12 <NA>          also like… <NA>  <NA>       NA                  <NA>           
13 <NA>          <NA>       at t… bottom,    NA                  <NA>           
14 <NA>          <NA>       <NA>  <NA>       NA                  too!           

Datos Excel (.xls, .xlsx)

Además con col_names = ... ya podemos renombrar las columnas en la importación (proporcionar nombres supone 1ª línea ya como dato)

deaths <-
  read_xlsx(path = "./datos/deaths.xlsx", skip = 5,
            col_names = c("nombre", "profesion", "edad", "hijos", "nacimiento", "muerte"))
deaths
# A tibble: 14 × 6
   nombre             profesion           edad  hijos nacimiento          muerte
   <chr>              <chr>               <chr> <chr> <dttm>              <chr> 
 1 David Bowie        musician            69    TRUE  1947-01-08 00:00:00 42379 
 2 Carrie Fisher      actor               60    TRUE  1956-10-21 00:00:00 42731 
 3 Chuck Berry        musician            90    TRUE  1926-10-18 00:00:00 42812 
 4 Bill Paxton        actor               61    TRUE  1955-05-17 00:00:00 42791 
 5 Prince             musician            57    TRUE  1958-06-07 00:00:00 42481 
 6 Alan Rickman       actor               69    FALSE 1946-02-21 00:00:00 42383 
 7 Florence Henderson actor               82    TRUE  1934-02-14 00:00:00 42698 
 8 Harper Lee         author              89    FALSE 1926-04-28 00:00:00 42419 
 9 Zsa Zsa Gábor      actor               99    TRUE  1917-02-06 00:00:00 42722 
10 George Michael     musician            53    FALSE 1963-06-25 00:00:00 42729 
11 Some               <NA>                <NA>  <NA>  NA                  <NA>  
12 <NA>               also like to write… <NA>  <NA>  NA                  <NA>  
13 <NA>               <NA>                at t… bott… NA                  <NA>  
14 <NA>               <NA>                <NA>  <NA>  NA                  too!  

Datos Excel (.xls, .xlsx)

A veces las fechas de Excel tienen un formato incorrecto (sorpresa): podemos utilizar convertToDate() del paquete {openxlsx} para convertirlo.

library(openxlsx)
deaths$muerte <- convertToDate(deaths$muerte)
deaths
# A tibble: 14 × 6
   nombre             profesion       edad  hijos nacimiento          muerte    
   <chr>              <chr>           <chr> <chr> <dttm>              <date>    
 1 David Bowie        musician        69    TRUE  1947-01-08 00:00:00 2016-01-10
 2 Carrie Fisher      actor           60    TRUE  1956-10-21 00:00:00 2016-12-27
 3 Chuck Berry        musician        90    TRUE  1926-10-18 00:00:00 2017-03-18
 4 Bill Paxton        actor           61    TRUE  1955-05-17 00:00:00 2017-02-25
 5 Prince             musician        57    TRUE  1958-06-07 00:00:00 2016-04-21
 6 Alan Rickman       actor           69    FALSE 1946-02-21 00:00:00 2016-01-14
 7 Florence Henderson actor           82    TRUE  1934-02-14 00:00:00 2016-11-24
 8 Harper Lee         author          89    FALSE 1926-04-28 00:00:00 2016-02-19
 9 Zsa Zsa Gábor      actor           99    TRUE  1917-02-06 00:00:00 2016-12-18
10 George Michael     musician        53    FALSE 1963-06-25 00:00:00 2016-12-25
11 Some               <NA>            <NA>  <NA>  NA                  NA        
12 <NA>               also like to w… <NA>  <NA>  NA                  NA        
13 <NA>               <NA>            at t… bott… NA                  NA        
14 <NA>               <NA>            <NA>  <NA>  NA                  NA        

Datos Excel (.xls, .xlsx)

También podemos cargar un Excel con varias hojas: para indicar la hoja (ya sea por su nombre o por su número) utilizaremos el argumento sheet = ....

mtcars <- read_xlsx(path = "./datos/datasets.xlsx", sheet = "mtcars")
mtcars
# A tibble: 32 × 11
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
# ℹ 22 more rows

Datos Excel (.xls, .xlsx)

Incluso podemos indicar el rango de celdas a cargar con range = ....

iris <- read_xlsx(path = "./datos/datasets.xlsx", sheet = "iris", range = "C1:E4")
iris
# A tibble: 3 × 3
  Petal.Length Petal.Width Species
         <dbl>       <dbl> <chr>  
1          1.4         0.2 setosa 
2          1.4         0.2 setosa 
3          1.3         0.2 setosa 

Importar desde SAS/STATA/SPSS

El paquete {haven} dentro del universo tidyverse nos permitirá importar ficheros de los 3 software de pago más importantes: SAS, SPSS y Stata.

library(haven)

# SAS
iris_sas <- read_sas(data_file = "./datos/iris.sas7bdat")

# SPSS
iris_spss <- read_sav(file = "./datos/iris.sav")

# Stata
iris_stata <- read_dta(file = "./datos/iris.dta")

Exportar

De la misma forma que podemos importar también podemos exportar

  • exportar en .RData (opción recomendada para variables almacenadas en R). Recuerda que esta extensión sólo se puede utilizar en R. Para ello, basta con utilizar save(object, file = path).
table <- tibble("a" = 1:4, "b" = 1:4)
save(table, file = "./datos/table.RData")
rm(table) # eliminar
load("./datos/table.RData")
table
# A tibble: 4 × 2
      a     b
  <int> <int>
1     1     1
2     2     2
3     3     3
4     4     4

Exportar

La ventaja de .RData es que podemos exportar múltiples variables de nuestro environment, no solo un único dataset.

table <- tibble("a" = 1:4, "b" = 1:4)
a <- 1
b <- c("javi", "sandra")
save(table, a, b, file = "./datos/mult_obj.RData")
rm(list = c("a", "b", "table"))
load("./datos/mult_obj.RData")
table
# A tibble: 4 × 2
      a     b
  <int> <int>
1     1     1
2     2     2
3     3     3
4     4     4

Exportar

  • exportado en .csv. Para ello simplemente utilizamos write_csv(object, file = path), y es el más recomendable para exportar bases de datos de tamaño pequeño o mediano. Ver https://arrow.apache.org/docs/r/ para bases de datos masivas.
write_csv(table, file = "./datos/table.csv")
read_csv(file = "./datos/table.csv")
# A tibble: 4 × 2
      a     b
  <dbl> <dbl>
1     1     1
2     2     2
3     3     3
4     4     4

Importar desde web

Una de las principales ventajas de R es que podemos hacer uso de todas las funciones anteriores de importar pero directamente desde una web, sin necesidad de realizar la descarga manual: en lugar de pasarle la ruta local le indicaremos el enlace. Por ejemplo, vamos a descargar los datos covid del ISCIII (https://cnecovid.isciii.es/covid19/#documentaci%C3%B3n-y-datos)

covid_data <-
  read_csv(file = "https://cnecovid.isciii.es/covid19/resources/casos_hosp_uci_def_sexo_edad_provres.csv")
covid_data
# A tibble: 500 × 8
   provincia_iso sexo  grupo_edad fecha      num_casos num_hosp num_uci num_def
   <chr>         <chr> <chr>      <date>         <dbl>    <dbl>   <dbl>   <dbl>
 1 A             H     0-9        2020-01-01         0        0       0       0
 2 A             H     10-19      2020-01-01         0        0       0       0
 3 A             H     20-29      2020-01-01         0        0       0       0
 4 A             H     30-39      2020-01-01         0        0       0       0
 5 A             H     40-49      2020-01-01         0        0       0       0
 6 A             H     50-59      2020-01-01         0        0       0       0
 7 A             H     60-69      2020-01-01         0        0       0       0
 8 A             H     70-79      2020-01-01         0        0       0       0
 9 A             H     80+        2020-01-01         0        0       0       0
10 A             H     NC         2020-01-01         0        0       0       0
# ℹ 490 more rows

Importar desde wikipedia

El paquete {rvest}, uno de los más útiles de {tidyverse} nos permite importar (scrappear) directamente desde un html. Por ejemplo, para exportar tablas de wikipedia basta con read_html() para importar el html, html_element(«table») para extraer los objetos tabla, y html_table() para convertir la tabla html a tibble.

library(rvest)
wiki_jump <- 'https://en.wikipedia.org/wiki/Men%27s_long_jump_world_record_progression'
wiki_jump |> read_html() |> 
  html_element("table") |> 
  html_table()
# A tibble: 19 × 5
   Mark                       Wind   Athlete                  Place        Date 
   <chr>                      <chr>  <chr>                    <chr>        <chr>
 1 7.61 m (24 ft 11+1⁄2 in)   ""     Peter O'Connor (IRE)     Dublin, Ire… 5 Au…
 2 7.69 m (25 ft 2+3⁄4 in)    ""     Edward Gourdin (USA)     Cambridge, … 23 J…
 3 7.76 m (25 ft 5+1⁄2 in)    ""     Robert LeGendre (USA)    Paris, Fran… 7 Ju…
 4 7.89 m (25 ft 10+1⁄2 in)   ""     DeHart Hubbard (USA)     Chicago, Un… 13 J…
 5 7.90 m (25 ft 11 in)       ""     Edward Hamm (USA)        Cambridge, … 7 Ju…
 6 7.93 m (26 ft 0 in)        "0.0"  Sylvio Cator (HAI)       Paris, Fran… 9 Se…
 7 7.98 m (26 ft 2 in)        "0.5"  Chuhei Nambu (JPN)       Tokyo, Japan 27 O…
 8 8.13 m (26 ft 8 in)        "1.5"  Jesse Owens (USA)        Ann Arbor, … 25 M…
 9 8.21 m (26 ft 11 in)       "0.0"  Ralph Boston (USA)       Walnut, Uni… 12 A…
10 8.24 m (27 ft 1⁄4 in)      "1.8"  Ralph Boston (USA)       Modesto, Un… 27 M…
11 8.28 m (27 ft 1+3⁄4 in)    "1.2"  Ralph Boston (USA)       Moscow, Sov… 16 J…
12 8.31 m (27 ft 3 in) A      "−0.1" Igor Ter-Ovanesyan (URS) Yerevan, So… 10 J…
13 8.33 m (27 ft 3+3⁄4 in)[2] ""     Phil Shinnick (USA)      Modesto, Un… 25 M…
14 8.31 m (27 ft 3 in)        "0.0"  Ralph Boston (USA)       Kingston, J… 15 A…
15 8.34 m (27 ft 4+1⁄4 in)    "1.0"  Ralph Boston (USA)       Los Angeles… 12 S…
16 8.35 m (27 ft 4+1⁄2 in)[5] "0.0"  Ralph Boston (USA)       Modesto, Un… 29 M…
17 8.35 m (27 ft 4+1⁄2 in) A  "0.0"  Igor Ter-Ovanesyan (URS) Mexico City… 19 O…
18 8.90 m (29 ft 2+1⁄4 in) A  "2.0"  Bob Beamon (USA)         Mexico City… 18 O…
19 8.95 m (29 ft 4+1⁄4 in)    "0.3"  Mike Powell (USA)        Tokyo, Japan 30 A…

Importar desde google drive

Otra opción disponible (especialmente si trabajamos con otras personas trabajando) es importar desde una hoja de cálculo de Google Drive, haciendo uso de read_sheet() del paquete {googlesheets4}.

La primera vez se te pedirá un permiso tidyverse para interactuar con tu drive

library(googlesheets4)
google_sheet <-
  read_sheet("https://docs.google.com/spreadsheets/d/1Uz38nHjl3bmftxDpcXj--DYyPo1I39NHVf-xjeg1_wI/edit?usp=sharing")
google_sheet
# A tibble: 3 × 4
  a         b     c d     
  <chr> <dbl> <dbl> <chr> 
1 d1      0.5     1 hombre
2 d2     -0.3     2 hombre
3 d3      1.1     3 mujer 

Importar desde API (owid)

Otra opción interesante es la descarga de datos desde una API: un intermediario entre una app o proveedor de datos y nuestro R. Por ejemplo, carguemos la librería {owidR}, que nos permite descargar datos de la web https://ourworldindata.org/. Por ejemplo, la función owid_covid() carga sin darnos cuenta más de 400 000 registros con más de 60 variables de 238 países.

library(owidR)
owid_covid() |> as_tibble()
# A tibble: 7 × 67
  iso_code continent location    date       total_cases new_cases
  <chr>    <chr>     <chr>       <IDate>          <int>     <int>
1 AFG      Asia      Afghanistan 2020-01-05           0         0
2 AFG      Asia      Afghanistan 2020-01-06           0         0
3 AFG      Asia      Afghanistan 2020-01-07           0         0
4 AFG      Asia      Afghanistan 2020-01-08           0         0
5 AFG      Asia      Afghanistan 2020-01-09           0         0
6 AFG      Asia      Afghanistan 2020-01-10           0         0
7 AFG      Asia      Afghanistan 2020-01-11           0         0
# ℹ 61 more variables: new_cases_smoothed <dbl>, total_deaths <int>,
#   new_deaths <int>, new_deaths_smoothed <dbl>, total_cases_per_million <dbl>,
#   new_cases_per_million <dbl>, new_cases_smoothed_per_million <dbl>,
#   total_deaths_per_million <dbl>, new_deaths_per_million <dbl>,
#   new_deaths_smoothed_per_million <dbl>, reproduction_rate <dbl>,
#   icu_patients <int>, icu_patients_per_million <dbl>, hosp_patients <int>,
#   hosp_patients_per_million <dbl>, weekly_icu_admissions <int>, …

Importar desde API (aemet)

En muchas ocasiones para conectarnos a la API primero tendremos que registrarnos y obtener una clave, este es el caso del paquete {climaemet} para acceder a datos meteorológicos de España (https://opendata.aemet.es/centrodedescargas/inicio).

Una vez tengamos la clave API la registramos en nuestro RStudio para poder utilizarla en el futuro.

library(climaemet)

# Api key
apikey <- "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYXZhbHYwOUB1Y20uZXMiLCJqdGkiOiI4YTU1ODUxMS01MTE3LTQ4MTYtYmM4OS1hYmVkNDhiODBkYzkiLCJpc3MiOiJBRU1FVCIsImlhdCI6MTY2NjQ2OTcxNSwidXNlcklkIjoiOGE1NTg1MTEtNTExNy00ODE2LWJjODktYWJlZDQ4YjgwZGM5Iiwicm9sZSI6IiJ9.HEMR77lZy2ASjmOxJa8ppx2J8Za1IViurMX3p1reVBU"

aemet_api_key(apikey, install = TRUE)

Importar desde API (aemet)

Con este paquete podemos hacer una búsqueda de estaciones para conocer tanto su código postal como su código identificador dentro de la red AEMET

stations <- aemet_stations()
stations

Importar desde API (aemet)

Por ejemplo, para obtener datos de la estación del aeropuerto de El Prat, Barcelona, el código a proporcionar es «0076», obteniendo datos horarios.

aemet_last_obs("0076")

Importar desde API (US census)

Una de las herramientas más útiles de los últimos años es la conocida como {tidycensus}: una herramienta para facilitar el proceso de descarga de datos censales de Estados Unidos desde R

library(tidycensus)
  • get_decennial(): para acceder a los datos de censos (US Decennial Census), se hacen cada 10 años (años 2000, 2010 y 2020).

  • get_acs(): para acceder a los datos anuales y quinquenales (5 años) de la ACS (American Community Survey) (censo != encuesta)

  • get_estimates(): para acceder a las estimaciones anuales de población, natalidad y mortalidad

  • get_pums(): para acceder a los microdatos (datos sin agregar) de la ACS (anonimizados a nivel individual)

  • get_flows(): para acceder a los datos de flujo de migraciones

Importar desde API (US census)

Por ejemplo vamos a descargarnos los datos censales (get_decennial()) a nivel estado (geography = "state") de la población (variable variables = "P001001") para el año 2010 (ver variables en tidycensus::load_variables())

total_population_10 <-
  get_decennial(geography = "state", 
  variables = "P001001",
  year = 2010)
total_population_10
# A tibble: 52 × 4
   GEOID NAME        variable    value
   <chr> <chr>       <chr>       <dbl>
 1 01    Alabama     P001001   4779736
 2 02    Alaska      P001001    710231
 3 04    Arizona     P001001   6392017
 4 05    Arkansas    P001001   2915918
 5 06    California  P001001  37253956
 6 22    Louisiana   P001001   4533372
 7 21    Kentucky    P001001   4339367
 8 08    Colorado    P001001   5029196
 9 09    Connecticut P001001   3574097
10 10    Delaware    P001001    897934
# ℹ 42 more rows

Importar desde API

Otras opciones

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 El conjunto de datos who2 del paquete {tidyr} que hemos utilizado en ejercicios anteriores, expórtalo a un formato nativo R en la carpeta datos de tu proyecto de RStudio

Código
library(tidyr)
save(who2, file = "./datos/who2.RData")

📝 Carga el conjunto de datos who2 pero desde la carpeta data (importa el fichero creado en el ejercicio anterior)

Código
load("./datos/who2.RData")

📝 Repite lo mismo (exportar e importar) en 4 formatos: .csv, .xlsx, .sav (spss) y .dta (stata)

Código
# csv
library(readr)
write_csv(who2, file = "./datos/who2.csv")
who2_data <- read_csv(file = "./datos/who2.csv")

# excel
library(openxlsx)
write.xlsx(who2, file = "./datos/who2.xlsx")
who2_data <- read_xlsx(path = "./datos/who2.xlsx")

# sas y stata
library(haven)
write_sav(who2, path = "./datos/who2.sav")
who2_data <- read_spss(path = "./datos/who2.sav")

write_dta(who2, path = "./datos/who2.dta")
who2_data <- read_dta(path = "./datos/who2.dta")

📝 Repita la carga de who2.csv pero seleccione sólo las 4 primeras columnas ya cargadas.

Código
who_select <-
  read_csv(file = "./datos/who2.csv",
           col_select = c("country", "iso2", "iso3", "year"))
who_select

🐣 Caso práctico II: encuesta CIS

📊 Datos

Vamos a poner en práctica la carga y preprocesado de un fichero generado por uno de los programas informáticos más utilizados (SPSS). El fichero contiene datos del barómetro del CIS (Centro de Investigaciones Sociológicas) «Percepciones sobre igualdad entre hombres y mujeres y estereotipos de género» cuyo trabajo muestral se realizó del 6 al 14 de noviembre (4000 entrevistas a mayores de 16 años de ambos sexos en 1174 municipios y 50 provincias).

 

Intenta responder a las preguntas planteadas en el workbook

Clase 14-15-16: concurso

Concurso: R base vs tidyverse

Pregunta 1

Debéis crear TODOS los miembros del equipo un proyecto de R Studio que contenga al menos:

library(tidyverse)
datos <- read_csv(file = "datos/datos_elecciones_brutos.csv")
datos
# A tibble: 48,737 × 137
   tipo_eleccion  anno mes   vuelta codigo_ccaa codigo_provincia
   <chr>         <dbl> <chr>  <dbl> <chr>       <chr>           
 1 02             2008 03         1 14          01              
 2 02             2008 03         1 14          01              
 3 02             2008 03         1 14          01              
 4 02             2008 03         1 14          01              
 5 02             2008 03         1 14          01              
 6 02             2008 03         1 14          01              
 7 02             2008 03         1 14          01              
 8 02             2008 03         1 14          01              
 9 02             2008 03         1 14          01              
10 02             2008 03         1 14          01              
# ℹ 48,727 more rows
# ℹ 131 more variables: codigo_municipio <chr>,
#   codigo_distrito_electoral <dbl>, numero_mesas <dbl>, censo <dbl>,
#   participacion_1 <dbl>, participacion_2 <dbl>, votos_blancos <dbl>,
#   votos_nulos <dbl>, `PARTIDO POPULAR` <dbl>,
#   `PARTIT POPULAR/PARTIDO POPULAR` <dbl>,
#   `UNION DEL PUEBLO NAVARRO EN COALICION CON EL PARTIDO POPULAR` <dbl>, …

Datos

Las variables son las siguientes:

  • tipo_eleccion: tipo de elección (02 si es elección al congreso)
  • anno, mes: año y mes de las elecciones
  • vuelta: vuelta electoral (1 si es primera vuelta)
  • codigo_ccaa, codigo_provincia, codigo_municipio, codigo_distrito_electoral: código de la ccaa, provincia, municipio y distrito electoral.
  • numero_mesas: número de mesas electorales

Datos

Las variables son las siguientes:

  • censo: censo electoral

  • participacion_1, participacion_2: participación en el primer avance (14:00) y segundo avance (18:00) antes del cierre de urnas (20:00)

  • votos_blancos: votos en blanco

  • votos_candidaturas: votos a papeletas de partidos

  • votos_nulos: votos nulos

  • variables de voto de cada partido (se ha adaptado el archivo depurándolo un poco)

Pregunta 2

Convierte a tidy data de la manera más apropiada y completa posible (organización, tipo de dato de cada variable debe ser el adecuado… ausentes, etc).

Código
datos_tidy <-
  datos |> 
  pivot_longer(cols = -c(tipo_eleccion:votos_nulos),
               names_to = "partidos", values_to = "votos",
               values_drop_na = TRUE) |> 
  # tenemos algunas lógicas que deberían ser números
  mutate(across(where(is.logical), as.numeric))

Pregunta 3

En estadística información = varianza: si no hay de lo segundo no hay de lo primero. ¿Qué variables deberíamos eliminar y por qué? Hazlo de la manera más eficiente posible.

Código
# vemos primero cantidad de valores únicos en cada una
n_dist <-
  datos_tidy |> 
  summarise(across(everything(), n_distinct))

# en tres (tipo_eleccion, vuelta, codigo_distrito_electoral) tenemos 
# un solo valor cte --> no nos aporta nada

datos_tidy <-
  datos_tidy |> 
  select(-c(tipo_eleccion, vuelta, codigo_distrito_electoral))

Pregunta 4

La variable fecha debería existir y no existe: solo tenemos año y mes de las elecciones (que en realidad no nos aportan una vez tuviésemos la fecha de las elecciones). Considera que todas las elecciones fueron el 1 de cada mes y crea dicha variable de manera adecuada

Código
# en tres (tipo_eleccion, vuelta, codigo_distrito_electoral) tenemos 
# un solo valor cte --> no nos aporta nada

datos_tidy <-
  datos_tidy |> 
  mutate("fecha" = as_date(glue("{anno}-{mes}-01")), .before = everything()) |> 
  select(-anno, -mes)

Pregunta 5

Crea los id que consideres para poder eliminar duplicados por municipio, fecha electoral y partido (un registro será considerado igual si es coinciden ambos campos). Deberás crear obligatoriamente al menos una variable nueva. Tras ello elimina esos duplicados.

Código
datos_tidy <-
  datos_tidy |> 
  mutate("id_mun" = glue("{codigo_ccaa}-{codigo_provincia}-{codigo_municipio}"),
         "id_elec" = glue("{fecha}-{id_mun}"), .after = fecha) |> 
  mutate("id_result" = glue("{id_elec}-{partidos}"), .before = everything()) |> 
  distinct(id_result, .keep_all = TRUE)

Pregunta 6

Tenemos muchísimos partidos que se presentan a las elecciones. Solo nos intereserán los partidos agrupados como (cuidado: tienen/tuvieron federaciones - sucursales - con algún otro nombre):

-   PSOE 
-   PARTIDO POPULAR
-   CIUDADANOS
-   PARTIDO NACIONALISTA VASCO
-   BLOQUE NACIONALISTA GALEGO
-   PODEMOS/UNIDAS PODEMOS/IU (no siempre han ido juntos, pero aquí los analizaremos juntos)
-   ESQUERRA REPUBLICANA DE CATALUNYA
-   BILDU
-   MÁS PAÍS
-   VOX

Recodifica de manera adecuada creando siglas. Todo lo que no sea alguno de los anteriores partidos deberá ser codificado como “OTROS”. Cuidado: si UNIDAS PODEMOS y PODEMOS son lo mismo, los votos de cada uno deberían estar sumados juntos bajo las siglas de UP (por ejemplo).

Pregunta 6

Código
datos_tidy <-
  datos_tidy |> 
  mutate("siglas" = 
           case_when(str_detect(partidos, "SOCIALISTA|SOCIALISTES") ~ "PSOE",
                     str_detect(partidos, "PARTIDO POPULAR") ~ "PP",
                     str_detect(partidos, "CIUDADANOS|CIUDADANÍA|CIUDADANIA|CIUTADANS") ~ "CS",
                     str_detect(partidos, "PARTIDO NACIONALISTA VASCO") ~ "PNV",
                     str_detect(partidos, "BLOQUE NACIONALISTA GALEGO") ~ "BNG",
                     str_detect(partidos, "PODEMOS|IZQUIERDA UNIDA") ~ "UP",
                     str_detect(partidos, "ESQUERRA REPUBLICANA DE CAT") ~ "ERC",
                     str_detect(partidos, "BILDU") ~ "BILDU",
                     str_detect(partidos, "MÁS PAÍS|MAS PAIS|MÁS PAIS|MAS PAÍS") ~ "MP",
                     str_detect(partidos, "VOX") ~ "VOX",
                     TRUE ~ "OTROS"))

datos_tidy <-
  datos_tidy |> 
  # hago mutate porqueno quiero perder el resto de info
  mutate("votos" = sum(votos, na.rm = TRUE), .by = c(id_elec, siglas)) |> 
  # pero entonces ahora si PACMA + PCE suman 200 votos, tendré
  # en ese municpio y fecha (id_elec) dos filas para OTROS con el mismo valor (200)
  distinct(id_elec, siglas, .keep_all = TRUE)

Pregunta 7

Incorpora como variable nueva el porcentaje de votos de cada partido (en cada cita electoral - fecha - y en cada municipio). El porcentaje se calcula como votos del partido entre votos válidos (y votos válidos = votos a partidos + blancos)

Código
votos_partidos <-
  datos_tidy |> 
  summarise("votos_partidos" = sum(votos, na.rm = TRUE), .by = id_elec)

# juntamos esa info a la tabla grande
datos_tidy <-
  datos_tidy |> 
  left_join(votos_partidos, by = "id_elec") |> 
  mutate("porc_voto" = 100*votos / (votos_partidos + votos_blancos))

Pregunta 8

Incorpora como variable el porcentaje de participación (en cada cita electoral y en cada municipio), calculado como votos emitidos (votos a partidos + blancos + nulos) entre censo.

Código
datos_tidy <-
  datos_tidy |> 
  mutate("participacion" = 100*(votos_partidos + votos_blancos + votos_nulos) / censo)

Clase 17-18: listas

Manejo de listas con paquete purrr. Ejemplos casos rales

Listas

Ya hemos visto que las listas son un objeto en R que nos permite almacenar colecciones de variables de distinto tipo (como con data.frame y tibble) pero también diferentes longitudes, con estructuras totalmente heterogéneas (incluso una lista puede tener dentro otra lista).

nombre <- "Javi"
edad <- 34
notas <- c(7, 8, 5, 3, 10, 9)
progenitores <- c("Paloma", "Goyo")

# Podemos no ponerle nombre pero solo podremos acceder por índice
list_var <- list("nombre" = nombre, "edad" = edad,
                 "notas" = notas, "parents" = progenitores)
list_var$nombre
[1] "Javi"
list_var$progenitores
NULL

Listas

También podemos hacer listas con otras listas dentro, de forma que para acceder a cada nivel debamos utilizar el operador [[]].

list_of_lists <- list("list_1" = list_var[1:2], "list_2" = list_var[3:4])
names(list_of_lists)
[1] "list_1" "list_2"
names(list_of_lists[[1]])
[1] "nombre" "edad"  
list_of_lists[[1]][[1]]
[1] "Javi"

¡Se nos permite almacenar datos n-dimensionales!

Listas

Sin embargo una de las desventajas es no puede ser vectorizada inmediatamente, por lo que cualquier operación aritmética aplicada a una lista dará error. Por ejemplo, en el código anterior, uno esperaría que hiciese la operación en cada elemento de la lista (en a y en b) pero no funciona.

data <- list("a" = 1:5, "b" = 10:20)
data / 2
Error in data/2: non-numeric argument to binary operator

Para ello una de las opciones habituales (de R base) era hacer uso de la familia lapply(lista, FUN = ...): aplica una función FUN = ... a cada elemento de la lista

lapply(data, FUN = function(x) { x / 2})
$a
[1] 0.5 1.0 1.5 2.0 2.5

$b
 [1]  5.0  5.5  6.0  6.5  7.0  7.5  8.0  8.5  9.0  9.5 10.0

Pero por defecto la salida de lapply() es siempre una lista de igual longitud.

Paquete purrr

Una opción más flexible y versátil es hacer uso del paquete {purrr} del entorno {tidyverse}.

library(purrr)

Este paquete pretende imitar la programación funcional de otros lenguajes como Scala o la estrategia map-reduce de Hadoop (de Google).

Paquete purrr

La función más sencilla del paquete {purrr} es la función map(), que aplica una función vectorizada a cada uno de los elementos de una lista. Veamos un primer ejemplo aplicado a vectores: imagina que tenemos la siguiente lista y queremos aplicar, a cada uno de sus elementos, la suma.

x <- list("x1" = 1:4, "x2" = 11:20)

map(lista, función) nos permite mapear la lista y aplicar la función que queramos elemento a elemento

map(x, sum) 
$x1
[1] 10

$x2
[1] 155

Ten cuidado…

La salida por defecto de map es a su vez otra lista.

Paquete purrr

Veamos otro ejemplo que puedes sernos útil: imagina que tenemos dos muestras aleatorias de distinto tamaño. La única forma en la que podemos almacenarlas juntas es con una lista pero…¿cómo hacer la media a ambas distribuciones sino podemos vectorizarlas?

x <- list(rnorm(n = 1500, mean = 0, sd = 0.7),
          rnorm(n = 2800, mean = 2, sd = 1.5))

Con map() y la función mean() dentro

map(x, mean)
[[1]]
[1] 0.01982399

[[2]]
[1] 2.034912

Paquete purrr

¿Y si quisiéramos calcular la media de sus valores al cuadrado? En este caso no disponemos de una función ya definida así que tenemos dos opciones: definirla antes (con un nombre) o bien definir la función dentro del propio map

map(x, function(x) { mean(x^2) })
[[1]]
[1] 0.4951544

[[2]]
[1] 6.306881

Una forma de definir la función más rápido es así:

map(x, \(x) mean(x^2))
[[1]]
[1] 0.4951544

[[2]]
[1] 6.306881

Paquete purrr

Además de ser más legible y eficiente, con {purrr} podemos decidir el formato de salida tras la operación

  • salida como un vector numérico (decimales) con map_dbl()
  • salida como un vector numérico (enteros) con map_int()
  • salida como un vector de caracteres con map_chr()
  • salida como un vector numérico lógico con map_lgl()
map_dbl(x, mean)
[1] 0.01982399 2.03491201
map_chr(x, function(x) { glue("Mean is {round(mean(x), 3)}") })
[1] "Mean is 0.02"  "Mean is 2.035"
map_lgl(x, function(x) { all(x < 3)})
[1]  TRUE FALSE

Paquete purrr

También puede sernos útil para acceder a un elemento de CADA lista si le pasas un número en lugar de una función.

c(x[[1]][3], x[[2]][3])
[1] 0.1999443 3.5309637
map_dbl(x, 3)
[1] 0.1999443 3.5309637

Paquete purrr

También podemos hacer uso de pluck() para acceder al i-th objeto de la lista.

lista <- list("a" = starwars, "b" = billboard)
lista |> pluck(1)
# A tibble: 87 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

Paquete purrr

También tenemos la opción de generalizarlo para poder utilizar funciones que necesiten dos argumentos (operaciones binarias) con map2(). Por ejemplo, supongamos que tenemos las siguientes dos listas, y queremos que suma el vector del primer elemento de ambas entre sí, y lo mismo con el segundo

x <- list("a" = 1:3, "b" = 4:6)
y <- list("a" = c(-1, 4, 0), "b" = c(5, -4, -1))
x$a + y$a
[1] 0 6 3
x$b + x$b
[1]  8 10 12

En map2() introducimos las 2 listas y una función con 2 argumentos

map2(x, y, function(x, y) { x + y})
$a
[1] 0 6 3

$b
[1] 9 1 5

Paquete purrr

Podemos también obtener la salida en forma de data.frame añadiendo list_rbind() o list_cbind(), que convierte una lista en una tabla.

x <- c("a", "b")
y <- 1:2
map2(x, y, function(x, y) { tibble(x, y) })
[[1]]
# A tibble: 1 × 2
  x         y
  <chr> <int>
1 a         1

[[2]]
# A tibble: 1 × 2
  x         y
  <chr> <int>
1 b         2
map2(x, y, function(x, y) { tibble(x, y) }) |> list_rbind()
# A tibble: 2 × 2
  x         y
  <chr> <int>
1 a         1
2 b         2

Paquete purrr

Podemos generalizarlo aún más con pmap_xxx() que nos permite utilizar múltiples argumentos (múltiples listas para funciónes multivariantes).

x <- list(1, 1, 1)
y <- list(10, 20, 30)
z <- list(100, 200, 300)
pmap_dbl(list(x, y, z), sum)
[1] 111 221 331

Paquete purrr

Tenemos otros tipos de iteradores que, aunque asumen entradas, no devuelven nada, como walk() (sólo un argumento de entrada), walk2() (dos argumentos) y pwalk() (múltiples argumentos), todos devuelven algo invisible, sólo llaman a una función por sus efectos secundarios en lugar de por su valor de retorno.

list("a" = 1:3, "b" = 4:6) |>
  map2(list("a" = 11:13, "b" = 14:16),
       function(x, y) { x + y }) |> 
  walk(print)
[1] 12 14 16
[1] 18 20 22

💻 Tu turno

📝 Define una lista de 4 elementos de distintos tipos y accede al segundo de ellos (incluiré uno que sea un tibble para que veas que en una lista cabe de todo).

Código
list_example <-
  list("name" = "Javier", "cp" = 28019,
       "siblings" = TRUE,
       "marks" = tibble("maths" = c(7.5, 8, 9),
                        "lang" = c(10, 5, 6)))
list_example

📝 From the list above, access the elements that occupy places 1 and 4 of the list defined above.

Código
list_example[c(1, 4)]

list_example$name
list_example$marks

list_example[c("name", "marks")]

📝 Load the starwars dataset from the {dplyr} package and access the second movie that appears in starwars$films (for each character). Determine which ones do not appear in more than one movie.

second_film <- map(starwars$films, 2)
map_lgl(second_film, is.null)
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE
[13] FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE  TRUE FALSE
[25]  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE FALSE FALSE  TRUE
[37]  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE
[49]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE
[61]  TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE
[73]  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE
[85]  TRUE  TRUE  TRUE

📝 Convierte a tibble la base de datos iris incluida en R base (no necesitas ningún paquete, ya la tienes). Vamos a agrupar por species para después usar group_split(): nos genera una lista tal que cada elemento es la tabla para cada uno de los grupos (como un filter de cada grupo pero en un mismo objeto).

iris_list <-
  iris |> as_tibble() |>
  group_by(Species) |> 
  group_split()

Accede a cada variable Sepal.Length para cada una de las subtablas y haz la media (nos tendría que salir un vector de medias)

Código
iris_list |> 
  map("Sepal.Length") |> 
  map_dbl(mean)

🐣 Caso práctico I: renta municipios

En el archivo municipios.csv tenemos guardada la información de los municipios de España a fecha de 2019.

  • La variable LAU_code representa el código como unidad administrativa local según la estandarización de la UE (ver más).

  • La variable codigo_ine está construida uniendo el código de la provincia y el de la comunidad autónoma.

# datos 2019
mun_data <- read_csv(file = "./datos/municipios.csv")
mun_data
# A tibble: 8,212 × 8
   codauto ine.ccaa.name cpro  ine.prov.name cmun  name      LAU_CODE codigo_ine
   <chr>   <chr>         <chr> <chr>         <chr> <chr>     <chr>    <chr>     
 1 01      Andalucía     04    Almería       001   Abla      04001    04001     
 2 01      Andalucía     04    Almería       002   Abrucena  04002    04002     
 3 01      Andalucía     04    Almería       003   Adra      04003    04003     
 4 01      Andalucía     04    Almería       004   Albanchez 04004    04004     
 5 01      Andalucía     04    Almería       005   Alboloduy 04005    04005     
 6 01      Andalucía     04    Almería       006   Albox     04006    04006     
 7 01      Andalucía     04    Almería       007   Alcolea   04007    04007     
 8 01      Andalucía     04    Almería       008   Alcóntar  04008    04008     
 9 01      Andalucía     04    Almería       009   Alcudia … 04009    04009     
10 01      Andalucía     04    Almería       010   Alhabia   04010    04010     
# ℹ 8,202 more rows

🐣 Caso práctico I: renta municipios

Por otro lado el archivo renta_mun contiene datos de la renta per capita edmai de cada unidad administrativa (municipios, distritos, provincias, comunidades autonónomas, etc) para diferentes años.

renta_mun <- read_csv(file = "./datos/renta_mun.csv")
renta_mun
# A tibble: 55,273 × 7
   Unidad                          `2019` `2018` `2017` `2016` `2015` codigo_ine
   <chr>                            <dbl>  <dbl>  <dbl>  <dbl>  <dbl> <chr>     
 1 44001 Ababuj                        NA     NA     NA     NA     NA 44001     
 2 4400101 Ababuj distrito 01          NA     NA     NA     NA     NA 4400101   
 3 4400101001 Ababuj sección 01001     NA     NA     NA     NA     NA 4400101001
 4 40001 Abades                     11429  10731  10314   9816   9904 40001     
 5 4000101 Abades distrito 01       11429  10731  10314   9816   9904 4000101   
 6 4000101001 Abades sección 01001  11429  10731  10314   9816   9904 4000101001
 7 10001 Abadía                      8954   8589   8207   7671   8416 10001     
 8 1000101 Abadía distrito 01        8954   8589   8207   7671   8416 1000101   
 9 1000101001 Abadía sección 01001   8954   8589   8207   7671   8416 1000101001
10 27001 Abadín                     10791  10258   9762   9478   9116 27001     
# ℹ 55,263 more rows

🐣 Caso práctico I: renta municipios

Antes de empezar vamos a normalizar nombres de variables haciendo uso de clean_names() del paquete {janitor}.

mun_data <-
  mun_data |> 
  janitor::clean_names()
renta_mun <-
  renta_mun |> 
  janitor::clean_names()

 

Intenta responder a las preguntas planteadas en el workbook

🐣 Caso práctico II: encuesta de satisfacción

Para este ejercicio usaremos la tabla de datos de satisfacción de pacientes en un hospital guardada en el archivo .csv llamada SatisfaccionPacientes.csv

library(tidyverse)
datos <-
  read_csv("./datos/SatisfaccionPacientes.csv") |> 
  # normalización de nombres de variables
  janitor::clean_names()
datos
# A tibble: 100 × 8
      id  edad genero    estado_civil tiempo_espera grado_satisfaccion
   <dbl> <dbl> <chr>     <chr>                <dbl>              <dbl>
 1     1    60 Masculino Casado                  28                  8
 2     2    44 Femenino  Soltero                 22                  8
 3     3    43 Masculino Soltero                  8                  9
 4     4    32 Masculino Soltero                 21                  8
 5     5    66 Masculino Divorciado               7                 10
 6     6    43 Masculino Divorciado              20                  8
 7     7    54 Masculino Casado                  18                  6
 8     8    55 Masculino Soltero                 29                  6
 9     9    56 Masculino Viudo                   17                  9
10    10    34 Femenino  Casado                  34                  8
# ℹ 90 more rows
# ℹ 2 more variables: numero_visitas <dbl>, estado_salud <chr>

🐣 Caso práctico II: encuesta de satisfacción

📝 Aplica el código que sea necesario para determinar el tamaño muestral, el número de variables y el tipo de estas.

Código
# Tamaño muestral / número de observaciones
n <- datos |> nrow()

# Número de variables
p <- datos |> ncol()

# Tipo de variables
dplyr::glimpse(datos)
Rows: 100
Columns: 8
$ id                 <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, …
$ edad               <dbl> 60, 44, 43, 32, 66, 43, 54, 55, 56, 34, 56, 57, 38,…
$ genero             <chr> "Masculino", "Femenino", "Masculino", "Masculino", …
$ estado_civil       <chr> "Casado", "Soltero", "Soltero", "Soltero", "Divorci…
$ tiempo_espera      <dbl> 28, 22, 8, 21, 7, 20, 18, 29, 17, 34, 20, 29, 2, 31…
$ grado_satisfaccion <dbl> 8, 8, 9, 8, 10, 8, 6, 6, 9, 8, 9, 8, 8, 8, 9, 8, 10…
$ numero_visitas     <dbl> 10, 1, 2, 8, 3, 10, 7, 1, 2, 3, 3, 6, 10, 5, 4, 2, …
$ estado_salud       <chr> "Regular", "Bueno", "Bueno", "Bueno", "Malo", "Buen…

🐣 Caso práctico II: encuesta de satisfacción

📝 Usando tidyverse, obtén la tabla de frecuencias absolutas de la variable genero.

Código
tabla_freq <-
  datos |> 
  count(genero)

🐣 Caso práctico II: encuesta de satisfacción

📝 Incorpora a la tabla anterior las frecuencias relativas. ¿Podríamos calcular las frecuencias acumuladas? ¿Por qué?

Código
tabla_freq <-
  tabla_freq |> 
  rename(frecuencia_abs = n) |> 
  mutate(frecuencia_rel = frecuencia_abs/sum(frecuencia_abs))

# No podemos acumuladas ya que necesitamos una jerarquía de orden
# son cualis nominales, no ordinales

Haciendo uso de la tabla, ¿qué % de pacientes son mujeres?

Haz lo mismo tu solo con la variable estado_civil y determina el número de personas casadas (deberían ser 26).

🐣 Caso práctico II: encuesta de satisfacción

En R las variables cualitativas pueden ser tratadas como tales convirtiendo una cadena de texto a lo que se conoce como factor. Por ejemplo, supongamos que tenemos un vector de notas

notas <- c("suspenso", "notable", "suspenso", "aprobado", "notable", "suspenso")
notas
[1] "suspenso" "notable"  "suspenso" "aprobado" "notable"  "suspenso"

Para convertir a factor nos basta con factor(). ¿Qué notas diferente?

notas_fct <- factor(notas)
notas_fct
[1] suspenso notable  suspenso aprobado notable  suspenso
Levels: aprobado notable suspenso

🐣 Caso práctico II: encuesta de satisfacción

notas_fct
[1] suspenso notable  suspenso aprobado notable  suspenso
Levels: aprobado notable suspenso

Si te fijas ahora tenemos disponibles unos niveles (levels): son las posibles modalidades de nuestra variable cualitativa, el soporte, de manera que aunque borremos uno de ellos (vamos a borrar todos los aprobados), la opción sigue disponible si entrase un dato nuevo (algo así como un menú de opciones permitidas)

notas_fct[notas_fct != "aprobado"]
[1] suspenso notable  suspenso notable  suspenso
Levels: aprobado notable suspenso

🐣 Caso práctico II: encuesta de satisfacción

En el caso de las cualitativas ordinales podemos incluso establecer una jerarquía, indicando explícitamente los niveles y ordered = TRUE

notas_fct_ord <- factor(notas, levels = c("aprobado", "notable", "suspenso"),
                        ordered = TRUE)
notas_fct_ord
[1] suspenso notable  suspenso aprobado notable  suspenso
Levels: aprobado < notable < suspenso

Fíjate que ahora tenemos una jerarquía y aunque sea cualitativa podemos buscar elementos <= o >= que otros

notas_fct_ord[notas_fct_ord <= "notable"]
[1] notable  aprobado notable 
Levels: aprobado < notable < suspenso

🐣 Caso práctico II: encuesta de satisfacción

📝 Haz que tu variable estado_salud sea una factor representando una cualitativa ordinal

Código
datos <-
  datos |>
  mutate(estado_salud =
           factor(estado_salud, levels = c("Malo", "Regular", "Bueno", "Excelente"),
                  ordered = TRUE))

🐣 Caso práctico II: encuesta de satisfacción

📝 Calcula la tabla de frecuencias para estado_salud incluyendo todo lo que puedas (¿se puede ahora calcular las frecuencias acumuladas). ¿Cuántos pacientes están en un estado de salud regular o peor? (pisa: 59 personas).

Código
tabla_freq <- 
  datos |> 
  count(estado_salud) |> 
  rename(frecuencia_abs = n) |> 
  mutate(frecuencia_rel = frecuencia_abs/sum(frecuencia_abs),
         frecuencia_acum_abs = cumsum(frecuencia_abs),
         frecuencia_acum_rel = cumsum(frecuencia_rel))

datos |>
  count(estado_salud <= "Regular")

🐣 Caso práctico II: encuesta de satisfacción

📝 Calcula la media, mediana, cuartiles y desviación típica de edad y tiempo de espera. Guarda los resultados en resumen y expórtalo a un resumen.csv

Código
resumen <-
  datos |>
  summarise(media_edad = mean(edad), sd_edad = sd(edad),
            mediana_edad = median(edad),
            Q1_edad = quantile(edad, probs = 0.25),
            Q3_edad = quantile(edad, probs = 0.75),
            # tiempo espera
            media_tiempo_espera = mean(tiempo_espera), 
            sd_tiempo_espera = sd(tiempo_espera),
            mediana_tiempo_espera = median(tiempo_espera),
            Q1_tiempo_espera = quantile(tiempo_espera, probs = 0.25),
            Q3_tiempo_espera = quantile(tiempo_espera, probs = 0.75))
write_csv(resumen, file = "./datos/resumen.csv")

🐣 Caso práctico II: encuesta de satisfacción

📝 Haz una tabla de frecuencias (absolutas) cruzada entre genero y estado_salud con tidyverse para que quede como la tabla inferior

Código
# Primero cuento de manera bidimensional con count(var1, var2)
conteo_bidim <-
  datos |>
  count(genero, estado_salud)
conteo_bidim
# A tibble: 7 × 3
  genero    estado_salud     n
  <chr>     <ord>        <int>
1 Femenino  Malo             5
2 Femenino  Regular         24
3 Femenino  Bueno           24
4 Masculino Malo            10
5 Masculino Regular         20
6 Masculino Bueno           16
7 Masculino Excelente        1

🐣 Caso práctico II: encuesta de satisfacción

📝 Haciendo uso de la tabla anterior, ¿qué debes hacer para obtener una tabla de frecuencias bidimensional como la de abajo? (la tabla de frecuencias en formato habitual)

Código
# Después pivoto para que use la columna estado_salud
# como futuros nombres de variables (pivota de vertical a horizontal)
#   - names_from: de donde saldrán los futuros nombres de columnas
#   - values_from: de donde sacamos los valores numéricos (en este caso n)
#     para rellenar la tabla
tabla_freq_abs <-
  conteo_bidim |> pivot_wider(names_from = estado_salud, values_from = n)
tabla_freq_abs
# A tibble: 2 × 5
  genero     Malo Regular Bueno Excelente
  <chr>     <int>   <int> <int>     <int>
1 Femenino      5      24    24        NA
2 Masculino    10      20    16         1

🐣 Caso práctico II: encuesta de satisfacción

También se puede hacer en R base con table() (que además de ser más sencillo respeta la jerarquía de la ordinal). Moraleja: a veces R base nos facilita la vida, no lo olvidemos porque lo vas a necesitar

tabla_freq <- table(datos$genero, datos$estado_salud)
tabla_freq
           
            Malo Regular Bueno Excelente
  Femenino     5      24    24         0
  Masculino   10      20    16         1

🐣 Caso práctico II: encuesta de satisfacción

La tabla anterior se puede calcular con frecuencias relativa por filas y por columnas (es decir,una que toda las filas sumen el total, 1, y otra que las columnas sumen 1) haciendo uso de prop.table() aplicada a la tabla anterior (si margin = 1 normaliza por filas, si margin = 2 por columnas).

Código
prop.table(tabla_freq, margin = 1)
           
                  Malo    Regular      Bueno  Excelente
  Femenino  0.09433962 0.45283019 0.45283019 0.00000000
  Masculino 0.21276596 0.42553191 0.34042553 0.02127660
Código
prop.table(tabla_freq, margin = 2)
           
                 Malo   Regular     Bueno Excelente
  Femenino  0.3333333 0.5454545 0.6000000 0.0000000
  Masculino 0.6666667 0.4545455 0.4000000 1.0000000

Moraleja: tablas de frecuencia podemos hacerlas en tidyverse pero bidimensionales R base nos ayuda mejor. Deberemos viajar entre los dos mundos muchas veces

🐣 Caso práctico II: encuesta de satisfacción

📝 Haciendo uso de las tablas anteriores contesta a las siguientes preguntas:

  • ¿Qué porcentaje de entre las mujeres tiene un buen estado de salud?

  • ¿Qué porcentaje de entre los hombres tiene un estado de salud regular?

  • ¿Qué porcentaje de los que tienen estado de salud malo son mujeres?

Clicka debajo para ver la respuesta cuando lo tengas

Código
# De entre las mujeres un 45.28% tiene un buen estado de salud
# De entre los hombres un 34.04% tiene un estado de salud regular
# Un 33.33% de los que tienen un estado de salud malo, son mujeres

🐣 Caso práctico II: encuesta de satisfacción

📝 Un poquito de reminder de inferencia. Haciendo uso de la tabla de frecuencias absolutas, ejecuta el código que consideres para responder a la pregunta: ¿están estas dos variables (estado_salud y genero) asociadas? ¿Existe algún tipo de dependencia entre ellas? Hazlo considerando \(\alpha = 0.05\).

Código
# Opción 1: prueba de chi-cuadrado que nos permite sacar conclusiones
# sobre la independencia de dos variables cualitativas

# chisq.test() hace el contraste haciendo uso de la tabla de frec
chisq.test(tabla_freq)

# Como el p-value = 0.2322 y alpha = 5%, no podemos rechazar la
# hipótesis nula de independencia: no hay evidencias suficientes 
# CON LA MUESTRA QUE TENEMOS para concluir que haya alguna asociación
# entre género y estado de salud

# Opción 2: test exacto de Fisher, especialmente útil cuando
# las frecuencias esperadas son bajas.
fisher.test(tabla_freq)
# Diferente p-valor pero misma conclusión

🐣 Caso práctico II: encuesta de satisfacción

Las funciones chisq.test() y fisher.test() pueden tomar como argumento una tabla de frecuencias ya resumida o puede tomar [dos variables y la función ya realiza el conteo]{.hl-yellow. Por ejemplo si hacemos chisq.test(var1, var2) obtenemos un objeto htest que dentro contiene el p-valor.

test_chisq <- chisq.test(datos$genero, datos$estado_salud)
names(test_chisq)
[1] "statistic" "parameter" "p.value"   "method"    "data.name" "observed" 
[7] "expected"  "residuals" "stdres"   

Por tanto si aplicamos la función y hacemos después $p.value podemos obtener directamente el valor numérico que nos interesa

chisq.test(datos$genero, datos$estado_salud)$p.value
[1] 0.2322175

🐣 Caso práctico II: encuesta de satisfacción

📝 ¿Cómo usar tidyverse para tener en una tabla resumen ambos p-valores?

Código
tabla_p_valores <-
  datos |> 
  summarise("sig_chisq" = chisq.test(genero, estado_salud)$p.value,
            "sig_fisher" = fisher.test(genero, estado_salud)$p.value)

🐣 Caso práctico II: encuesta de satisfacción

📝 Cálcula la matriz correlaciones de Pearson entre las variables numéricas. ¿Existe dependencia LINEAL entre la variable edad y el tiempo de espera? (recuerda: correlación de Pearson solo mide asociación lineal). Echa un vistazo al paquete {corrr}

Código
# Opción 1
mat_cor <-
  datos |> 
  select(where(is.numeric)) |> 
  cor()

# Opción 2
mat_cor <-
  datos |> 
  select(where(is.numeric)) |> 
  corrr::correlate()

# La correlación es de 0.0669 por lo que no parezca exista relación

🐣 Caso práctico II: encuesta de satisfacción

📝 Calcula en una tabla resumen la correlación y el p-valor derivado de un test de correlaciones (cor.test()) entre ambas variables

Código
cor_summary <-
  datos |> 
  summarise("cor" = cor(edad, tiempo_espera),
            "sig_cor" = cor.test(edad, tiempo_espera)$p.value)
# No parece existir evidencia significativa de dependencia lineal

🐣 Caso práctico II: encuesta de satisfacción

📝 Repite todo lo anterior con las variables tiempo de espera y grado de satisfacción

Código
mat_cor <- 
  datos |> 
  select(where(is.numeric)) |> 
  corrr::correlate()

cor_summary <-
  datos |> 
  summarise("cor" = cor(grado_satisfaccion, tiempo_espera),
            "sig_cor" = cor.test(grado_satisfaccion, tiempo_espera)$p.value)
# Sí parece existir evidencia significativa de dependencia lineal
# concretamente negativa: a más espera, menor satisfacción

Clase 19: entrega III

Ponderación: 25%

Instrucciones

El día de la entrega tendrás subido una plantilla de entrega en formato .qmd en el campus.

  • Descomprime la carpeta (¡importante! si no descomprimes, aunque puedas editar el .qmd, no podrás generar el .html)

  • Edita la cabecera con tu nombre y DNI

  • Deberás rellenar cada chunk con el código que consideres (en algunos te he dejado pistas) y cambiar de #| eval: false a #| eval: true (si los quitas directamente, por defecto ya es true)

  • Deberás de comentar con texto normal lo que consideres para responder a las preguntas.

  • Será OBLIGATORIO subir el archivo .html generado (solo se corregirá dicho archivo) así que ve renderizando según rellenas el documento, no lo dejes para el final.

Clase 20: intro a dataviz

Empieza la fantasía: visualización de datos

Dataviz: historia

La aparición de gráficos estadísticos es relativamente reciente en la ciencia ya que hasta la Edad Media la única visualización estaba en los mapas. 1 Las propias palabras chart y cartography derivan del mismo origen latino, charta, aunque el primer uso de coordenadas viene de los egipcios. 2 3

No es hasta la Edad Media, cuando la navegación y la astronomía empezaban a tomar relevancia, cuando aparece la primera gráfica (no propiamente estadística), del movimiento cíclico de los planetas (siglos X y XI)

Primer gráfico estadístico

La mayoría de expertos, como Tufte 1 2, consideran este gráfico casi longitudinal como la primera visualización de datos de la historia, hecha por Van Langren en 1644, representando la distancia entre Toledo y Roma.

¿Qué es una dataviz?

¿Es una gráfica estadística? ¿Por qué sí o por qué no?

No hay ninguna INFORMACIÓN representada

¿Qué es una dataviz?

¿Es una gráfica estadística? ¿Por qué sí o por qué no?

No hay ningún PROCESO DE MEDIDA representado, no cuantifica nada (real).

¿Qué es una dataviz?

¿Es una gráfica estadística? ¿Por qué sí o por qué no?

No hay ningún DATO representado en él, es una magnitud física teórica, no un dato (medido empíricamente o simulado).

¿Qué es una dataviz?

Esas mismas preguntas se hizo Joaquín Sevilla 1, proporcionando 3 requisitos:

  1. Que se base en el esquema de composición de eje métrico (proceso de medida): debe medir algo.
  1. Debe incluir información estadística (datos)
  1. La relación de representatividad debe ser reversible: los datos deberían poder «recuperarse» a partir de la gráfica .

Vizfails

  • La figura elegida (persona caminando) sin relación con lo visualizado: mala metáfora.

  • Los sectores señalados sin relación con el ítem a representar, lo que dificulta su interpretación.

  • Los colores sin codificar: no dan información de ningún tipo.

  • Las formas irregulares impiden la comparación de las áreas (amén de que la suma total supera el 100%).

  • Sin fuente

Vizfails

Vizfails

La importancia del CONTEXTO

Una buena idea puede estar mal ejecutada: la forma de llevarla a cabo es importante

Dataviz: historia

En el siglo XVII hubo un boom de la estadística al empezar a aplicarse en demografía. Uno de los autores más importantes fue J. Graunt, autor de «Natural and Political Observations Made upon the Bills of Mortality» (1662), estimando la población de Londres con las primeras tablas de natalidad y mortalidad.

Son precisamente las tablas de Graunt las que usó Christiaan Huygens para generar la primera gráfica de densidad de una distribución continua (esperanza de vida vs edad).

Primera función de densidad, extraída de https://omeka.lehigh.edu/exhibits/show/data_visualization/vital_statistics/huygen

Gráficos de Playfair

La figura que cambió el dataviz fue, sin lugar a dudas, el economista y político William Playfair (1759-1823), publicando en 1786 el «Atlas político y comercial» 1 [^12] con 44 gráficas (43 series temporales y el diagrama de barras más famoso de la historia).

Extraídas de Funkhouser y Walker (1935)

Extraídas de Funkhouser y Walker (1935)

Gráficos de Playfair

Playfair es además el autor del gráfico de barras más famoso (no fue el primero pero sí quien lo hizo mainstream).

Gráficas de Playfair de importaciones (barras grises) y exportaciones (negras) de Escocia en 1781, extraídas de la wikipedia.

Primer diagrama de barras (P. Buache y G. de L’Isle), visualizando los niveles del Sena (1732 - 1766), extraída de https://friendly.github.io/HistDataVis

Mapas de Minard

Otro pionero en combinar visualizaciones fue Minard, autor del famoso «Carte figurative des pertes successives en hommes de l’Armée Française dans la campagne de Russie 1812-1813», según Tufte «el mejor gráfico estadístico jamás dibujado», publicado en 1869 sobre la desastrosa campaña rusa de Napoleón en 1812 (3 variables en un gráfico bidimensional)

Extraída de https://friendly.github.io/HistDataVis.

Primer scatter plot

Según J. Sevilla, se considera al astrónomo británico John Frederick William Herschel el autor del primer diagrama de dispersión o scatterplot en 1833, visualizando el movimiento de la estrella doble Virginis (tiempo en el eje horizontal, posición angular en el eje vertical)

Extraído de https://friendly.github.io/HistDataVis.

Florence Nigthingale

  • El 21 de octubre de 1854 Florence Nigthingale fue enviada para mejorar las condiciones sanitarias de los soldados británicos en la guerra de Crimea.

  • A su regreso demostró que los soldados fallecían por las condiciones sanitarias. Nigthingale es la creadora del famoso diagrama de rosa, visualizando tres variables a la vez y su estacionalidad.

  • El 8 de febrero de 1955, The Times la describió como la «ángel guardián» de los hospitales, y acabó siendo conocida como «The Lady with the Lamp» tras un poema de H. W. Longfellow (1857).

  • Años después se convirtió en la primera mujer en la Royal Statistical Society.

Diagrama de rosa

Florence Nigthingale es la creadora del famoso diagrama de rosa, permitiendo pintar tres variables a la vez y su estacionalidad: tiempo (cada gajo es un mes), nº de muertes (área del gajo) y causa de la muerte (color del gajo: azules enfermedades infecciosas, rojas por heridas, negras otras causas).

Recursos de dataviz

📚 «The Functional Art: an introduction to information graphics and visualization» de Alberto Cairo

📚 «Gramática de las gráficas: pistas para mejorar las representaciones de datos» de Joaquín Sevilla

📚 «A Brief History of Visualization» de Friendly et al. (2008)

📚 «Quantitative Graphics in Statistics: A Brief History» de James R. Beniger y Dorothy L. Robyn. The American Statistician (1978)]

📚 «Presentation Graphics» de Leland Wilkinson. International Encyclopedia of the Social & Behavioral Sciences

📚 «The Grammar of Graphics» de Leland Wilkinson

📚 «The Minard System: The Graphical Works of Charles-Joseph Minard» de Sandra Rendgen

📚 «The Visual Display of Quantitative Information» de E. W. Tufte

Dataviz en R: ggplot2

El paquete {ggplot2} se basa en la idea de Wilkinson en «Grammar of graphics»: dotar a los gráficos de una gramática propia. Una de las principales fortalezas de R es la visualización con {ggplot2}.

library(ggplot2)

La visualización de datos debería ser una parte fundamental de todo análisis de datos. No es solo una cuestión estética.

Dataviz en R: ggplot2

La filosofía detrás de {ggplot2} es entender los gráficos como parte del flujo de trabajo, dotándoles de una gramática. El objetivo es empezar con un lienzo en blanco e ir añadiendo capas a tu gráfico. La ventaja de {ggplot2} es poder mapear atributos estéticos (color, forma, tamaño) de objetos geométricos (puntos, barras, líneas) en función de los datos.

 

La documentación del paquete puedes consultarla en https://ggplot2-book.org/introduction.html

Dataviz en R: ggplot2

Dataviz en R: ggplot2

Un gráfico se podrá componer de capas

  • Datos (data)
  • Mapeado (aesthetics) de elementos estéticos: ejes, color, forma, etc (en función de los datos)
  • Geometría (geom): puntos, líneas, barras, polígonos, etc.
  • Componer gráficas (facet)
  • Transformaciones (stat): ordenar, resumir, etc.
  • Coordenadas (coord): coordenadas cartesianas, polares, grids, etc.
  • Temas (theme): fuente, tamaño de letra, subtítulos, captions, leyenda, ejes, etc.

Primer intento: scatter plot

Veamos un primer intento para entender la filosofía ggplot. Imagina que queremos dibujar un scatter plot (diagrama de dispersión de puntos). Para ello vamos a usar el conjunto de datos gapminder, del paquete homónimo: un fichero con datos de esperanzas de vida, poblaciones y renta per cápita de distintos países en distintos momentos temporales.

library(gapminder)
gapminder
# A tibble: 1,704 × 6
   country     continent  year lifeExp      pop gdpPercap
   <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
 1 Afghanistan Asia       1952    28.8  8425333      779.
 2 Afghanistan Asia       1957    30.3  9240934      821.
 3 Afghanistan Asia       1962    32.0 10267083      853.
 4 Afghanistan Asia       1967    34.0 11537966      836.
 5 Afghanistan Asia       1972    36.1 13079460      740.
 6 Afghanistan Asia       1977    38.4 14880372      786.
 7 Afghanistan Asia       1982    39.9 12881816      978.
 8 Afghanistan Asia       1987    40.8 13867957      852.
 9 Afghanistan Asia       1992    41.7 16317921      649.
10 Afghanistan Asia       1997    41.8 22227415      635.
# ℹ 1,694 more rows

Primer intento: scatter plot

El fichero consta de 1704 registros y 6 variables: country, continent, year, lifeExp (esperanza de vida), pop (población) y gdpPercap (renta per cápita).

glimpse(gapminder)
Rows: 1,704
Columns: 6
$ country   <fct> "Afghanistan", "Afghanistan", "Afghanistan", "Afghanistan", …
$ continent <fct> Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia, Asia, …
$ year      <int> 1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, …
$ lifeExp   <dbl> 28.801, 30.332, 31.997, 34.020, 36.088, 38.438, 39.854, 40.8…
$ pop       <int> 8425333, 9240934, 10267083, 11537966, 13079460, 14880372, 12…
$ gdpPercap <dbl> 779.4453, 820.8530, 853.1007, 836.1971, 739.9811, 786.1134, …

Para empezar con algo sencillo filtraremos solo los datos de 2007

gapminder_2007 <-
  gapminder |>
  filter(year == 2007) |> 
  drop_na(gdpPercap, lifeExp, pop)

Ingredientes: (x, y)

¿Qué elementos necesitamos para realizar un diagrama de puntos? Para iniciar el lienzo necesitamos una base de datos y dos variables a representar.

  • Datos (data): conjunto gapminder_2007.

  • Mapeado: indicar dentro de aes() (aesthetics) las variables en cada coordenada. Todo dentro de aes() será mapeado de los datos.

ggplot(data = gapminder_2007,
       aes(x = gdpPercap, y = pop))

Primera geometría: geom_point()

  • Geometría (geom): optaremos por puntos usando geom_point(). En este caso las variables deben ser cuantitativas continuas.
ggplot(gapminder_2007,
       aes(x = gdpPercap, y = pop)) +
  geom_point()

Rol de los ejes: (x, y)

Vamos a profundizar en ese mapeado: ¿cómo cambiar el rol de los ejes (población en el eje X y renta per cápita en el eje Y)?

  • Eje X: población (variable pop)
  • Eje Y: renta per cápita (variable gdpPercap)
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = pop)) +
  geom_point() 

Rol de los ejes: (x, y)

¿Y un scatter plot con esperanza de vida en eje X frente a renta per cápita?

  • Eje X: esperanza de vida (variable lifeExp)
  • Eje Y: renta per cápita (variable gdpPercap)
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point()

Color, size, shape: fijos

Dentro de geom_point() tenemos varios argumentos a usar:

  • na.rm = ...: si queremos que nos quite ausentes.

  • color = ...: color (si tiene dimensión, color del contorno)

  • fill = ...: color del relleno.

Empezaremos por un color fijo, por ejemplo "red" (existen otros como "blue", "black", "yellow", etc)

ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point(color = "red")

Color, size, shape: fijos

  • size = ...: tamaño de la geometría (en este caso el tamaño de los puntos), cuanto mayor sea el número, mayor será el tamaño de la geometría.
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point(color = "red", size = 7) 

Color, size, shape: fijos

  • alpha = ...: grado de opacidad del color (1 totalmente opaco, 0 totalmente transparente)
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point(color = "red", size = 7,
             alpha = 0.4)

Color, size, shape: fijos

  • shape = ...: forma de la geometría, en este caso del «punto» (ver todas las opciones en vignette("ggplot2-specs"))
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point(color = "red",
             fill = "black",
             size = 7,
             alpha = 0.4,
             shape = 23)

Color, size, shape: fijos

  • stroke = ...: tamaño del contorno
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point(color = "red", size = 7,
             alpha = 0.4, stroke = 3)

Color, size, shape: fijos

Los colores también podemos asignárselos por su código hexadecimal, consultando en https://htmlcolorcodes.com/es/, eligiendo el color que queramos. El código hexadecimal siempre comenzará con #

# Color en hexadecimal
# https://htmlcolorcodes.com/es/
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp)) +
  geom_point(color = "#A02B85",
             alpha = 0.4, size = 7) 

Mapeado estético: aes()

Hasta ahora los atributos estéticos se los hemos pasado fijos y constantes. Pero la verdadera potencia y versatilidad de ggplot es que podemos mapear los atributos estéticos en función de los datos en aes() para que dependan de variables de los datos.

Por ejemplo, vamos a asignar un color a cada dato en función de su continente con aes(color = continent)

# Tamaño fijo
# Color por continentes
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp,
           color = continent)) +
  geom_point(size = 7)

Mapeado estético: aes()

Podemos combinarlo con lo que hemos hecho anteriormente:

  • color en función del continente.

  • tamaño en función de la población.

  • transparencia la fijamos constante del 50%.

ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp,
           color = continent, size = pop)) +
  geom_point(alpha = 0.7)

A este scatter plot particular se le conoce BUBBLE CHART

Visualización multivariante

Reflexionemos sobre el gráfico anterior:

  • color en función del continente.
  • tamaño en función de la población
  • transparencia fija del 50%

 

Usando los datos hemos conseguido dibujar en un gráfico bidimensional 4 variables: lifeExp y gdpPercap en los ejes , continent como color y pop como tamaño de la geometría, con muy pocas líneas de código.

Etiquetas sencillas: labs()

Podemos personalizar de manera sencilla haciendo uso de la capa labs():

  • title, subtitle: título/subtítulo
  • caption: pie de gráfica
  • x, y: nombres de los ejes
  • size, color, fill, ...: nombre en la leyenda de las variables que codifiquen los distintos atributos

ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  labs(x = "Esperanza de vida", y = "Renta per cápita",
       title = "Primer ggplot", subtitle = "Datos de gapminder",
       caption = "J. Álvarez Liébana", color = "continente", size = "población")

Eliminar de la leyenda

Podemos eliminar variables de la leyenda con guides(atributo = "none")

ggplot(gapminder_2007,
       aes(y = gdpPercap, 
           x = lifeExp,
           color = continent, 
           size = pop)) +
  geom_point(alpha = 0.7) +
  guides(size = "none") +
  labs(x = "Esperanza de vida",
       y = "Renta per cápita",
       title = "Primer ggplot",
       caption = "J. Álvarez Liébana",
       color = "continente")

Escalas (scale): colores

Una de las capas más importantes es la capa de escalas (personalizar) ya que dentro de aes() solo le indicamos qué variable mapeamos pero no sus ajustes.

 

Por ejemplo, con scale_color_...() y scale_fill_...() podremos personalizar qué colores usar.

Escalas (scale): colores

Con scale_color_manual() podemos indicar manualmente una paleta (https://htmlcolorcodes.com/)

pal <- c("#A02B85", "#2DE86B", "#4FB2CA", "#E8DA2D", "#E84C2D")
ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent)) +
  geom_point(alpha = 0.7) +
  scale_color_manual(values = pal) +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Escalas (scale): colores

Otra opción es elegir alguna de las paletas de colores diseñadas en el paquete {ggthemes}:

  • scale_color_economist(): paleta de colores basada en los colores de The Economist.

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  ggthemes::scale_color_economist() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Escalas (scale): colores

Otra opción es elegir alguna de las paletas de colores diseñadas en el paquete {ggthemes}:

  • scale_color_colorblind(): paleta de colores basada en los colores de daltónicos/as.

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  ggthemes::scale_color_colorblind() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Escalas (scale): colores

Incluso cargar paletas de colores diseñadas en base a películas o arte

  • películas: paquete {harrypotter} (repositorio de Github aljrico/harrypotter) usando scale_color_hp_d().

Paleta basada en la casa Ravenclaw

devtools::install_github(repo = "aljrico/harrypotter") 
ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  harrypotter::scale_color_hp_d(option = "ravenclaw")+
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Escalas (scale): colores

Incluso cargar paletas de colores diseñadas en base a películas o arte

  • cuadros: paquete {MetBrewer} (repositorio de Github BlakeRMills/MetBrewer) usando scale_colour_manual(values = met.brewer(...)).

devtools::install_github(repo = "BlakeRMills/MetBrewer") 
library(MetBrewer)

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_color_manual(values = met.brewer("Monet")) +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Escalas (scale): colores

Incluso cargar paletas de colores diseñadas en base a películas o arte

  • discos: paquete {peRReo} (repositorio de Github jbgb13/peRReo) usando scale_colour_manual(values = latin_palette()).

devtools::install_github(repo = "jbgb13/peRReo") 
library(peRReo)

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_color_manual(values = latin_palette("rosalia")) +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Esclas (scale): colores

Incluso cargar paletas de colores diseñadas en base a películas o arte

  • discos: paquete {taylor} usando scale_color_taylor_d(album = ...) para usar paletas de álbumes de Taylor Swift.

library(taylor)
ggplot(gapminder_2007,
       aes(y = gdpPercap, x = lifeExp,
           color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_color_taylor_d(album = "Speak Now") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Clase 21: ggplot1

scale: colores continuos

Ya vimos como definir paletas de colores pero si te fijas la variable que codifica el color era cualitativa o discreta, ¿y si la variable que codifica color fuese cuantitativa numérica?

Existen unas paletas de colores conocidas como ColorBrewer pudiendo definirse de manera secuencial, divergente o de manera cualitativa (ver info en https://colorbrewer2.org)

RColorBrewer::brewer.pal.info
         maxcolors category colorblind
BrBG            11      div       TRUE
PiYG            11      div       TRUE
PRGn            11      div       TRUE
PuOr            11      div       TRUE
RdBu            11      div       TRUE
RdGy            11      div      FALSE
RdYlBu          11      div       TRUE
RdYlGn          11      div      FALSE
Spectral        11      div      FALSE
Accent           8     qual      FALSE
Dark2            8     qual       TRUE
Paired          12     qual       TRUE
Pastel1          9     qual      FALSE
Pastel2          8     qual      FALSE
Set1             9     qual      FALSE
Set2             8     qual       TRUE
Set3            12     qual      FALSE
Blues            9      seq       TRUE
BuGn             9      seq       TRUE
BuPu             9      seq       TRUE
GnBu             9      seq       TRUE
Greens           9      seq       TRUE
Greys            9      seq       TRUE
Oranges          9      seq       TRUE
OrRd             9      seq       TRUE
PuBu             9      seq       TRUE
PuBuGn           9      seq       TRUE
PuRd             9      seq       TRUE
Purples          9      seq       TRUE
RdPu             9      seq       TRUE
Reds             9      seq       TRUE
YlGn             9      seq       TRUE
YlGnBu           9      seq       TRUE
YlOrBr           9      seq       TRUE
YlOrRd           9      seq       TRUE

scale: colores continuos

Con RColorBrewer::brewer.pal() podemos obtener el vector de n colores para una paleta dada, por ejemplo la paleta divergente RdYlBu

RColorBrewer::brewer.pal(n = 5, name = "RdYlBu")
[1] "#D7191C" "#FDAE61" "#FFFFBF" "#ABD9E9" "#2C7BB6"

Con RColorBrewer::display.brewer.pal() podemos visualizar los colores de dicha paleta

RColorBrewer::display.brewer.pal(n = 10, name = "RdYlBu")

scale: colores continuos

Un ejemplo de paleta secuencial es PuBuGn

RColorBrewer::display.brewer.pal(n = 8, name = "PuBuGn")

scale: colores continuos

Un ejemplo de paleta cualitativa es Pastel1

RColorBrewer::display.brewer.pal(n = 8, name = "Pastel1")

scale: colores continuos

Para incluirlo podemos usar scale_color_brewer() o bien scale_color_distiller() si queremos crear una escala continua (interpolando entre los colores)

ggplot(gapminder_2007, aes(y = gdpPercap, x = pop, color = lifeExp)) +
  geom_point(alpha = 0.7, size = 3) +
  scale_color_distiller(palette = "RdYlBu") +
  guides(size = "none") +
  labs(x = "Población", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "esperanza de vida") +
  theme_minimal()

scale: gradiente de color

También podemos crear gradientes de color personalizados con scale_color_gradient(), scale_fill_gradient(), scale_color_gradient2(), scale_fill_gradient2()

Vamos a usar scale_color_gradient2(low = ..., mid = ..., high = ..., midpoint = ...) indicándole el color más bajo y el más alto, pero también el de en medio (y el valor numérico al que queremos asociarlo)

ggplot(gapminder_2007, aes(y = gdpPercap, x = pop, color = lifeExp)) +
  geom_point(alpha = 0.7, size = 3) +
  scale_color_gradient2(low = "#825598", mid = "#9dd0c1",
                        high = "#d93535", midpoint = 60) +
  guides(size = "none") +
  labs(x = "Población", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "esperanza de vida") +
  theme_minimal()

Escalas (scale): ejes

La misma idea que usamos para personalizar colores la podemos aplicar para, por ejemplo, personalizar los ejes

Vamos a personalizar las marcas del eje x con scale_x_continuous(breaks = ...) para tener marcas cada 10 unidades.

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent)) +
  geom_point(alpha = 0.7) +
  ggthemes::scale_color_colorblind() +
  scale_x_continuous(breaks = seq(30, 90, by = 10)) +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

Escalas (scale): ejes

Vamos a profundizar un poco dentro de nuestro scatter plot en escalas

  • ¿Cómo fijar límites en los ejes? En scale_x_continuous() y scale_y_continuous(), además de “saltos” podemos indicar límites con limits = ...

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(limits = c(50, 70), breaks = seq(50, 70, by = 5)) +
  scale_y_continuous(limits = c(1000, 18000), breaks = seq(0, 18000, by = 1000)) +
  ggthemes::scale_color_colorblind() +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

Escalas (scale): ejes

  • ¿Cómo etiquetar las unidades de los ejes? Haciendo uso del paquete {scales} podemos añadir prefijos/sufijos con labels = label_number(...)

library(scales)
ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(limits = c(50, 70), breaks = seq(50, 70, by = 5),
                     labels = label_number(suffix = " años")) +
  scale_y_continuous(limits = c(1000, 18000), breaks = seq(0, 18000, by = 1000),
                     labels = label_number(suffix = " $")) +
  ggthemes::scale_color_colorblind() +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

Escalas (scale): otros

Vamos a profundizar un poco dentro de nuestro scatter plot en escalas

  • ¿Cómo cambiar los ajustes de tamaño, alpha, etc? Igual que tenemos scale_x_...() o scale_color_...(), tenemos también scale_size_...() y scale_alpha_...()

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(aes(alpha = pop)) +
  scale_size(range = c(4, 12)) +
  scale_alpha(range = c(0.1, 0.5)) +
  ggthemes::scale_color_colorblind() +
  guides(size = "none", alpha = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

Escalas (scale): ejes

Vamos a profundizar un poco dentro de nuestro scatter plot en escalas

  • ¿Cómo cambiar la escala (relación) lineal entre los ejes? Con scale_x_sqrt() o scale_x_log10() podemos cambiar la escala de los ejes.

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_y_log10() +
  ggthemes::scale_color_colorblind() +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

theme: capa de temas

En {ggplot2} vamos a poder personalizar todo el gráfico incluido definir nuestro propio tema estético haciendo uso de las capas theme_...()

Por ejemplo, vamos a usar theme_minimal() para tener un tema “austero” y minimalista (aprenderemos a definir cada detalle).

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(breaks = seq(35, 85, by = 10)) +
  ggthemes::scale_color_colorblind() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

coord: capa de coordenadas

Además de escalas tenemos una capa de coordenadas con coord_... para indicar si queremos un sistema cartesiano, coordenadas polares (coord_polar()), si queremos coordenadas iguales (coord_equal()) o invertir su rol (coord_flip())

ggplot(gapminder_2007, aes(y = gdpPercap, x = pop, color = lifeExp)) +
  geom_point(alpha = 0.8, size = 3) +
  scale_x_log10() +
  scale_color_gradient2(low = "#E92745", mid = "#F4ED5B", high = "#56B1F7", midpoint = 60) +
  coord_flip() +
  labs(x = "Población", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "esperanza de vida") +
  theme_minimal()

stats: capa estadística

Una capa también importante es la capa de estadísticas

  • stat_smooth(): visualiza un ajuste suavizado de los datos (reg. lineal, glm, loess, gam, etc). Con stat_smooth(method = "lm") una recta de regresión. También con geom_smooth(method = "lm")

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp)) +
  geom_point(aes(color = continent, size = pop), alpha = 0.8) +
  stat_smooth(method = "lm", se = FALSE, linewidth = 1.5) +
  scale_y_log10() +
  guides(size = "none") +
  ggthemes::scale_color_colorblind() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

stats: capa estadística

Fíjate que si usas en la primera capa parámetros estéticos se acaban heredando a capas posteriores, en concreto al ajuste visualizado.

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.8) +
  stat_smooth(method = "lm", se = FALSE, linewidth = 1.5) +
  scale_y_log10() +
  guides(size = "none") +
  ggthemes::scale_color_colorblind() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

stats: capa estadística

Dentro de stat_smooth() podemos especificarle otro ajuste polinómico dándole expresión en formula = ..., por ejemplo un polinomio de grado 5 formula = y ~ x + I(x^2) + I(x^3) + I(x^4) + I(x^5)

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp)) +
  geom_point(aes(color = continent, size = pop), alpha = 0.8) +
  stat_smooth(method = "lm", formula = y ~ x + I(x^2) + I(x^3) + I(x^4) + I(x^5),
              color = "firebrick", se = FALSE, linewidth = 1.2) +
  guides(size = "none") +
  ggthemes::scale_color_colorblind() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

stats: capa estadística

Con stat_summary() podemos incluso añadir estadísticas por grupos, como la media o mediana.

ggplot(gapminder, aes(y = gdpPercap, x = year)) +
  geom_point(size = 1.7, alpha = 0.2) +
  stat_summary(fun = "mean", size = 0.4, color = "coral") + 
  stat_summary(fun = "median", size = 0.4, color = "darkcyan") +
  ggthemes::scale_color_colorblind() +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

facet: composición

Por último la capa facet_(): podemos desagregar los gráficos (facetar) por grupos, equivalente al group_by() en tidyverse.

Por ejemplo, vamos a crear un gráfico por continente, mostrando todos los gráficos a la vez (pero por separado) con facet_wrap(~continent).

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, size = pop, color = continent)) +
  geom_point(alpha = 0.75) +
  ggthemes::scale_color_colorblind() +
  facet_wrap(~continent) +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

Componiendo (facet)

También podemos desagregar los gráficos (facetar) por grupos, equivalente al group_by() en tidyverse.

Por defecto las escalas en los ejes son compartidas. Si queremos que la escala de los ejes vaya por libre debemos usar scales = "free_x", scales = "free_y" o scales = "free"

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, size = pop, color = continent)) +
  geom_point(alpha = 0.75) +
  ggthemes::scale_color_colorblind() +
  facet_wrap(~continent, scales = "free") +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

Componiendo (facet)

También podemos desagregar los gráficos (facetar) por grupos, equivalente al group_by() en tidyverse.

Con nrow = ... y ncol = ... podemos especificar cuantas columnas y filas tenemos en la cuadrícula de gráficas

ggplot(gapminder_2007, aes(y = gdpPercap, x = lifeExp, size = pop, color = continent)) +
  geom_point(alpha = 0.75) +
  ggthemes::scale_color_colorblind() +
  facet_wrap(~continent, scales = "free", nrow = 3) +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()

Componiendo (facet)

También le podemos pasar dos argumentos (variables) para formar un grid de gráficas con facet_grid(var1 ~ var2)

ggplot(gapminder |> filter(year >= 1962), aes(y = gdpPercap, x = lifeExp, size = pop, color = continent)) +
  geom_point(alpha = 0.7) +
  ggthemes::scale_color_colorblind() +
  facet_grid(continent ~ year, scales = "free") +
  guides(size = "none") +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente") +
  theme_minimal()