SOFTWARE PARA GESTIÓN DE BASES DE DATOS

Introducción al análisis de datos en R (y Stata)

Máster de Bioestadística • curso 2023-2024

¡Bienvenidos a R!

Dejad vuestras hojas de cálculo a un lado

¡Buenas!

Correo: . Despacho: 722 (3ª planta). Tutorías: lunes (14:30-16:00), martes (12:00-13:00) y viernes (13:00-14:00).

  • Javier Álvarez Liébana, de Carabanchel (Bajo).

  • Licenciado en Matemáticas (UCM). Doctorado 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.

Actualmente, investigador y docente en la Facultad de Estadística de la UCM. 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. La asistencia no será obligatoria pero si se valorará muy positivamente la participación en clase (nunca penalizando).
  • Evaluación. A lo largo del curso se han planteado 4 entregas individuales, así como una entrega final grupal (entre 2 y 4 personas) donde se deberá presentar el análisis realizado de un caso real.
  • Nota mínima. Para no ir al examen final se deberá obtener al menos un 4/10 en cada entrega o una media ponderada superior al 6/10. Se deberá obtener además en la entrega grupal una nota superior a 6-6.5-7/10 (según 2-3-4 personas)
  • Examen final. Cualquier alumno podrá presentarse a un examen final, siendo la valoración del mismo el 100% de su nota (perdiendo la evaluación continua).

Planificación I

CLASE SEMANA FECHAS TOPIC SCRIPTS EJ. CASO PRÁCTICO ENTREGA
1 S1 11 sep Primeros pasos en R 📝 💻
2 S1 13 sep Tipos básicos de datos 📝 💻 💻 💻 🐣
3 S2 18 sep Primeras bases de datos. R base vs Tidyverse 📝 💻 💻 🐣 🐣
4 S2 19 sep Importar/exportar e intro a Quarto/rmd 📝 💻 🐣 🐣
5 S3 25 sep Tidyverse (filas) 📝 💻 💻 🐣
6 S3 27 sep Entrega I 💻 🎯 10%
7 S3 2 oct Tidyverse (columnas) 📝 💻 💻 🐣
8 S4 4 oct Tidyverse (crear/modificar variables) 📝 💻 💻 🐣
9 S5 9 oct Funciones 📝 💻 [🐣] [🐣]

Materiales

En el menú de las diapositivas (abajo a la izquierda) tienes una opción para descargarlas en pdf en Tools (consejo: hazlo el final del curso ya que irán modificándose)

 

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).

  2. Instalar R: será nuestro lenguaje. La descarga la haremos (gratuitamente) desde https://cran.r-project.org/

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

  • Necesitaremos una gramática (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.

Advertencia

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 que se ha instalado correctamente, tras abrir R, deberías ver una pantalla blanca similar a esta.

Esa «pantalla blanca» se llama consola y podemos hacer un primer uso de ella como una calculadora.

Idea: a una variable llamada a le asignaremos el valor 1 (escribiremos el código de abajo en la consola y daremos «enter»)

a <- 1

Primera operación

Para comprobar que se ha instalado correctamente, tras abrir R, deberías ver una pantalla blanca similar a esta.

Esa «pantalla blanca» se llama consola y podemos hacer un primer uso de ella como una calculadora.

Idea: definiremos otra variable llamada b y le asignaremos el valor 2

a <- 1
b <- 2

Fíjate que…

En R usaremos <- como una flecha: la variable a la izquierda de dicha flecha le asignamos el valor que hay a la derecha (por ejemplo, a <- 1)

Primera operación

Para comprobar que se ha instalado correctamente, tras abrir R, deberías ver una pantalla blanca similar a esta.

Esa «pantalla blanca» se llama consola y podemos hacer un primer uso de ella como una calculadora.

Idea: haremos la suma a + b y nos devolverá su resultado

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

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 el lenguaje estadístico por excelencia, creado por y para estadísticos/as, con 5 ventajas fundamentales frente a Excel:

  • Lenguaje de programación: la obviedad → análisisreplicables

  • Gratuito: la filosofía de la comunidad de R es el compartir código bajo copyleft uso ético de dinero público

  • Software libre: no solo es gratis sino que permite acceder libremente a código ajeno, incluso al propio código fuente flexibilidad y transparencia

  • Lenguaje modular: hemos instalado lo mínimo, pero existen códigos de otras personas que podemos reusar (casi 20 000 paquetes) ahorro de tiempo

  • Lenguaje de alto nivel: facilita la programación (como Python) menor curva de aprendizaje

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

¿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.

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:
  • 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.

💻 Tu turno

Ejecuta tu primer script: crea un script de cero, programa lo indicado debajo y ejecútalo (de las 3 maneras posibles)

📝 Define una variable de nombre a y cuyo valor sea -1

Code
a <- -1

📝 Añade debajo otra línea para definir una variable b con el valor 5. Tras ello múltiplica ambas variables

Code
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.

c <- # deberías asignarle el valor 3
d <- # deberías asignarle el valor -1
Code
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().

Code
x <- 5
sqrt(x)

y <- -2
abs(y)

📝 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
Code
z <- x - 5
z

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).

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.

Filosofía: 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).
  • 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

Clase 2: primeros datos

¿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).
  • 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

Celdas: tipos de datos

¿Existen variables más allá de los números?

Piensa por ejemplo en los datos guardados de una persona:

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

Variables numéricos

El dato más sencillo (ya lo hemos usado) serán las variables numéricas

a <- 5
b <- 2
a + b

Para saber el tipo de una variable tenemos la función class()

class(a)
[1] "numeric"

Con las variables numéricas podemos realizar las operaciones aritméticas de una calculadora: sumar (+), raíz cuadrada (sqrt()), cuadrado (^2), etc.

a^2
[1] 25
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

En 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"

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 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 <- FALSE # ¿Está soltero? --> NO
class(soltero)
[1] "logical"

Puede tomar un tercer valor, NA o dato ausente, representando las siglas de not available.

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í a la izquierda es igual a la derecha

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 <, <=, > o >=.

¿Tiene la persona menos de 32 años?

edad <- 38
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] 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 representa 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 con as_date() del paquete {lubridate}

library(lubridate) # instala si no lo has hecho
fecha <- as_date("2023-03-28")
fecha + 1
[1] "2023-03-29"
class(fecha)
[1] "Date"

Variables de fecha

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

  • Con today() podemos obtener directamente la fecha actual.
today()
[1] "2023-12-18"
  • Con now() podemos obtener la fecha y hora actual
now()
[1] "2023-12-18 15:59:34 CET"
  • Con year(), month() o day() podemos extraer el año, mes y día
fecha <- today()
year(fecha)
[1] 2023
month(fecha)
[1] 12

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)

Code
edad <- 33
nombre <- "Javi"

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

Code
hermanos <- TRUE

library(lubridate)
fecha_nacimiento <- as_date("1989-09-10")

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

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

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

Code
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

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(33, 27, 60, 61)
edades
[1] 33 27 60 61

Tip

Un número individual x <- 1 (o bien x <- c(1)) es en realidad un vector de longitud uno.

Vectores: concatenar

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

edades
[1] 33 27 60 61

 

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

length(edades)
[1] 4

También podemos concatenar vectores

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

Secuencias numéricas

En muchas ocasiones querremos crear secuencias numéricas (por ejemplo, los días del mes). El comando seq(inicio, fin) nos permite crear una secuencia 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

El comando 1:n nos devuelve lo mismo que seq(1, n) (si el elemento inicial es mayor que el final, entenderá que la secuencia es decreciente)

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

Secuencias numéricas

También podemos definir otro tipo de distancia (paso) entre consecutivos

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

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

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

Operaciones con 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.

Operaciones con 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

Operaciones con 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

Operaciones con 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

Operaciones con 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

Operaciones con vectores

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

Operaciones con vectores

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 

Operaciones con vectores

Otra operación muy habitual es la de acceder a elementos del mismo. 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"

Tip

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

Operaciones con vectores

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"

Operaciones con 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, y calcula su suma.

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

sum(x)

📝 Obtén los elementos de x mayores que 4. Calcula el número de elementos de x mayores que 4.

Code
x[x > 4]
sum(x > 4)

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

Code
z <- 1/x
sort(z)
z[order(z)]

📝 Encuentra el máximo y el mínimo del vector x

Code
min(x)
max(x)

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

Code
x[x > 1 & x < 7]
all(x > 0)

📝 Dado el vector x <- c(1, -5, 8, NA, 10, -3, 9), extrae los elementos que ocupan los lugares 1, 2, 5, 6. Elimina del vector el segundo elemento. Tras eliminarlo determina su suma y su media

Code
x <- c(1, -5, 8, NA, 10, -3, 9)
x[c(1, 2, 5, 6)]
y <- x[-2]
sum(y, na.rm = TRUE)
mean(y, na.rm = TRUE)

🐣 Caso práctico

En el paquete {datasets} tenemos diversos conjuntos de datos y uno de ellos es airquality. Debajo te he extraído 3 variables de dicho dataset

temperature <- airquality$Temp
month <- airquality$Month
day <- airquality$Day
  1. ¿Qué representan los datos? ¿Cómo averiguarlo?
Code
? airquality

Haciendo uso de ? ... podemos consultar en el panel de ayuda lo que significa el objeto.

🐣 Caso práctico

  1. ¿Cuántos registros tenemos de mayo? ¿Y de abril? Construye una nueva variable date con la fecha de cada registro (combinando año, mes y día)
Code
# Una forma para registros de mayo
sum(month == 5)
[1] 31
Code
# Otra forma
length(month[month == 5])
[1] 31
Code
# ídem en abril
sum(month == 4)
[1] 0
Code
# variable date
dates <- lubridate::as_date(glue("{1973}-{month}-{day}"))
  1. Crea una nueva variable temp_celsius con la temperatura en ºC
Code
# Temperatura en celsius
temp_celsius <- (temperature - 32) * (5/9)

🐣 Caso práctico

  1. ¿Cuál fue la media de temperatura del mes de agosto? Extrae los días en los que la temperatura superó los 30 grados y calcula la cantidad de días en los que lo hizo.
Code
# media en agosto
mean(temperature[month == 8], na.rm = TRUE)
[1] 83.96774
Code
mean(temp_celsius[month == 8], na.rm = TRUE)
[1] 28.87097
Code
# Extremos días > 30
dates[temp_celsius > 30]
 [1] "1973-06-08" "1973-06-09" "1973-06-10" "1973-06-11" "1973-06-12"
 [6] "1973-07-07" "1973-07-08" "1973-07-09" "1973-07-10" "1973-07-14"
[11] "1973-07-19" "1973-07-28" "1973-08-06" "1973-08-07" "1973-08-08"
[16] "1973-08-09" "1973-08-10" "1973-08-27" "1973-08-28" "1973-08-29"
[21] "1973-08-30" "1973-08-31" "1973-09-01" "1973-09-02" "1973-09-03"
[26] "1973-09-04" "1973-09-05"
Code
length(dates[temp_celsius > 30]) # n días
[1] 27
Code
sum(temp_celsius > 30) # n días (otra forma)
[1] 27

Antes de seguir: microbenchmark

Hay un paquete muy útil para medir tiempos de distintas órdenes que hacen lo mismo (el paquete {microbenchmark}) para intentar ser con nuestro código lo más eficientes posible. Vamos a comparar por ejemplo order() y sort().

library(microbenchmark) # instalar primera vez
x <- rnorm(2e3) # 2000 elementos aleatorias
microbenchmark(sort(x), x[order(x)], times = 5e2) # 500 veces
Unit: microseconds
        expr    min     lq     mean median       uq    max neval
     sort(x) 88.806 90.446 97.40058 91.963 108.8960 174.66   500
 x[order(x)] 73.718 75.153 80.01093 76.342  80.7905 111.11   500

Primer intento: matrices

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?

  • Con cbind() concatenamos vectores en forma de columnas
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() (aunque lo recomendable es tener cada variable en columna e individuo en fila).
# Construimos la matriz por filas
rbind(estaturas, pesos)
          [,1] [,2] [,3] [,4]
estaturas  150  160  170  180
pesos       63   70   85   95
  • Podemos visualizar la matriz con la función View().
  • Podemos comprobar las dimensiones con dim(), nrow() y ncol(): nuestros datos están tabulados:
dim(datos_matriz)
[1] 4 2
nrow(datos_matriz)
[1] 4

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, para acceder a elementos deberemos proporcionar el índice de la fila y de la columna (si quedan libres implica todos de esa dimensión)
datos_matriz[2, 1]
estaturas 
      160 
datos_matriz[, 2]
[1] 63 70 85 95

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:15, ncol = 5) 
z
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    4    7   10   13
[2,]    2    5    8   11   14
[3,]    3    6    9   12   15

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] [,4] [,5]
[1,]  0.2  0.8  1.4  2.0  2.6
[2,]  0.4  1.0  1.6  2.2  2.8
[3,]  0.6  1.2  1.8  2.4  3.0

Primer intento: 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
# Media (mean) por columnas (MARGIN = 2)
apply(datos_matriz, MARGIN = 2, FUN = "mean")
estaturas     pesos 
   165.00     78.25 
# (Cuasi)varianza (var) por columnas (MARGIN = 2)
apply(datos_matriz, MARGIN = 2, FUN = "var")
estaturas     pesos 
 166.6667  208.9167 

No profundizaremos más ya que nuestro objetivo es tener variables de misma longitud pero tipos diferentes (pero que sepas que existen).

💻 Tu turno (matrices)

Intenta realizar los siguientes ejercicios sin mirar las soluciones

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

x <- matrix(0, nrow = 2, ncol = 3)
x
Code
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 y obtén sus dimensiones

Code
new_matrix <- (x + 1)/5
t(new_matrix)
dim(new_matrix)

📝 Define la matriz x <- matrix(1:12, nrow = 4). Obtén la primera fila, la tercera columna, y el elemento (4, 1).

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

📝 Con la matriz anterior definida como x <- matrix(1:12, nrow = 4), calcula la media de todos los elementos, la media de cada fila y la media de cada columna. Calcula la suma de de cada fila y de cada columna

Code
x <- matrix(1:12, nrow = 4)
mean(x) # de todos
apply(x, MARGIN = 1, FUN = "mean") # media por filas
apply(x, MARGIN = 2, FUN = "mean") # media por columnas
apply(x, MARGIN = 1, FUN = "sum") # suma por filas
apply(x, MARGIN = 2, FUN = "sum") # suma por columnas

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

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

Al ya no ser números 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 lo que se conoce como data.frame: concatenación de variables de igual longitud pero pueden ser de tipo distinto.

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

Segundo intento: data.frame

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

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   34  FALSE   javi   1989-09-10
2   24     NA carlos   1992-04-01
3   NA   TRUE  lucía   1980-11-27

Segundo intento: data.frame

¡TENEMOS NUESTRO PRIMER CONJUNTO DE DATOS! Puedes visualizarlo escribiendo su nombre en consola o con View(tabla)

Segundo intento: data.frame

Si queremos acceder a sus elementos, podemos como en las matrices (aunque no es recomendable): ahora 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 carlos   1992-04-01
tabla[, 3]  # tercera columna (de todos los individuos)
[1] "javi"   "carlos" "lucía" 
tabla[2, 1]  # primera característica de la segunda persona
[1] 24

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

Segundo intento: data.frame

  • 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"

Segundo intento: data.frame

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   34  FALSE   javi   1989-09-10          0
2   24     NA carlos   1992-04-01          2
3   NA   TRUE  lucía   1980-11-27          3

Clase 3: primeras bases de datos. R base vs Tidyverse

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

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

 

De ahora en adelante usaremos el formato 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

Intento final: tibble

tabla <- tibble("estatura" = c(1.7, 1.8, 1.6), "peso" = c(80, 75, 70),
                "IMC" = peso / (estatura^2))
tabla
# 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
tabla$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

Tip

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

💻 Tu turno (tibble)

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).

Code
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?

Code
names(airquality_tb)
ncol(airquality_tb)
nrow(airquality_tb)

📝 Filtra solo los datos del mes de agosto.

Code
airquality_tb[airquality_tb$Month == 8, ]

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

Code
airquality_tb[!(airquality_tb$Month %in% c(7, 8)), ]

📝 Modifica el siguiente código para quedarte solo con las variable de ozono y temperatura.

Code
airquality_tb[, c("Ozone", "Temp")]

📝 Selecciona los datos de temperatura y viento de agosto. Traduce a castellano el nombre de las columnas del conjunto filtrado.

Code
airquality_tb[airquality_tb$Month == 8, c("Temp", "Wind")]
names(airquality_tb) <- c("ozono", "rad_solar", "viento", "temp", "mes", "dia") 

Resumen hasta ahora

  • 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 –> objeto bidimensional (dos índices)
  • Un data.frame nos permite concatenar variables de DISTINTO tipo y MISMA longitud –> usaremos tibble como una opción mejorada de base de datos

🐣 Caso práctico

Del paquete Biostatistics usaremos el conunto de datos pinniped

Biostatistics::pinniped
  1. ¿Qué representan los datos? ¿Qué tipo de dato es? En caso de que no lo sea, convierte la base de datos a un tibble (renombra con pinniped_tb)
Code
? Biostatistics::pinniped
Code
class(Biostatistics::pinniped) # no es un tibble
[1] "data.frame"
Code
pinniped_tb <- as_tibble(Biostatistics::pinniped)
  1. ¿Cuántos registros hay? ¿Y variables? ¿De qué tipo es cada una?
Code
nrow(pinniped_tb)
[1] 33
Code
ncol(pinniped_tb)
[1] 6

🐣 Caso práctico

  1. Incorpora una variable extra llamada phoca que sea de tipo lógico y que nos diga si una especie es de la categoría Phoca o no.
Code
pinniped_tb$phoca <- pinniped_tb$Species == "Phoca"
  1. ¿A qué sexo le pesa más el cerebro: a las hembras o a los machos? ¿A quienes les pesa más el cuerpo: a los monógamos o a los polígamos?
Code
# ¿a quién le pesa más el cerebro?
mean(pinniped_tb$Male_brain_g, na.rm = TRUE) >
  mean(pinniped_tb$Female_brain_g, na.rm = TRUE)
[1] TRUE
Code
# ¿a quién le pesa más el cerebro?
mean(c(pinniped_tb$Male_mass_Kg[pinniped_tb$Mate_type == "mono"],
       pinniped_tb$Female_mass_Kg[pinniped_tb$Mate_type == "mono"])) >
  mean(c(pinniped_tb$Male_mass_Kg[pinniped_tb$Mate_type == "poly"],
         pinniped_tb$Female_mass_Kg[pinniped_tb$Mate_type == "poly"]))
[1] FALSE

🐣 Caso práctico

  1. Incopora una nueva variable que represente la diferencia entre el peso del cerebro entre machos y hembras (machos - hembras) para cada especie.
Code
pinniped_tb$dif_m_f <- pinniped_tb$Male_brain_g - pinniped_tb$Female_brain_g
pinniped_tb
# A tibble: 33 × 8
   Species     Male_brain_g Female_brain_g Male_mass_Kg Female_mass_Kg Mate_type
   <chr>              <dbl>          <dbl>        <dbl>          <dbl> <chr>    
 1 Monachus s…         370             NA          173            272. mono     
 2 Monachus m…         480            480          260            275  mono     
 3 Mirounga a…         700            640         2275            488  poly     
 4 Mirounga l…        1431.           899.        3510            566. poly     
 5 Leptonycho…         535            638.         450            447  poly     
 6 Ommatophoc…         425            530          154.           164  mono     
 7 Lobodon ca…         578.           539.         220.           224  mono     
 8 Hydrurga l…         765            660          324            367  mono     
 9 Cystophora…         480            430          343.           222. mono     
10 Erignathus…          NA            460          312.           326  mono     
# ℹ 23 more rows
# ℹ 2 more variables: phoca <lgl>, dif_m_f <dbl>

R base vs Tidyverse

Si conoces algún otro lenguaje de programación (o tienes gente cercana que programa) te extrañará que aún no hayamos hablado de conceptos habituales como

  • Bucles for: repetir un código un número fijo de iteraciones.

  • Bucles while: repetir un código hasta que se cumpla una condición

  • Estructuras if-else: estructuras de control para decidir por donde camina el código en función del valor de las variables.

Y aunque conocer dichas estructuras puede sernos en algún momento interesante, en la mayoría de ocasiones vamos a poder evitarlas (en especial los bucles)

¿Qué es tidyverse?

{tidyverse} es un «universo» de paquetes para garanatizar un flujo de trabajo (de inicio a fin) 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?

  • {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

También tenemos los paquetes {purrr} para el manejo de listas, {forcast} para cualitativas, {lubridate} para fechas, {readxl} para importar archivos .xls y .xlsx, {rvest} para web scraping y {rmarkdown} para comunicar resultados.

¿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

También tenemos los paquetes {purrr} para el manejo de listas, {forcast} para cualitativas, {lubridate} para fechas, {readxl} para importar archivos .xls y .xlsx, {rvest} para web scraping y {rmarkdown} para comunicar resultados.

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 introducido por Hadley Wickham (el Dios al que rezo) de estandarizar el formato 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

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 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?

Code
table4b |>
  pivot_longer(cols = "1999":"2000", names_to = "year",
               values_to = "cases")

📝 Echa un vistazo a la tabla relig_income del paquete {tidyr}. ¿Es tidydata? En caso negativo, ¿qué falla? ¿Cómo convertirla a tidy data en caso de que no lo sea ya?

Code
relig_income |>
  pivot_longer(cols = "<$10k":"Don't know/refused",
               names_to = "income",
               values_to = "people")

📝 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?

Code
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 who (dataset de la Organización Mundial de la Salud)

library(tidyr)
who
  1. ¿Qué significan los datos? ¿Cuántas variables y observaciones tenemos?
Code
# ? who
nrow(who)
[1] 7240
Code
ncol(who)
[1] 60
  1. ¿Cuántos tipos de variables tenemos?

🐣 Caso práctico

  1. ¿Todas las variables son necesarias? Elimina la información redundante.
Code
who$iso2 <- NULL

Solo necesitamos uno de los códigos ISO

🐣 Caso práctico

  1. Convierte a tidydata la base de datos realizando todas las opciones que consideres (consejo: usa papel y boli para bocetar como debería quedar la base de datos).

Todos empiezan por new_ (y no hay otro tipo) así que le indicamos al pivotar que los nombres tienen un prefijo a quitar. Además separamos ya entre tipo y sexo-edad (separados por _) y eliminamos ausentes. Tras ello separamos sexo de edad

Code
who |>
  pivot_longer(cols = "new_sp_m014":"newrel_f65",
               names_to = c("type", "sex-age"),
               values_to = "cases",
               names_prefix = "new_",
               names_sep = "_",
               values_drop_na = TRUE) |> 
  separate(col = "sex-age", into = c("sex", "age"),
           sep = 1)
# A tibble: 76,046 × 7
   country     iso3   year type  sex   age   cases
   <chr>       <chr> <dbl> <chr> <chr> <chr> <dbl>
 1 Afghanistan AFG    1997 sp    m     014       0
 2 Afghanistan AFG    1997 sp    m     1524     10
 3 Afghanistan AFG    1997 sp    m     2534      6
 4 Afghanistan AFG    1997 sp    m     3544      3
 5 Afghanistan AFG    1997 sp    m     4554      5
 6 Afghanistan AFG    1997 sp    m     5564      2
 7 Afghanistan AFG    1997 sp    m     65        0
 8 Afghanistan AFG    1997 sp    f     014       5
 9 Afghanistan AFG    1997 sp    f     1524     38
10 Afghanistan AFG    1997 sp    f     2534     36
# ℹ 76,036 more rows

Clase 4: import/export, comunicar

Importar/exportar datos en R. Comunicar resultados: rmd y Quarto

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 (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 (nuevo rmarkdown)

Comunicar: rmd y Quarto

Los archivos de extensión .qmd (o .rmd) 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 (no solo en R), 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/

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.

Cabecera de un qmd

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

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
  • date: fecha
  • 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
    • toc-depth: profundidad del índice
  • editor: si estás en modo visual o source.

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).

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

🐣 Caso práctico

Elabora 3 informes .qmd tal que

  • Crea un archivo por cada uno de los 3 casos prácticos de los anteriores temas, uno por tema
  • En cada uno de ellos haz una sección por ejercicio
  • Detalla todos los pasos que consideres mezclando texto, código y salidas
  • Si aparecen medidas estadísticas como la media, prueba a meter fórmulas con $$ (busca información de cómo introducir ecuaciones en latex)
  • Para avanzados: investiga las opciones del paquete {DT} (con la función datatable()) que nos permite introducir dentro de los .qmd los datos en formato de tabla dinámica, permitiendo ordenar y filtrar.

Importar/exportar datos

Hasta ahora solo hemos usado datos cargados ya en paquetes pero muchas veces necesitaremos importar datos de manera externa. Una de las principales fortalezas de R es que podemos importar datos de manera muy sencilla en distintos formatos:

  • Formatos nativos de R: formatos .rda, .RData y .rds

  • Datos rectangulares (tabulados): formatos .csv y .tsv

  • Datos sin tabular: formato .txt

  • Datos en excel: formatos .xls y .xlsx

  • Datos desde SAS/Stata/SPSS: formatos .sas7bdat, .sav y .dat

  • Datos Google Drive

  • Datos desde API: aemet, catastro, twitter, spotify, etc

Formatos nativos de R

Los ficheros más simples para importar en R (y que suele ocupar menos espacio en disco) son sus propias extensiones nativas: archivos con formatos .RData, .rda y .rds. Para cargar los primeros simplemente necesitamos usar la función nativa load() indicándole la ruta del archivo.

  • Archivo .RData: vamos a importar un dataset con las distintas características de los viajeros del Titanic, incluyendo quién sobrevivió y quién murió.
load("./datos/titanic.RData")
as_tibble(titanic)
# A tibble: 5 × 12
  PassengerId Survived Pclass Name    Sex     Age SibSp Parch Ticket  Fare Cabin
        <int>    <int>  <int> <fct>   <fct> <dbl> <int> <int> <fct>  <dbl> <fct>
1           1        0      3 Braund… male     22     1     0 A/5 2…  7.25 ""   
2           2        1      1 Cuming… fema…    38     1     0 PC 17… 71.3  "C85"
3           3        1      3 Heikki… fema…    26     0     0 STON/…  7.92 ""   
4           4        1      1 Futrel… fema…    35     1     0 113803 53.1  "C12…
5           5        0      3 Allen,… male     35     0     0 373450  8.05 ""   
# ℹ 1 more variable: Embarked <fct>

Formatos nativos de R

  • Archivo .rda: vamos a importar un dataset con datos de cáncer de pecho de Royston and Altman (2013), incluyendo 2982 pacientes y sus características
load("./datos/rotterdam_breast_cancer.rda")
as_tibble(rotterdam)
# A tibble: 7 × 15
    pid  year   age  meno size  grade nodes   pgr    er hormon chemo rtime recur
  <int> <int> <int> <int> <fct> <int> <int> <int> <int>  <int> <int> <dbl> <int>
1     1  1992    74     1 <=20      3     0    35   291      0     0  1799     0
2     2  1984    79     1 20-50     3     0    36   611      0     0  2828     0
3     3  1983    44     0 <=20      2     0   138     0      0     0  6012     0
4     4  1985    70     1 20-50     3     0     0    12      0     0  2624     0
5     5  1983    75     1 <=20      3     0   260   409      0     0  4915     0
6     6  1983    52     0 <=20      3     0   139   303      0     0  5888     0
7     7  1993    40     0 <=20      2     0    13     4      0     0  2491     0
# ℹ 2 more variables: dtime <dbl>, death <int>

Formatos nativos de R

  • Archivo .rds: para este tipo debemos usar 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.
lung_cancer <-
  readRDS(file = "./datos/NCCTG_lung_cancer.rds") |> as_tibble()
# A tibble: 5 × 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

Importante

Las rutas deben ir siempre sin espacios, ni eñes, ni tildes. Y fíjate que los archivos cargados con load() se cargan automáticamente en el environment (con el nombre guardado originalmente), pero las funciones read() solo se carga de manera local (sino se guarda, no existe a futuro)

Datos rectangulares: readr

El paquete {readr} dentro del entorno {tidyverse} contiene distintas funciones útiles para la carga de datos rectangulares (sin formatear).

  • read_csv(): archivos .csv cuyo separador sea la coma
  • read_csv2(): punto y coma
  • read_tsv(): tabulador.
  • read_table(): espacio.
  • read_delim(): función genérica para archivos delimitados por caracteres.

Todos necesitan como argumento la ruta del archivo amén de otros opcionales (saltar o no cabecera, decimales, etc). Ver más en https://readr.tidyverse.org/

Datos tabulados (.csv, .tsv)

La principal ventaja de {readr} es que automatiza el formateo para pasar de un archivo plano (sin formato) a un tibble (en filas y columnas, con formato).

  • Archivo .csv: con read_csv() cargaremos archivos separados por coma, pasando como argumento la ruta en file = .... Vamos a importar el dataset chickens.csv (sobre pollos de dibujos animados, why not). Si te fijas en la salida nos proporciona 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 las variables normalmente lo hará read_csv() de forma automática, 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 lo haga normalmente bien de forma automática podemos especificar el formato explícitamente en col_types = list() (en formato lista, con col_xxx() para cada tipo de variable, por ejemplo una la pondremos como cualitativa o factor). Incluso podemos indicar que variables que queremos seleccionar (sin ocupar memoria), indicándoselo en col_select = ...

chickens <-
  read_csv(file = "./datos/chickens.csv",
           col_types = list(col_character(), col_factor(), col_double(), col_character()),
           col_select = c(chicken, sex, eggs_laid))
chickens
# A tibble: 5 × 3
  chicken                 sex     eggs_laid
  <chr>                   <fct>       <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 sin tabular (.txt)

¿Qué sucede cuando el separador no es el correcto?

Si usamos read_csv() espera que el separador entre columnas sea una coma pero, como puedes ver con el siguiente .txt, nos interpreta todo como una sola columna: no tiene comas y no sabe por donde 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 sin tabular (.txt)

Para ello tenemos

  • read_csv2() cuando el separador sea el punto y coma, read_tsv() cuando el sea un tabulador y read_table() cuando el sea 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 en excel (.xls, .xlsx)

Otro de los paquetes fundamentales de importación será el paquete {readxl} para importar datos desde una Excel. Tres funciones serán claves:

  • read_xls() específica para .xls, read_xlsx() específica para .xlsx
  • read_excel(): para ambas

Vamos a importar deaths.xlsx con registros de fallecimientos de famosos

library(readxl)
deaths <- read_xlsx(path = "./datos/deaths.xlsx")
deaths
# A tibble: 8 × 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    
7 Chuck Berry                  musician   90    TRUE     9788          42812    
8 Bill Paxton                  actor      61    TRUE     20226         42791    

Datos en excel (.xls, .xlsx)

deaths
# A tibble: 8 × 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    
7 Chuck Berry                  musician   90    TRUE     9788          42812    
8 Bill Paxton                  actor      61    TRUE     20226         42791    

Algo por desgracia muy habitual es que haya algún tipo de comentario o texto al inicio del archivo, teniendo que saltarnos dichas filas.

Datos en excel (.xls, .xlsx)

Podemos saltarnos dichas filas directamente en la carga con skip = ... (indicando el número de filas que nos saltamos)

library(readxl)
deaths <- read_xlsx(path = "./datos/deaths.xlsx", skip = 4)
deaths
# A tibble: 5 × 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          

Datos en excel (.xls, .xlsx)

Además con col_names = ... podemos renombrar ya las columnas en la importación (si proporcionamos nombres asume la 1ª línea ya como un dato)

deaths <-
  read_xlsx(path = "./datos/deaths.xlsx",
            skip = 5,
            col_names = c("name", "profession", "age", "kids", "birth", "death"))
deaths
# A tibble: 7 × 6
  name               profession age   kids  birth               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 Henderson actor      82    TRUE  1934-02-14 00:00:00 42698

Datos en excel (.xls, .xlsx)

En ocasiones las fechas de Excel están mal formateadas (sorpresa): podemos hacer uso de convertToDate() del paquete {openxlsx} para convertirlo

library(openxlsx)
deaths$death <- convertToDate(deaths$death)
deaths
# A tibble: 7 × 6
  name               profession age   kids  birth               death     
  <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

Datos en excel (.xls, .xlsx)

También podemos cargar un Excel con varias hojas: para indicarle la hoja (bien por su nombre bien por su número) usaremos el argumento sheet = ...

mtcars <- read_xlsx(path = "./datos/datasets.xlsx", sheet = "mtcars")
mtcars
# A tibble: 5 × 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

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 

Desde SAS/STATA/SPSS

El paquete {haven} dentro de la órbita tidyverse nos permitirá importar archivos 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 manera que podemos importar también podemos exportar

  • exportado en .RData (opción recomendada para variables guardadas en R). Recuerda que esta extensión solo se podrá usar en R. Para ello nos basta con usar save(objeto, file = ruta)
tabla <- tibble("a" = 1:4, "b" = 1:4)
save(tabla, file = "./datos/tabla_prueba.RData")
rm(tabla) # eliminar
load("./datos/tabla_prueba.RData")
tabla
# A tibble: 4 × 2
      a     b
  <int> <int>
1     1     1
2     2     2
3     3     3
4     4     4

Exportar

De la misma manera que podemos importar también podemos exportar

  • exportado en .csv. Para ello nos basta con usar write_csv(objeto, file = ruta)
write_csv(tabla, file = "./datos/tabla_prueba.csv")
read_csv(file = "./datos/tabla_prueba.csv")
# A tibble: 4 × 2
      a     b
  <dbl> <dbl>
1     1     1
2     2     2
3     3     3
4     4     4

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 de covid del ISCIII (https://cnecovid.isciii.es/covid19/#documentaci%C3%B3n-y-datos)

covid_datos <-
  read_csv(file = "https://cnecovid.isciii.es/covid19/resources/casos_hosp_uci_def_sexo_edad_provres.csv", n_max = 700)
covid_datos

Desde google drive

Otra opción disponible (sobre todo si trabajamos con otras personas que trabajan) es importar desde una hoja de cálculo Google Drive, haciendo uso de read_sheet() del paquete {googlesheets4}

La primera vez te pedirá un permiso de tidyverse para interactuar con vuestro drive

library(googlesheets4)
google_sheet <-
  read_sheet("https://docs.google.com/spreadsheets/d/1n_UTbD93-oDJR2r-rsMNff5ro147NL_ZN_vYIA2eJ3Q/edit?usp=sharing")
google_sheet

Desde API (owid)

Una opción también muy interesante es la carga de datos desde una API: un intermediario entre una app o proveedor datos y nuestro R. Por ejemplo, vamos a cargar la librería {owidR}, que nos permite la descarga de datos de la web https://ourworldindata.org/. La función owid_covid() nos carga sin darnos cuenta más de 300 000 registros con más de 50 variables de 238 países

library(owidR)
owid_covid()

Desde API (owid)

Este paquete tiene la función owid_search() para buscar datasets por palabras clave, por ejemplo, emissions, dándonos un dataset con el título de la base de datos y su id para luego usarla.

as_tibble(owid_search("emissions"))

Vamos a pedirle por ejemplo las emisiones de la oecd

owid("emissions-of-air-pollutants-oecd")

Desde API (aemet)

En muchas ocasiones para conectar con la API tendremos antes que registrarnos y obtener una clave, es el caso del paquete {climaemet} para acceder a datos meteorológicos (https://opendata.aemet.es/centrodedescargas/inicio)

Una vez que tenemos la clave de la API la registramos en nuestro RStudio para poder usarla a futuro

library(climaemet)

# Definir la clave
apikey <- "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYXZhbHYwOUB1Y20uZXMiLCJqdGkiOiI4YTU1ODUxMS01MTE3LTQ4MTYtYmM4OS1hYmVkNDhiODBkYzkiLCJpc3MiOiJBRU1FVCIsImlhdCI6MTY2NjQ2OTcxNSwidXNlcklkIjoiOGE1NTg1MTEtNTExNy00ODE2LWJjODktYWJlZDQ4YjgwZGM5Iiwicm9sZSI6IiJ9.HEMR77lZy2ASjmOxJa8ppx2J8Za1IViurMX3p1reVBU"

aemet_api_key(apikey, install = TRUE)

Desde API (aemet)

Con dicho 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 (por ejemplo, la estación del aeropuerto de El Prat, Barcelona, es el código "0076")

stations <- aemet_stations()
stations
aemet_last_obs("0076")

Desde API (catastro)

También puedes conectar con la API del catrastro y buscar por ejemplo por coordenadas

library(CatastRo)
catr_ovc_get_rccoor(lat = 38.61965, lon = -3.45624, srs = "4230")

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 El dataset who que hemos usado en ejercicios anteriores, expórtalo a un formato nativo de R en la carpeta datos del proyecto

Code
library(tidyr)
save(who, file = "./datos/who.RData")

📝 Carga el dataset who pero desde la carpeta de datos (importa el archivo creado en el ejercicio anterior)

Code
load("./datos/who.RData")

📝 Repite lo mismo (exportar e importar) en 4 formatos: .csv, .xlsx, .sav (spss) y .dta (stata)

Code
# csv
library(readr)
write_csv(who, file = "./datos/who.csv")
who_data <- read_csv(file = "./datos/who.csv")

# excel
library(openxlsx)
write.xlsx(who, file = "./datos/who.xlsx")
who_data <- read_xlsx(path = "./datos/who.xlsx")

# sas y stata
library(haven)
write_sav(who, path = "./datos/who.sav")
who_data <- read_spss(path = "./datos/who.sav")

write_dta(who, path = "./datos/who.dta")
who_data <- read_dta(path = "./datos/who.dta")

📝 Repite la carga del who.csv pero solo selecciona ya en la carga las 4 primeras columnas

Code
who_select <-
  read_csv(file = "./datos/who.csv",
           col_select = c("country", "iso2", "iso3", "year"))

🐣 Caso práctico

En la carpeta de datos tienes el dataset breast-cancer-wisconsin-data.csv. Crea un archivo .qmd y personalízalo incluyendo lo siguiente:

  1. Importa el archivo csv a un tibble. ¿Es tidydata? ¿Cuántos pacientes y variables tenemos?
Code
datos <- read_csv(file = "./datos/breast-cancer-wisconsin-data.csv")
datos
# A tibble: 568 × 33
         id diagnosis radius_mean texture_mean perimeter_mean area_mean
      <dbl> <chr>           <dbl>        <dbl>          <dbl>     <dbl>
 1   842302 M                18.0         10.4          123.      1001 
 2   842517 M                20.6         17.8          133.      1326 
 3 84300903 M                19.7         21.2          130       1203 
 4 84348301 M                11.4         20.4           77.6      386.
 5 84358402 M                20.3         14.3          135.      1297 
 6   843786 M                12.4         15.7           82.6      477.
 7   844359 M                18.2         20.0          120.      1040 
 8 84458202 M                13.7         20.8           90.2      578.
 9   844981 M                13           21.8           87.5      520.
10 84501001 M                12.5         24.0           84.0      476.
# ℹ 558 more rows
# ℹ 27 more variables: smoothness_mean <dbl>, compactness_mean <dbl>,
#   concavity_mean <dbl>, `concave points_mean` <dbl>, symmetry_mean <dbl>,
#   fractal_dimension_mean <dbl>, radius_se <dbl>, texture_se <dbl>,
#   perimeter_se <dbl>, area_se <dbl>, smoothness_se <dbl>,
#   compactness_se <dbl>, concavity_se <dbl>, `concave points_se` <dbl>,
#   symmetry_se <dbl>, fractal_dimension_se <dbl>, radius_worst <dbl>, …

🐣 Caso práctico

  1. El dataset representa datos de cáncer de pecho (id identificador, diagnosis el diagnóstico maligno/benigno y el resto propiedades del tumor). Usando SOLO LO APRENDIDO, ¿qué % tenían un tumor maligno y qué % uno benigno?
Code
100 * sum(datos$diagnosis == "M") / nrow(datos)
[1] 37.32394
Code
100 * sum(datos$diagnosis == "B") / nrow(datos)
[1] 62.67606
  1. ¿Cuál de los dos tipos de tumores tienen, de media, un radio más elevado?
Code
mean_M <- mean(datos$radius_mean[datos$diagnosis == "M"], na.rm = TRUE)
mean_B <- mean(datos$radius_mean[datos$diagnosis == "B"], na.rm = TRUE)

🐣 Caso práctico

  1. Busca la ayuda de la función t.test(). Dicha función nos permite contrastar si la media de dos distribuciones son o no iguales. ¿Podemos rechazar la hipótesis nula de que la media del radio sea la misma, bajo una significancia de \(\alpha = 0.05\)?
Code
datos_M <- datos$radius_mean[datos$diagnosis == "M"]
datos_B <- datos$radius_mean[datos$diagnosis == "B"]
t.test(datos_M, datos_B, conf.level = 0.95)

    Welch Two Sample t-test

data:  datos_M and datos_B
t = 22.177, df = 288.79, p-value < 2.2e-16
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 4.833259 5.774710
sample estimates:
mean of x mean of y 
 17.46283  12.15885 

Sí, podemos rechazar.

Clase 5: tidyverse (filas)

Operaciones con filas

¿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

También tenemos los paquetes {purrr} para el manejo de listas, {forcast} para cualitativas, {lubridate} para fechas, {readxl} para importar archivos .xls y .xlsx, {rvest} para web scraping y {rmarkdown} para comunicar resultados.

Preprocesamiento: dplyr

Dentro de {tidyverse} usaremos el paquete {dplyr} para el preprocesamiento y depuración de datos de 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

Code
starwars |>
  filter(species == "Droid" | is.na(species))

📝 Selecciona del conjunto de starwars solo los personajes cuyo peso esté entre 65 y 90 kg.

Code
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

Code
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.

Code
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

Code
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 Lando Ca…    177    79 black      dark       brown             31 male  mascu…
2 Tarfful      234   136 brown      brown      blue              NA male  mascu…
# ℹ 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 Han Solo     180    80 brown      fair       brown             29 male  mascu…
2 Gregar T…    185    85 black      dark       brown             NA <NA>  <NA>  
3 Yarael P…    264    NA none       white      yellow            NA male  mascu…
4 Wicket S…     88    20 brown      brown      brown              8 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 6 1 4 5 5 2 5 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 1 1 2 4 1 1 2 1 1 1 4 5 1 4 4 1 1 1 1 5 1 3 2 1 1 2 5 1 1 1 3 1 2 2 2 1 1
[39] 2 4 2 2 1 1 1 1 1 6 3 1

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()

💻 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.

Code
starwars |>
  filter(eye_color == "brown" & species == "Human") |> 
  arrange(height, desc(mass))

📝 Extrae 3 registros aleatoriamente.

Code
starwars |> slice_sample(n = 3)

📝 Extrae el 10% de los registros aleatoriamente.

Code
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)

Code
starwars |>
  drop_na(mass) |> 
  slice_sample(n = 10, weight_by = mass)

📝 Selecciona los 3 personajes más mayores.

Code
starwars |> slice_max(birth_year, n = 3)

Clase 6: entrega I

Primera entrega individual en clase

Ejercicios previos

Ejercicios recomendables para practicar antes de la práctica y conocer el dataset en cuestión.

Realiza la importación de los datos de covid del ISCIII directamente desde la web (archivo casos_hosp_uci_def_sexo_edad_provres.csv) incluyendo todas las filas pero cargando solo las columnas provincia_iso, sexo, grupo_edad, fecha y num_casos.

📝 Determina el tipo de dato es cada variable importada

📝 ¿Cuántas observaciones tenemos?

📝 Renombra el nombre de las variables al inglés

📝 ¿Podemos convertir el dataset a una matriz? Argumenta e ilustra el por qué sí o no.

📝 Crea una nueva variable (fuera de la base de datos) que nos diga el número de días que han pasado de cada registro respecto a hoy.

Entrega I (27/09/2023)

Se actualizará al inicio de la clase correspondiente.

Importante

Esta entrega ponderará un 10% en la nota final

Podrás usar todo el material del curso e internet salvo cualquier aplicación de mensajería o sistema de comunicación: en caso de tener alguna abierta a lo largo de la entrega, aunque no se esté usando, deberás abandonar la entrega y contará como un 0.

Contarás con aproximadamente 1 hora y 50 minutos, pero deberás realizar la entrega, como tarde, a las 18:00. Será obligatorio subir tanto el .qmd como el .html: una entrega que no haya podido ser renderizada (es decir, con errores graves) será penalizada contando solo un 65% de la nota obtenida (si la entrega es perfecta, la nota máxima será un 6.5 sino se adjunta el .html generado).

Te recomiendo practicar bastante y preguntar dudas en clase y/o tutoría ya que irás justo de tiempo, así que necesitas fluidez en los ejercicios.

Si usas un pc de la biblioteca asegúrate de tomarlo con suficiente antelación para instalar todas las librerías necesarias antes de empezar para no perder tiempo.

Clase 7: tidyverse (filas y columnas)

Operaciones con columnas

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

📝 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.

Code
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.

Code
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 usar el dataset biopsy que podemos encontrar en el agregador de datasets https://vincentarelbundock.github.io/Rdatasets/index.html. El dataset contiene datos de 699 pacientes a lo que se les realizó una biopsia de pecho, obteniendo 11 variables (una que hace de id y 10 escalas medidas de 1 a 10)

Puedes ver la documentación en https://vincentarelbundock.github.io/Rdatasets/doc/MASS/biopsy.html

  1. Crea de cero un informe en Quarto donde hagas los ejercicios posteriores (usa un archivo de estilos para que quede estético)
  1. Importa el .csv desde la propia web (a través del enlace del archivo)
  1. Habrás visto que hay 12 columnas en realidad importadas (la primera nos sobra ya que es solo un contador de filas). Vuelve a cargarlo seleccionando en la carga solo desde ID hasta class

🐣 Caso práctico

  1. La variable ID en realidad es un identificador (una cualitativa). Repite la carga especificando los tipos de datos: cualitativa o factor para ID y class, números para el resto de variables)
  1. La variable ID debería ser identificador de cada registro: elimina duplicados por dicha variable del dataset anterior.
  1. Del dataset anterior filtra solo los pacientes con tumor maligno y la variable V9 con valor 4 o inferior, eliminando además cualquier registro que contenga ausente en cualquiera de la variables.
  1. Del dataset anterior obtén una muestra del 20% de los datos (cada registro puede ser elegido con la misma probabilidad), y órdenalos por de mayor a menor por la variable V1 y, en caso de empate, de menor a mayor por la variable V2

Selección columnas: select()

datos |> selecciono(var1, var2, ...)
starwars |> select(var1, var2, ...)

La opción más sencilla para seleccionar variables por nombre es 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.

📝 Con los datos obtenidos del ejercicio anterior, traduce el nombre de las columnas a castellano

📝 Con los datos obtenidos del ejercicio anterior, coloca la variable de color de pelo justo detrás de la variable de nombres.

📝 Con los datos obtenidos del ejercicio anterior, comprueba cuántas modalidades únicas hay en la variable de color de pelo (sin usar unique()).

📝 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.

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>

Clase 8: tidyverse (resúmenes) y recategorizar

Summarise y group_by(). Contar y resumir: estadísticas desagregadas por factores/grupos

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>

💻 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.

Code
starwars |> 
  select(name, height, contains("color")) |> 
  drop_na(height)

📝 Con los datos obtenidos del ejercicio anterior, traduce el nombre de las columnas a castellano.

Code
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.

Code
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.

Code
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

Code
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()).

Code
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.

Code
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 8

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.
  1. 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)\).

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.

Code
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

Code
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.

Code
starwars |>
  slice_min(birth_year, by = sex)

📝 Obtén la edad del personaje más joven y más viejo de cada sexo.

Code
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.

Code
starwars |>
  count(birth_decade = round(birth_year, -1))

Clase 9: repaso e intro a funciones

¿Qué es una función? ¿Cómo se definen?

🐣 Caso práctico 9

Vamos antes a hacer un repaso de lo aprendido en {tidyverse}

  1. Carga la tabla billboard del paquete {tidyr}.
Code
billboard
  1. Antes de nada, selecciona solo las primeras 52 semanas. Tras ello convierte el dataset a tidydata con los formatos y tipos adecuados para cada variable
Code
billboard_tidy <-
  billboard |> 
  select(artist:date.entered, wk1:wk52) |> 
  pivot_longer(cols = wk1:wk52,
               names_to = "week",
               values_to = "rank",
               values_drop_na = TRUE,
               names_prefix = "wk") |> 
  mutate(week = as.numeric(week))
  1. Extrae la lista de artistas distintos que aparecen en la tabla, incluyendo cuántas veces aparece cada uno.
Code
billboard_tidy |> 
  count(artist)

🐣 Caso práctico 9

  1. Determina cuántas canciones tiene cada artistal
Code
billboard_tidy |> 
  distinct(artist, track) |> 
  count(artist)
  1. Determina las 5 canciones que más semanas aparecen en la lista de éxitos.
Code
billboard_tidy |> 
  count(track) |> 
  slice_max(n = 5, n)
  1. Determina para cada artista la canción que más semanas aparece en la lista de éxitos.
Code
billboard_tidy |> 
  count(artist, track) |> 
  slice_max(n = 1, n, by = artist)

🐣 Caso práctico 9

  1. Determina el artista con más canciones en la lista
Code
billboard_tidy |> 
  distinct(artist, track) |> 
  count(artist) |> 
  slice_max(n = 1, n)
  1. Calcula la posición más alta en la que ha estado cada canción. Calcula la posición más alta en la que ha estado un artista
Code
billboard_tidy |> 
  slice_min(rank, n = 1, with_ties = FALSE, by = track)
Code
billboard_tidy |> 
  slice_min(rank, n = 1, with_ties = FALSE, by = artist)

🐣 Caso práctico 9

  1. Obtén una tabla resumen con el ranking medio de cada artista (contando solo el ranking más alto alcanzado por sus canciones), así como el número de canciones (distintas) que ha colocado en el top 100.
Code
billboard_tidy |> 
  slice_min(rank, n = 1, with_ties = FALSE, by = track) |> 
  summarise(avg_rank = mean(rank), n_songs = n(), .by = artist)
  1. Realiza un muestreo aleatorio estratificado, extrayendo el 50% de los datos pero manteniendo la proporción de datos entre los distintos cuatrimestres.
Code
billboard_tidy |> 
  mutate(quarter = quarter(date.entered)) |> 
  slice_sample(prop = 0.5, by = quarter)

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()).
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:

  • 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 que queramos ejecutar
  código
  
  # Salida
  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) {
  
  area <- lado_1 * lado_2
  return(area)
  
}

Creando funciones

También podemos hacer una definición directa, sin almacenar variables 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

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 lista.

# 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))
  
}

Clase 10: profundizando funciones

¿Qué es una función? ¿Cómo se definen? Variables locales vs globlales. Introducción a listas

Funciones en R

Repasando el último día…

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))
  
}
salida <- calcular_area(5, 3)
salida["area"]
$area
[1] 15
salida["lado_1"]
$lado_1
[1] 5

Importante

Todas las variables que definamos dentro de la función son variables locales: solo existirán dentro

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.

variable_1 <- c("Paloma", "Gregorio")
variable_2 <- "Madrid"
variable_3 <- c(25, 30, 26)

lista <- list("progenitores" = variable_1, "lugar_nacimiento" = variable_2,
              "edades_hermanos" = variable_3)
lista
$progenitores
[1] "Paloma"   "Gregorio"

$lugar_nacimiento
[1] "Madrid"

$edades_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" = variable_1,
       "lugar_nacimiento" = variable_2,
       "edades_hermanos" = variable_3)
Error in `tibble()`:
! Tibble columns must have compatible sizes.
• Size 2: Existing data.
• Size 3: Column `edades_hermanos`.
ℹ Only values of size one are recycled.

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 el operador $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_nacimiento
[1] "Madrid"

Salida múltiple

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

Es altamente recomendable hacer la llamada a la función indicando explícitamente los argumentos para mejorar la 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

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.

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)
Code
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)
Code
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. Echa un vistazo al paquete {stringr}.

Code
# 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(toupper(persona_1) == toupper(persona_2))
}
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 y el nombre.

Code
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".

Code
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.

Code
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)

🐣 Caso práctico 10: funciones

Define una función llamada conversor_temperatura que, dada una temperatura en Fahrenheit, Celsius o Kelvin, la convierta a cualquiera de las otras (piensa que argumentos necesita el usuario). Aplica la función a la columna Temp del conjunto airquality, e incorpórala al fichero en una nueva columna Temp_Celsius.

🐣 Caso práctico 10: tidyverse

Acude a https://cnecovid.isciii.es/covid19/#documentaci%C3%B3n-y-datos e importa casos_hosp_uci_def_sexo_edad_provres.csv. Documentación en https://cnecovid.isciii.es/covid19/resources/metadata_casos_hosp_uci_def_sexo_edad_provres.pdf

  1. ¿Cuántos casos hay notificados para edad desconocida? ¿Y desagregado por sexo? Recodifica adecuadamente las provincias
  1. Dado su escaso peso en el total, genera una nueva base de datos en la que borremos aquellos registros con franja de edad desconocida.
  1. Con la base de datos generada en el ejercicio anterior, calcula la proporción de casos con sexo desconocido. Haz lo mismo con provincia desconocida. Elimina dichos registros si el número de casos representa menos del 1% (para cada una).

🐣 Caso práctico 10: tidyverse

  1. Del dataset anterior, elimina la variables de hospitalizados y UCI. Tras ello renombra las columnas de casos y fallecidos por casos_diarios y fallec_diarios, respectivamente. Tras ello crea dos nuevas variables llamadas casos_acum y otra fallec_acum, que contengan los casos acumulados y fallecidos acumulados para cada fecha, desagregados por provincia, tramo etario y sexo.
  1. ¿Cuáles fueron las 7 provincias con más casos a lo largo de toda la pandemia? ¿Y las 5 provincias con menos fallecidos? ¿Y si lo desagregamos por sexo?

🐣 Caso práctico 10: tidyverse

  1. Define una función llamada calculo_letalidad() que, dados como argumentos un vector ordenado (por fecha) de casos y otro de fallecidos, devuelva el % de casos que han fallecido, de manera acumulada en cada fecha. Haz uso de dicha función y crea una nueva variable que represente la letalidad, en cada grupo de edad, sexo y provincia.

Tras ello, determina las 5 provincias con mayor letalidad en mujeres mayores de 80 años a fecha 01 de marzo de 2022.

Clase 11: entrega II

Segunda entrega individual en clase

Entrega II

Se actualizará al inicio de la clase correspondiente.

Importante

Esta entrega ponderará un 15% en la nota final

Podrás usar todo el material del curso e internet salvo cualquier aplicación de mensajería o sistema de comunicación: en caso de tener alguna abierta a lo largo de la entrega, aunque no se esté usando, deberás abandonar la entrega y contará como un 0.

Contarás en clase con aproximadamente 1 hora y 50 minutos, pero podrás realizar la entrega, como tarde, hasta las 18:00. Será obligatorio subir tanto el .qmd como el .html: una entrega que no haya podido ser renderizada (es decir, con errores graves) será penalizada contando solo un 65% de la nota obtenida (si la entrega es perfecta, la nota máxima será un 6.5 sino se adjunta el .html generado).

Te recomiendo practicar bastante y preguntar dudas en clase y/o tutoría ya que irás justo de tiempo, así que necesitas fluidez en los ejercicios.

Si usas un pc de la biblioteca asegúrate de tomarlo con suficiente antelación para instalar todas las librerías necesarias antes de empezar para no perder tiempo.

Evita copiar: la IA detecta bastante bien el plagio :)

Clase 12: inicio a la visualización

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 .

Abolición de tartas

Hay muchas formas de hacer una gráfica estadística, y no suele pasar por un gráfico de tartas ya que tienen un grave problema de reversibilidad:

  • Si hay muchas variables: salvo que tengas transportador de ángulos…

  • Si hay pocas variables: ¿aporta algo distinto (y/o mejor) que una tabla?

Abolición de tartas

El principal problema de un diagrama de sectores es que la posible información está contenida en los ángulos, pero nuestra interpretación la realizamos a través de la comparación de áreas (nuestros ojos no miden bien ángulos), las cuales dependen no solo del ángulo sino del radio.

 

Algo similar sucede con los mal llamados gráficos tridimensionales (son bidimensionales con perspectiva en realidad): los valores más cercanos aparecen sobredimensionados, siendo prácticamente imposible la reversibilidad por la distorsión.

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 2 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 no solo fue el primero en usar el dataviz para entender (y no solo describir): fue el primero en usar conceptos modernos como grid, tema o color

Extraída de https://friendly.github.io/HistDataVis

Extraída de la wikipedia.

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

Gráficos de Playfair

Playfair además fue el primero en combinar gráficos en la misma visualización 1 2

Visualiza 3 series temporales: precios (barras) del trigo, salarios (línea) y time-line con reinados, extraída de https://friendly.github.io/HistDataVis.

Time-line histórico, 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.

Primera pirámide poblacional

La primera pirámide de población (doble histograma de población), fue publicada por Francis Amasa Walker, superintendente del censo de EE.UU., en 1874.

Extraída de https://www.depauw.edu/learn/dew/wpaper/workingpapers/DePauw2016-02-Barreto-DemographyEconomics.pdf

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 1997

gapminder_1997 <-
  gapminder |>
  filter(year == 1997) |> 
  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_1997.

  • Mapeado: indicar dentro de aes() (aesthetics) las variables en cada coordenada. Todo dentro de aes() será mapeado de los datos

ggplot(data = gapminder_1997,
       aes(x = gdpPercap, y = pop))

Primera geometría: geom_point()

  • Geometría (geom): optaremos por puntos usando geom_point().
ggplot(gapminder_1997,
       aes(x = gdpPercap, y = pop)) +
  geom_point()

Rol de os 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_1997,
       aes(y = gdpPercap, x = pop)) +
  geom_point() 

Rol de os 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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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_1997,
       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): ejes

Una de las capas más importantes es la capa de escalas: dentro de aes() solo le indicamos que variable mapeamos pero no sus ajustes.

Vamos a configurar el eje x para tener marcas cada 10 unidades (scale_x_continuous())

ggplot(gapminder_1997,
       aes(y = gdpPercap, x = lifeExp,
           color = continent)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(breaks = seq(35, 85, by = 10)) +
  labs(x = "Esperanza de vida", y = "Renta per cápita",
       title = "Primer ggplot", caption = "J. Álvarez Liébana",
       color = "continente")

Escalas (scale): colores

La misma idea la podemos aplicar a otro atríbuto como los colores con scale_color_...() y scale_fill_...(): hemos indicado que mapeé dicho atributo por continente pero…¿qué colores usar?

Con scale_color_manual() podemos indicar manualmente una paleta (puedes buscar en https://htmlcolorcodes.com/)

pal <- c("#A02B85", "#2DE86B", "#4FB2CA", "#E8DA2D", "#E84C2D")
ggplot(gapminder_1997, aes(y = gdpPercap, x = lifeExp, color = continent)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(breaks = seq(35, 85, by = 10)) +
  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_1997, 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_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_1997, 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")

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_1997, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(breaks = seq(35, 85, by = 10)) +
  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_1997, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(breaks = seq(35, 85, by = 10)) +
  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_1997, aes(y = gdpPercap, x = lifeExp, color = continent, size = pop)) +
  geom_point(alpha = 0.7) +
  scale_x_continuous(breaks = seq(35, 85, by = 10)) +
  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")

Escalas : otros atributos

Lo mismo que hemos hecho para los ejes o colores podemos hacer para el resto de atríbutos estéticos

Por ejemplo, vamos a indicarle que mapeé el tamaño en función de población pero indicándole el rango de valores (continuo en este caso) entre los que moverse con scale_size_continuous()

ggplot(gapminder_1997, 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() +
  scale_size_continuous(range = c(3, 15)) +
  labs(x = "Esperanza de vida", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "continente")

::: ::::

Tema (básico)

Por último en este primer gráfico, vamos personalizar el tema con alguna de las capas theme_...()

Por ejemplo, vamos a usar theme_minimal() para tener un tema “austero” y minimalista (aprenderemos a definir cada detalle de nuestro tema).

ggplot(gapminder_1997, 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()

Clase 13: profundizando en ggplot

Visualización de datos

Resumen de capas

Vamos a hacer un pequeño resumen de lo que llevamos aprendido hasta ahora respecto a {ggplot2}

  • Capa de datos: normalmente un gráfico en {ggplot2} empieza con ggplot(datos).
  • Mapeo de atributos estéticos: todo lo que queramos que se codifique en función de los datos debe ir dentro de aes()
  • Capa geométrica: para decidir si queremos un scatter plot, un diagrama de barras, un histograma, etc
  • Capas de escalas: para decidir los ajustes personalizados de atributos estéticos (escala del alpha o size, paleta de colores, etc)
  • Tema: tema para personalizar el gráfico (y etiquetas)

Capa de escalas

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_1997, 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()

Capa de escalas

  • ¿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_1997, 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()

Capa de escalas

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_1997, 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()

Capa de escalas

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_1997, 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()

scale_color: paleta Brewer

Una de las capas de escalas más importantes son las capas de color. Ya vimos como definir paletas manuales, ¿pero qué opciones hay para escalas continuas de colores?

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_color: paleta Brewer

Con RColorBrewer::brewer.pal() podemos obtener el vector de n colores para una paleta dada

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 = 5, name = "RdYlBu")

scale_color: paleta Brewer

Para incluirlo podemos usar scale_colour_brewer() o bien scale_color_distiller() si queremos crear una escala continua (interpolando entre los colores)

ggplot(gapminder_1997, aes(y = gdpPercap, x = pop, color = lifeExp)) +
  geom_point(alpha = 0.7, size = 3) +
  scale_x_log10() +
  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()

Gradiente de color manual

Tambien podemos crear un gradiente de color manual son scale_..._gradient() para dos colores, scale_..._gradient2() para tres colores (bajo, medio y alto) y scale_..._gradientn() para n colores

ggplot(gapminder_1997, 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) +
  labs(x = "Población", y = "Renta per cápita", title = "Primer ggplot",
       caption = "J. Álvarez Liébana", color = "esperanza de vida") +
  theme_minimal()

Capa de coordenadas

Además de escalas tenemos una capa de coordenadas con coord_... para indicar si queremos un sistema cartesiano (y sus límites), coordenadas polares (coord_polar()), si queremos coordenadas iguales (coord_equal()) o invertir su rol (coord_flip())

ggplot(gapminder_1997, 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()

Capa de stats

Una capa 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", se = FALSE) una recta de regresión (sin intervalos).

ggplot(gapminder_1997, 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()

Capa de stats

Una capa importante es la capa de estadísticas que nos permite combinar en nuestro gráfico algunas funcionalidades

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_1997, 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()

geom_text()

Podemos añadirle textos simples con geom_text(label = ...), por ejemplo, para añadir la correlación del ajuste.

cor <- round(cor(gapminder_1997$gdpPercap, gapminder_1997$lifeExp), 3)
ggplot(gapminder_1997, aes(y = gdpPercap, x = lifeExp)) +
  geom_point(aes(color = continent, size = pop), alpha = 0.8) +
  stat_smooth(method = "lm", se = FALSE) +
  geom_text(aes(x = 50, y = 20000, label = glue("Correlación: {cor}")),
                size = 5, color = "darkcyan") +
  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()

Capa de stats

Dentro de stat_smooth() podemos especificarle otro ajuste polinómico dándole expresión en formula = ...

ggplot(gapminder_1997, 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()

Capa de stats

Sin method especificado ajuste por un LOESS (menos de 1000 puntos) o GAM (más de 1000 puntos)

ggplot(gapminder_1997, aes(y = gdpPercap, x = lifeExp)) +
  geom_point(aes(color = continent, size = pop), alpha = 0.8) +
  stat_smooth(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()

Capa de stats

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()

Capa de stats

Fíjate que si no tenemos una variable cuali, la media la hace con n = 1 (es decir, es el propio punto).

ggplot(gapminder, aes(y = gdpPercap, x = pop)) +
  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()

Componiendo (facet)

También 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_1997, 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_1997, 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_1997, 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

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()

Componiendo (facet)

Aprenderemos distintas personalizaciones del tema pero con theme(legend.position = ...) podemos decidir la posición de la leyenda

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() +
  theme(legend.position = "bottom")

Clase 14: profundizando en ggplot

Visualización de datos

Variables continuas

Hemos aprendido a realizar uno de los gráficos más famosos, un diagrama de dispersión, pero…¿qué propiedades deben cumplir las variables?

Para visualizar dos variables con un diagrama de dispersión es necesario que ambas sean variables numéricas continuas

¿Se te ocurre algúna gráfico básico para variables discretas?

Cualis: barras

¿Y si tengo variables discretas o cualitativas?

Vamos a usar el ya conocido conjunto starwars para visualizar en un diagrama de barras: vamos a representar la frecuencia de una variable cualitativa como es sex.

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

Cualis: barras

La ventaja de ggplot es que, al trabajar por capas, todo lo que hemos aprendido nos sirve: solo tenemos que cambiar la geometría.

En este caso para realizar un diagrama de barras usaremos geom_bar() en lugar de geom_point(), indicando solo la variable de grupo con x = sex (ggplot hará solo el recuento)

ggplot(starwars, aes(x = sex)) +
  geom_bar()
  theme_minimal()

Cualis: barras

Podemos aplicar lo aprendido sobre colores para codificar la información, en este caso vamos a usar las paletas ya cargadas en scale_color_colorblind() del paquete {ggthemes}

library(ggthemes)
starwars |> 
  drop_na(sex) |> 
  ggplot(aes(x = sex)) +
  geom_bar(aes(color = sex), alpha = 0.5) + 
  scale_color_colorblind() +
  theme_minimal()

Cualis: barras

Fíjate que ahora solo nos ha coloreado el contorno: en otras geometrías, como las barras, será importante distinguir entre color y fill

starwars |> 
  drop_na(sex) |> 
  ggplot(aes(x = sex)) +
  geom_bar(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  theme_minimal()

Cualis: barras

Podemos personalizar el gráfico haciendo uso de las opciones ya vistas, por ejemplo, con escalas en ejes, títulos de las variables, leyendas, etc

starwars |> 
  drop_na(sex) |> 
  ggplot(aes(x = sex)) +
  geom_bar(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  scale_y_continuous(breaks = seq(0, 70, by = 10)) +
  labs(x = "sexo", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
       caption = "J. Álvarez Liébana") +
  theme_minimal()

Cualis: barras

Para cambiar el rol de los ejes, generando un diagrama de barras horizontales, podemos dejar el gráfico igual y luego simplemente hacer coord_flip()

starwars |> 
  drop_na(sex) |> 
  ggplot(aes(x = sex)) +
  geom_bar(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  scale_y_continuous(breaks = seq(0, 70, by = 10)) +
  coord_flip() +
  labs(x = "sexo", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
       caption = "J. Álvarez Liébana") +
  theme_minimal()

Paréntesis: temas

Vamos a hacer un paréntesis y aprender a personalizar más nuestras gráficas

  • theme_set(theme_minimal()) fija tema base

  • theme_update(...) personaliza parámetros.

Por ejemplo, en plot.title vamos a indicarle el tamaño y negrita en el título, dentro de element_text()

theme_set(theme_minimal())
theme_update(plot.title = element_text(size = 25, face = "bold"))
starwars |> 
  drop_na(sex) |> 
  ggplot(aes(x = sex)) +
  geom_bar(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  coord_flip() +
  labs(x = "sexo", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente", caption = "J. Álvarez Liébana")

Paréntesis: temas

Podemos hacer lo mismo con otros textos con plot.subtitle o plot.caption

theme_set(theme_minimal())
theme_update(
  plot.title =
    element_text(size = 25, face = "bold"),
  plot.subtitle = element_text(size = 13),
  plot.caption = element_text(size = 9))

Paréntesis: temas

Vamos incluso a elegir fuente o el color

  • sysfonts::font_add_google(): le indicaremos la tipografía de https://fonts.google.com/

  • showtext_auto() del paquete {showtext} nos permite su uso.

library(showtext)
library(sysfonts)
font_add_google(name = "Roboto")
showtext_auto()
theme_set(theme_minimal(base_family = "Roboto")) 

# Configurar tema
theme_update(
  plot.title = element_text(color = "#C34539", face = "bold", size = 33),
  plot.subtitle = element_text(color = "#3E6FCB", face = "bold", size = 21),
  axis.title.x = element_text(size = 19),
  axis.title.y = element_text(size = 19))

Cualis: barras apiladas

¿Podríamos visualizar dos variables discretas/cualis a la vez?

Podemos incluir una en x = ... y otra en fill = ..., de manera que por defecto nos visualiza barras apiladas, por ejemplo, para ver el reparto de sexos entre humanos y no humanos.

starwars |> 
  drop_na(sex) |> 
  mutate(Human = species == "Human") |> 
  ggplot(aes(x = Human)) +
  geom_bar(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  labs(x = "¿Humanos?", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
       caption = "J. Álvarez Liébana")

Cualis: barras solapas

Con position = "dodge" visualizamos las barras sin apilar, solapadas una al lado de otra

starwars |> 
  drop_na(sex) |> 
  mutate(Human = species == "Human") |> 
  ggplot(aes(x = Human)) +
  geom_bar(aes(fill = sex), alpha = 0.5, position = "dodge") + 
  scale_fill_colorblind() +
  labs(x = "¿Humanos?", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
       caption = "J. Álvarez Liébana")

Cualis: barras fill

Con position = "fill" visualizamos las barras en forma de frecuencia relativa, con las barras de la misma altura para facilitar la comparativa.

starwars |> 
  drop_na(sex) |> 
  mutate(Human = species == "Human") |> 
  ggplot(aes(x = Human)) +
  geom_bar(aes(fill = sex), alpha = 0.5,  position = "fill") + 
  scale_fill_colorblind() +
  labs(x = "¿Humanos?", y = "frecuencia relativa", fill = "sexo", title = "Primer diagrama de barras",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
       caption = "J. Álvarez Liébana")

Paréntesis: factores

En el caso de las variables cualitativas, llamaremos niveles o modalidades a los diferentes valores que pueden tomar estos datos. Por ejemplo, en el caso de la variable sex del conjunto starwars, tenemos 4 niveles permitidos: female, hermaphroditic, male y none (amén de datos ausentes).

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

Paréntesis: factores

Este tipo de variables se conocen en R como factores. Y el paquete fundamental para tratarlos es {forcats} (del entorno {tidyverse}).

Paréntesis: factores

Este paquete nos permite fijar los niveles (guardados internamente como levels) que toma una determinada variable categórica, dándoles un tratamiento diferente a las cadena de texto normales.

Veamos un ejempo sencillo definiendo una variable estado que tome los valores "sano", "leve" y "grave" de la siguiente manera.

estado <-
  c("leve", "grave", "sano", "sano", "leve", "sano", "sano", "grave",
    "grave", "leve", "grave", "sano", "sano")
estado
 [1] "leve"  "grave" "sano"  "sano"  "leve"  "sano"  "sano"  "grave" "grave"
[10] "leve"  "grave" "sano"  "sano" 

La variable estado actualmente es de tipo texto, de tipo chr, algo que podemos comprobar con class(estado).

class(estado)
[1] "character"

Paréntesis: factores

Desde un punto de vista estadístico y computacional, para R esta variable ahora mismo sería equivalente una variable de nombres. Pero estadísticamente no es lo mismo una variable con nombres (que identifican muchas veces el registro) que una variable categórica como estado que solo puede tomar esos 3 niveles. ¿Cómo convertir a factor?

Haciendo uso de la función as_factor() del paquete {forcats}.

library(tidyverse)
estado_fct <- tibble(paciente = 1:length(estado),
                     estado = as_factor(estado))
estado_fct
# A tibble: 13 × 2
   paciente estado
      <int> <fct> 
 1        1 leve  
 2        2 grave 
 3        3 sano  
 4        4 sano  
 5        5 leve  
 6        6 sano  
 7        7 sano  
 8        8 grave 
 9        9 grave 
10       10 leve  
11       11 grave 
12       12 sano  
13       13 sano  

Paréntesis: factores

No solo ha cambiado la clase de la variable sino que ahora, debajo del valor guardado, nos aparece la frase Levels: grave leve sano: son las modalidades o niveles de nuestra cualitativa.

 

Imagina que ese día en el hospital no tuviésemos a nadie en estado grave: aunque ese día nuestra variable no tome dicho valor, el estado grave es un nivel permitido en la base de datos, así que aunque lo eliminemos, por ser un factor, el nivel permanece (no lo tenemos ahora pero es un nivel permitido).

estado_fct |> 
  filter(estado %in% c("sano", "leve")) |> 
  pull(estado)
[1] leve sano sano leve sano sano leve sano sano
Levels: leve grave sano

Paréntesis: factores

Con factor() podemos especificar explícitamente los nombres de las modalidades, incluso si son nominales u ordinales

estado_fct <-
  tibble(paciente = 1:length(estado),
         estado = factor(estado, ordered = TRUE))
estado_fct |> pull(estado)
 [1] leve  grave sano  sano  leve  sano  sano  grave grave leve  grave sano 
[13] sano 
Levels: grave < leve < sano

Paréntesis: factores

Con levels = ... podemos indicarle explícitamente el orden de las modalidades

estado_fct <-
  tibble(paciente = 1:length(estado),
         estado = factor(estado,
                         levels = c("sano", "leve", "grave"),
                         ordered = TRUE))
estado_fct |> pull(estado)
 [1] leve  grave sano  sano  leve  sano  sano  grave grave leve  grave sano 
[13] sano 
Levels: sano < leve < grave

Paréntesis: factores

Si queremos indicarle que elimine un nivel no usado en ese momento (y que queremos excluir de la definición) podemos hacerlo con fct_drop()

estado_fct |> 
  filter(estado %in% c("sano", "leve")) |> 
  mutate(estado = fct_drop(estado)) |> 
  pull(estado)
[1] leve sano sano leve sano sano leve sano sano
Levels: sano < leve

Paréntesis: factores

Al igual que podemos eliminar niveles podemos ampliar los niveles existentes (aunque no existan datos de ese nivel en ese momento) con fct_expand()

estado_fct |> 
  mutate(estado = fct_expand(estado, c("UCI", "fallecido"))) |> 
  pull(estado)
 [1] leve  grave sano  sano  leve  sano  sano  grave grave leve  grave sano 
[13] sano 
Levels: sano < leve < grave < UCI < fallecido

Paréntesis: factores

Además con fct_explicit_na() podemos asignar un nivel a los valores para que sea incluido dicho nivel en los análisis y visualizaciones.

fct_explicit_na(factor(c("a", "b", NA)))
[1] a         b         (Missing)
Levels: a b (Missing)

Paréntesis: factores

Incluso una vez definidos podemos reordenar los níveles con fct_relevel()

estado_fct_expand <- 
  estado_fct |> 
  mutate(estado = fct_expand(estado, c("UCI", "fallecido"))) |> 
  pull(estado)

estado_fct_expand |>
  fct_relevel(c("fallecido", "leve", "sano", "grave", "UCI"))
 [1] leve  grave sano  sano  leve  sano  sano  grave grave leve  grave sano 
[13] sano 
Levels: fallecido < leve < sano < grave < UCI

Paréntesis: factores

Esta forma de trabajar con variables cualitativas nos permite dar una definición teórica de nuestra base de datos, pudiendo incluso contar valores que aún no existen (pero que podrían), haciendo uso de fct_count()

estado_fct |> 
  mutate(estado = fct_expand(estado, c("UCI", "fallecido"))) |> 
  pull(estado) |> 
  fct_count()
# A tibble: 5 × 2
  f             n
  <ord>     <int>
1 sano          6
2 leve          3
3 grave         4
4 UCI           0
5 fallecido     0

Paréntesis: factores

Los níveles también podemos ordenarlos por frecuencia con fct_infreq()

estado_fct |> 
  mutate(estado = fct_infreq(estado)) |> 
  pull(estado) |> 
  fct_count()
# A tibble: 3 × 2
  f         n
  <ord> <int>
1 sano      6
2 grave     4
3 leve      3

Paréntesis: factores

A veces querremos agrupar niveles, por ejemplo, no permitiendo niveles que no sucedan un mínimo de veces con fct_lump_min(.., min = ..) (las observaciones que no lo cumplan irán a un nivel genérico llamado Other, aunque se puede cambiar con el argumento other_level).

estado_fct |> 
  pull(estado) |> 
  fct_lump_min(min = 4)
 [1] Other grave sano  sano  Other sano  sano  grave grave Other grave sano 
[13] sano 
Levels: sano < grave < Other
estado_fct |> 
  pull(estado) |> 
  fct_lump_min(min = 4,
               other_level = "otros")
 [1] otros grave sano  sano  otros sano  sano  grave grave otros grave sano 
[13] sano 
Levels: sano < grave < otros

Paréntesis: factores

Podemos hacer algo equivalente pero en función de su frecuencia relativa con fct_lump_prop().

estado_fct |> 
  pull(estado) |> 
  fct_lump_prop(prop = 0.4,
                other_level = "otros")
 [1] otros otros sano  sano  otros sano  sano  otros otros otros otros sano 
[13] sano 
Levels: sano < otros

Paréntesis: factores

Esto lo podemos aplicar a nuestros conjuntos de datos para recategorizar variables de forma muy rápida.

starwars |> 
  drop_na(species) |> 
  mutate(species =
           fct_lump_min(species, min = 3,
                        other_level = "Otras")) |> 
  count(species)
# A tibble: 4 × 2
  species     n
  <fct>   <int>
1 Droid       6
2 Gungan      3
3 Human      35
4 Otras      39

Paréntesis: factores

Con fct_reorder() podemos también indicar que queremos ordenar los factores en función de una función aplicada a otra variable.

starwars_factor <- 
  starwars |> 
  drop_na(height, species) |> 
  mutate(species =
           fct_lump_min(species, min = 3,
                        other_level = "Otras"))
starwars_factor |> pull(species)
 [1] Human  Droid  Droid  Human  Human  Human  Human  Droid  Human  Human 
[11] Human  Human  Otras  Human  Otras  Otras  Human  Otras  Human  Human 
[21] Droid  Otras  Human  Human  Otras  Human  Otras  Otras  Human  Otras 
[31] Human  Human  Gungan Gungan Gungan Human  Otras  Otras  Human  Human 
[41] Otras  Otras  Otras  Otras  Otras  Otras  Otras  Human  Otras  Otras 
[51] Otras  Otras  Otras  Otras  Otras  Otras  Human  Otras  Otras  Otras 
[61] Human  Human  Human  Human  Otras  Otras  Otras  Otras  Human  Droid 
[71] Otras  Otras  Otras  Otras  Otras  Human  Otras 
Levels: Droid Gungan Human Otras
starwars_factor |>
  mutate(species = fct_reorder(species, height, mean)) |> 
  pull(species)
 [1] Human  Droid  Droid  Human  Human  Human  Human  Droid  Human  Human 
[11] Human  Human  Otras  Human  Otras  Otras  Human  Otras  Human  Human 
[21] Droid  Otras  Human  Human  Otras  Human  Otras  Otras  Human  Otras 
[31] Human  Human  Gungan Gungan Gungan Human  Otras  Otras  Human  Human 
[41] Otras  Otras  Otras  Otras  Otras  Otras  Otras  Human  Otras  Otras 
[51] Otras  Otras  Otras  Otras  Otras  Otras  Human  Otras  Otras  Otras 
[61] Human  Human  Human  Human  Otras  Otras  Otras  Otras  Human  Droid 
[71] Otras  Otras  Otras  Otras  Otras  Human  Otras 
Levels: Droid Otras Human Gungan

💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Dada la variable meses definida debajo (definida como un vector de caracteres), convierte dicha variable a factor (solo eso)

meses <- c("Ene", "Feb", "Mar", "Abr")
Code
meses <- c("Ene", "Feb", "Mar", "Abr")
meses_fct <- as_factor(meses)
meses_fct

📝 Dada la variable meses definida debajo convierte dicha variable a factor pero indicando los niveles de forma correcta.

meses <- c(NA, "Abr", "Ene", "Oct", "Jul", "Ene", "Sep", NA, "Feb", "Dic",
           "Jul", "Mar", "Ene", "Mar", "Feb", "Abr", "May", "Oct", "Sep",  NA,
           "Dic", "Jul", "Nov", "Feb", "Oct", "Jun", "Sep", "Oct", "Oct", "Sep")
Code
meses <- c(NA, "Abr", "Ene", "Oct", "Jul", "Ene", "Sep", NA, "Feb", "Dic",
           "Jul", "Mar", "Ene", "Mar", "Feb", "Abr", "May", "Oct", "Sep",  NA,
           "Dic", "Jul", "Nov", "Feb", "Oct", "Jun", "Sep", "Oct", "Oct", "Sep")

# Orden de niveles correcto e incluimos agosto aunque no haya
meses_fct <-
  factor(meses,
         levels = c("Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"))
meses_fct

📝 Cuenta cuantos valores hay de cada mes pero teniendo en cuenta que son factores (quizás haya niveles sin ser usados y de los que debería obtener un 0).

Code
meses_fct |> fct_count()

📝 Dado que hay ausentes, indica que los ausentes sea un decimotercer nivel etiquetado como “ausente”.

Code
meses_fct <- 
  meses_fct |>
  fct_explicit_na(na_level = "ausente")
meses_fct

📝 Elimina los niveles no usados.

Code
meses_fct <- 
  meses_fct |>
  fct_drop()
meses_fct

📝 Ordena los niveles por frecuencia de aparición.

Code
meses_fct |> 
  fct_infreq()

📝 Agrupa niveles de forma que todo nivel que no aparezca al menos el 7% de las veces se agrupe en un nivel llamado “otros meses”

Code
meses_fct <-
  meses_fct |> 
  fct_lump_prop(prop = 0.07, other_level = "otros")
meses_fct

Cualis: barras ordenadas

Haciendo uso de los que sabemos sobre factores podemos indicarle que nos ordene las columnas de manera personalizada definiendo la variable cuali como un factor.

starwars |> 
  drop_na(sex) |>
  mutate(sex = factor(sex, levels = c("female", "male", "hermaphroditic", "none"))) |> 
    ggplot(aes(x = sex)) +
    geom_bar(aes(fill = sex), alpha = 0.5) + 
    scale_fill_colorblind() +
    labs(x = "Sexo", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
         subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
         caption = "J. Álvarez Liébana")

Cualis: barras ordenadas

También podemos indicarle que nos ordene las columnas de mayor a menor frecuencia usando simplemente fct_infreq()

starwars |> 
    drop_na(sex) |>
  mutate(sex = factor(sex, levels = c("female", "male", "hermaphroditic", "none"))) |> 
    ggplot(aes(x = fct_infreq(sex))) +
    geom_bar(aes(fill = sex), alpha = 0.5) + 
    scale_fill_colorblind() +
    labs(x = "Sexo", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
         subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
         caption = "J. Álvarez Liébana")

Cualis: barras ordenadas

Para invertir el orden de los factores basta usar fct_rev()

starwars |> 
    drop_na(sex) |>
  mutate(sex = factor(sex, levels = c("female", "male", "hermaphroditic", "none"))) |> 
    ggplot(aes(x = fct_rev(fct_infreq(sex)))) +
    geom_bar(aes(fill = sex), alpha = 0.5) + 
    scale_fill_colorblind() +
    labs(x = "Sexo", y = "frecuencia absoluta", fill = "sexo", title = "Primer diagrama de barras",
         subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
         caption = "J. Álvarez Liébana")

Cualis: geom_col()

La capa geom_bar() está solo pensada para conteos de variables discretas o cualitativas. ¿Y si queremos visualizar en el peso por sexo?

Usaremos geom_col() (ahora si necesitamos x,y)

starwars |> 
  drop_na(mass, sex) |>
  ggplot(aes(x = sex, y = mass)) +
  geom_col(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  labs(x = "Sexo", y = "Peso", fill = "sexo", title = "Primer diagrama de columnas",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente", caption = "J. Álvarez Liébana")

Cualis: geom_col()

Fíjate que por defecto lo que hace es sumar la variable continua. ¿Cómo pedir que visualice, por ejemplo, la media por grupos?

La forma más inmediata es hacer un geom_col() pero en lugar de a la tabla original a un resumen de la misma.

starwars |> 
  drop_na(mass, sex) |> 
  summarise(mean_mass = mean(mass), .by = sex) |> 
  ggplot(aes(x = sex, y = mean_mass)) +
  geom_col(aes(fill = sex), alpha = 0.5) + 
  scale_fill_colorblind() +
  labs(x = "Sexo", y = "Peso (medio)", fill = "sexo", title = "Primer diagrama de columnas",
       subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente", caption = "J. Álvarez Liébana")

Cualis: geom_col()

Otra opción es no usar la capa geométrica sino la capa estadística, con stat_summary() e indicándole la función a visualizar y el geometría

starwars |> 
    drop_na(mass, sex) |>
  mutate(sex = factor(sex, levels = c("female", "male", "hermaphroditic", "none"))) |>
    ggplot(aes(x = sex, y = mass, fill = sex)) +
    stat_summary(geom = "col", fun = mean, alpha = 0.5) + 
    scale_fill_colorblind() +
    labs(x = "Sexo", y = "Peso (medio)", fill = "sexo",
         title = "Primer diagrama de columnas",
         subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
         caption = "J. Álvarez Liébana")

Cualis: geom_col()

Fíjate que ambas formas nos permiten visualizar cualquier otro estadístico, por ejempo, la mediana

starwars |> 
    drop_na(mass, sex) |>
  mutate(sex = factor(sex, levels = c("female", "male", "hermaphroditic", "none"))) |>
    ggplot(aes(x = sex, y = mass, fill = sex)) +
    stat_summary(geom = "col", fun = median, alpha = 0.5) + 
    scale_fill_colorblind() +
    labs(x = "Sexo", y = "Peso (mediana)", fill = "sexo",
         title = "Primer diagrama de columnas",
         subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
         caption = "J. Álvarez Liébana")

Cualis: geom_col()

La última opción es volver a nuestra conocida geom_bar(), indicándole stat = "summary", fun = "mean", por ejemplo (por defecto stat = "count") con ahora sí dos variables

starwars |> 
    drop_na(mass, sex) |>
  mutate(sex = factor(sex, levels = c("female", "male", "hermaphroditic", "none"))) |>
    ggplot(aes(x = sex, y = mass)) +
    geom_bar(aes(fill = sex), alpha = 0.5,
             stat = "summary", fun = "mean") + 
    scale_fill_colorblind() +
    labs(x = "Sexo", y = "Peso (media)", fill = "sexo",
         title = "Primer diagrama de columnas",
         subtitle = "Sexos: femenino, masculino, hemafrodita, ninguno y ausente",
         caption = "J. Álvarez Liébana")

Imitando a Nightingale

Vamos a intentar replicar el famoso gráfico de rosa o diagrama de área polar de Florence Nightingale, cargando los datos de {HistData}. Los datos representan, por meses, las diferentes causas de mortalidad de los soldados ingleses en la Guerra de Crimea (busca en la ayuda para saber qué es cada cosa)

library(HistData)
datos <- as_tibble(Nightingale)

Imitando a Nightingale

  1. Filtra solo las variables relativas a fecha y las relativadas a tasas de mortalidad (por cada 1000 habitantes), aquellas que acaban por ".rate". Tras ello prepara los datos de manera adecuada para su visualización (todo en castellano)
Code
datos_filtrados <-
  datos |> 
  select(Date:Year, contains("rate")) |> 
  pivot_longer(cols = contains("rate"),
               names_to = "causa",
               values_to = "tasa") |> 
  rename(fecha = Date, mes = Month, anno = Year) |> 
  mutate(causa =
           case_when(causa == "Disease.rate" ~ "infecciosas",
                     causa == "Wounds.rate" ~ "heridas",
                     TRUE ~ "otras"))
datos_filtrados
# A tibble: 72 × 5
   fecha      mes    anno causa        tasa
   <date>     <ord> <int> <chr>       <dbl>
 1 1854-04-01 Apr    1854 infecciosas   1.4
 2 1854-04-01 Apr    1854 heridas       0  
 3 1854-04-01 Apr    1854 otras         7  
 4 1854-05-01 May    1854 infecciosas   6.2
 5 1854-05-01 May    1854 heridas       0  
 6 1854-05-01 May    1854 otras         4.6
 7 1854-06-01 Jun    1854 infecciosas   4.7
 8 1854-06-01 Jun    1854 heridas       0  
 9 1854-06-01 Jun    1854 otras         2.5
10 1854-07-01 Jul    1854 infecciosas 150  
# ℹ 62 more rows

Imitando a Nightingale

  1. Realiza las transformaciones en los datos que consideres y replica el gráfico.

Imitando a Nightingale

Code
datos_filtrados <-
  datos_filtrados |>
  mutate(periodo = ifelse(fecha >= "1855-04-01", "Abril 1855 - Marzo 1856", "Abril 1854 - Marzo 1855"),
         periodo =
           factor(periodo, levels = c("Abril 1854 - Marzo 1855", "Abril 1855 - Marzo 1856"),
                  ordered = TRUE))

theme_set(theme_minimal())
ggplot(datos_filtrados, aes(x = mes, y = tasa, fill = causa)) + 
  geom_col(alpha = 0.85) +
  scale_fill_manual(values = c("#e3aeae", "#a5acb0", "#594b4a")) +
  facet_wrap(~periodo) +
  labs(fill = "Causas", title = "Causas de mortalidad",
       subtitle = "Periodos: Abril 1854 - Marzo 1855 y Abril 1855 - Marzo 1856",
       caption = "Autor: J. Álvarez Liébana | Data: {HistData}")

Imitando a Nightingale

  1. Nuestros datos abarcan dos periodos: de abril 1854 a marzo 1855, y de abril 1855 a marzo 1856. Realiza los cambios necesarios para obtener el siguiente gráfico.

Imitando a Nightingale

Code
datos_filtrados <-
  datos_filtrados |>
  mutate(periodo = ifelse(fecha >= "1855-04-01", "Abril 1855 - Marzo 1856", "Abril 1854 - Marzo 1855"),
         periodo =
           factor(periodo, levels = c("Abril 1854 - Marzo 1855", "Abril 1855 - Marzo 1856"),
                  ordered = TRUE),
         mes = fct_relevel(mes, "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
                           "Dec", "Jan", "Feb", "Mar"))

theme_set(theme_minimal())
ggplot(datos_filtrados, aes(x = mes, y = tasa, fill = causa)) + 
  geom_col(alpha = 0.85) +
  scale_fill_manual(values = c("#e3aeae", "#a5acb0", "#594b4a")) +
  facet_wrap(~periodo) +
  labs(fill = "Causas", title = "Causas de mortalidad",
       subtitle = "Periodos: Abril 1854 - Marzo 1855 y Abril 1855 - Marzo 1856",
       caption = "Autor: J. Álvarez Liébana | Data: {HistData}")

Imitando a Nightingale

  1. Piensa que cambio hemos realizado en el siguiente gráfico. ¿Qué se ha cambiado en las coordenadas? ¿Tenemos alguna capa que pueda ayudarnos a realizarlo? Investiga

Imitando a Nightingale

Code
theme_set(theme_minimal())
ggplot(datos_filtrados, aes(x = mes, y = tasa, fill = causa)) + 
  geom_col(alpha = 0.85) +
  scale_fill_manual(values = c("#e3aeae", "#a5acb0", "#594b4a")) +
  coord_polar() +
  facet_wrap(~periodo) +
  labs(fill = "Causas", title = "Causas de mortalidad",
       subtitle = "Periodos: Abril 1854 - Marzo 1855 y Abril 1855 - Marzo 1856",
       caption = "Autor: J. Álvarez Liébana | Data: {HistData}")

Imitando a Nightingale

  1. Ahora mismo se notan mucho las diferencias entre gajos. ¿Cómo podríamos reducir esa diferencia? ¿Qué tipo de cambio deberíamos realizar (qué tipo de capa usar)?

Imitando a Nightingale

Code
theme_set(theme_minimal())
ggplot(datos_filtrados, aes(x = mes, y = tasa, fill = causa)) + 
  geom_col(alpha = 0.85) +
  scale_fill_manual(values = c("#e3aeae", "#a5acb0", "#594b4a")) +
  coord_polar() +
  scale_y_sqrt() +
  facet_wrap(~periodo) +
  labs(fill = "Causas", title = "Causas de mortalidad",
       subtitle = "Periodos: Abril 1854 - Marzo 1855 y Abril 1855 - Marzo 1856",
       caption = "Autor: J. Álvarez Liébana | Data: {HistData}")

Imitando a Nightingale

  1. Dado que en el gráfico original no hay marcas en el eje Y, vamos eliminar el eje Y (sin título, sin textos, sin marcas). ¿Qué habría que cambiar? Investiga en theme() Situa además la leyenda en la parte inferior

Imitando a Nightingale

Code
theme_set(theme_minimal())
theme_update(axis.title.y = element_blank(),
             axis.text.y = element_blank(),
             axis.ticks.y = element_blank(),
             legend.position = "bottom")

Imitando a Nightingale

  1. Para que sea más legible vamos a reducir el tamaño de las etiquetas de los meses y vamos a darle etiquetas correctas a los meses (July en lugar de Jul, December en lugar de Dec, etc).

Imitando a Nightingale

Code
theme_set(theme_minimal())
theme_update(axis.title.y = element_blank(), axis.text.y = element_blank(),
             axis.ticks.y = element_blank(), axis.text.x = element_text(size = 6, face = "bold"),
             legend.position = "bottom")

... + 
  scale_x_discrete(labels =
                     c("JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER",
                       "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE"))

🐣 Caso práctico 14 a: ggplot

Usa el dataset gapminder y visualiza en un gráfico la media de la variable gdpPercap por continente y año mediante un diagrama de barras en 3 formas:

  • barras solapadas
  • barras apiladas
  • barras fill

🐣 Caso práctico 14 b: ggplot

El objetivo es analizar un conjunto de datos que contiene las respuestas a las pregunta «¿Qué probabilidad (%) asignarías al término (entre otros) …

  • «almost no chance»
  • «probable»
  • «almost certainly»

…con el objetivo de comprender cómo la gente percibe el vocabulario de la probabilidad.

datos <-
  read_csv("https://raw.githubusercontent.com/zonination/perceptions/master/probly.csv")
datos
# A tibble: 46 × 17
   `Almost Certainly` `Highly Likely` `Very Good Chance` Probable Likely
                <dbl>           <dbl>              <dbl>    <dbl>  <dbl>
 1                 95              80                 85       75     66
 2                 95              75                 75       51     75
 3                 95              85                 85       70     75
 4                 95              85                 85       70     75
 5                 98              95                 80       70     70
 6                 95              99                 85       90     75
 7                 85              95                 65       80     40
 8                 97              95                 75       70     70
 9                 95              95                 80       70     65
10                 90              85                 90       70     75
# ℹ 36 more rows
# ℹ 12 more variables: Probably <dbl>, `We Believe` <dbl>,
#   `Better Than Even` <dbl>, `About Even` <dbl>, `We Doubt` <dbl>,
#   Improbable <dbl>, Unlikely <dbl>, `Probably Not` <dbl>,
#   `Little Chance` <dbl>, `Almost No Chance` <dbl>, `Highly Unlikely` <dbl>,
#   `Chances Are Slight` <dbl>

🐣 Caso práctico 14 b: ggplot

datos <-
  read_csv("https://raw.githubusercontent.com/zonination/perceptions/master/probly.csv")
datos
# A tibble: 46 × 17
   `Almost Certainly` `Highly Likely` `Very Good Chance` Probable Likely
                <dbl>           <dbl>              <dbl>    <dbl>  <dbl>
 1                 95              80                 85       75     66
 2                 95              75                 75       51     75
 3                 95              85                 85       70     75
 4                 95              85                 85       70     75
 5                 98              95                 80       70     70
 6                 95              99                 85       90     75
 7                 85              95                 65       80     40
 8                 97              95                 75       70     70
 9                 95              95                 80       70     65
10                 90              85                 90       70     75
# ℹ 36 more rows
# ℹ 12 more variables: Probably <dbl>, `We Believe` <dbl>,
#   `Better Than Even` <dbl>, `About Even` <dbl>, `We Doubt` <dbl>,
#   Improbable <dbl>, Unlikely <dbl>, `Probably Not` <dbl>,
#   `Little Chance` <dbl>, `Almost No Chance` <dbl>, `Highly Unlikely` <dbl>,
#   `Chances Are Slight` <dbl>

Solo haciendo uso de los gráficos aprendidos hasta ahora, ¿cómo visualizarías dicho dataset? Realiza las tranformaciones que consideres para una correcta preparación de los datos.

Clase 15: dataviz

Visualización de datos

Variables continuas

Solo haciendo uso de los gráficos aprendidos hasta ahora, ¿cómo visualizarías dicho dataset? Realiza las tranformaciones que consideres para una correcta preparación de los datos.

datos <-
  read_csv("https://raw.githubusercontent.com/zonination/perceptions/master/probly.csv")

Lo primero que deberemos hacer es preparar nuestros datos para la posterior visualización en formato tidy

datos_tidy <-
  datos |> 
  pivot_longer(cols = everything(),
               names_to = "termino", values_to = "prob")
datos_tidy
# A tibble: 782 × 2
   termino           prob
   <chr>            <dbl>
 1 Almost Certainly    95
 2 Highly Likely       80
 3 Very Good Chance    85
 4 Probable            75
 5 Likely              66
 6 Probably            75
 7 We Believe          66
 8 Better Than Even    55
 9 About Even          50
10 We Doubt            40
# ℹ 772 more rows

Variables continuas

¿Cómo podemos visualizar estos datos?

Tenemos dos variables:

  • termino: cualitativa ordinal
  • prob: cuantitativa continua

Por lo que de momento no tenemos herramienta para visualizarlo ya que

  • scatter plot: dos variables continuas.
  • diagrama de barras: variables discretas o cualitativas

La única manera será realizar un resumen de los datos visualizando, por ejemplo, la media de probabilidad asignada

Variables continuas

La única manera será realizar un resumen de los datos visualizando, por ejemplo, la media de probabilidad asignada

resumen <- 
  datos_tidy |>
  summarise(mean_prob = mean(prob), .by = termino)
resumen
# A tibble: 17 × 2
   termino            mean_prob
   <chr>                  <dbl>
 1 Almost Certainly       92.6 
 2 Highly Likely          86.2 
 3 Very Good Chance       79.8 
 4 Probable               71.5 
 5 Likely                 72   
 6 Probably               71.5 
 7 We Believe             68.5 
 8 Better Than Even       58.4 
 9 About Even             49.6 
10 We Doubt               27.9 
11 Improbable             18.0 
12 Unlikely               19.9 
13 Probably Not           29.5 
14 Little Chance          16.0 
15 Almost No Chance        5.63
16 Highly Unlikely        10.1 
17 Chances Are Slight     14.1 

Variables continuas

Con scale_fill_gradient2() vamos a crear un gradiente de color, y con scale_y_continuous() incorporamos % en el eje Y.

ggplot(resumen, aes(x = termino, y = mean_prob, fill = mean_prob)) +
  geom_col(alpha = 0.8) +
  scale_fill_gradient2(low = "#DA4A4A", mid = "#FEFADF", high = "#144F8D", midpoint = 50) +
  scale_y_continuous(labels = scales::label_number(suffix = "%")) +
  labs(fill = "Prob. media", x = "Términos", y = "Probabilidad media",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Variables continuas

Para mejorar la legibilidad vamos a reducir el tamaño de las etiquetas del eje X.

ggplot(resumen, aes(x = termino, y = mean_prob, fill = mean_prob)) +
  geom_col(alpha = 0.8) +
  scale_fill_gradient2(low = "#DA4A4A", mid = "#FEFADF", high = "#144F8D", midpoint = 50) +
  scale_y_continuous(labels = scales::label_number(suffix = "%")) +
  labs(fill = "Prob. media", x = "Términos", y = "Probabilidad media",
       title = "Percepción de la probabilidad") +
  theme_minimal() +
  theme(axis.text.x = element_text(size = 6, angle = 30))

Variables continuas

Por último, vamos a ordenar las barras de más a menos

ggplot(resumen |> mutate(termino = fct_reorder(termino, mean_prob)), aes(x = termino, y = mean_prob, fill = mean_prob)) +
  geom_col(alpha = 0.8) +
  scale_fill_gradient2(low = "#DA4A4A", mid = "#FEFADF", high = "#144F8D", midpoint = 50) +
  scale_y_continuous(labels = scales::label_number(suffix = "%")) +
  labs(fill = "Prob. media", x = "Términos", y = "Probabilidad media",
       title = "Percepción de la probabilidad") +
  theme_minimal() +
  theme(axis.text.x = element_text(size = 6, angle = 30))

Variables continuas

Aun así al haber sumarizado, estamos perdiendo información…

¿Qué otros gráficos se te ocurren para hacer con variables continuas?

Algunos de los más habituales son:

  • Distribuciones: histogramas, densidades (ridgeline), boxplot (alternativas: violín, raincloud plots)
  • Evolución: gráficos de líneas, gráficos de área, series temporales, etc
  • Correlaciones: mapas de calor, correlograma, grafos, etc

Distribuciones: histogramas

Nuestra primera alternativa será el conocido como histograma con geom_histogram()

Fíjate que está realizando el histograma de todo el dataset.

ggplot(datos_tidy, aes(x = prob)) +
  geom_histogram(alpha = 0.8, fill = "#144F8D") +
  labs(x = "Probabilidad", y = "Frecuencia",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: histogramas

El argumento bins = ... nos servirá para personalizar el nímero de barras que queremos. Fíjate que el gráfico es una proximación discreta de un gráfico de densidad.

ggplot(datos_tidy, aes(x = prob)) +
  geom_histogram(bins = 12, alpha = 0.8, fill = "#144F8D") +
  labs(x = "Probabilidad", y = "Frecuencia",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: histogramas

Si queremos hacer uno por término, basta con añadir a nuestro gráfico un facet_wrap() para componer

ggplot(datos_tidy, aes(x = prob, fill = termino)) +
  geom_histogram(bins = 12, alpha = 0.8) +
  facet_wrap(~termino, scale = "free_y", ncol = 4) +
  labs(x = "Probabilidad", y = "Frecuencia",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: histogramas

Vamos a filtrar solo algunos términos para poder usar la paleta scale_fill_brewer()

datos_tidy <-
  datos_tidy |> 
  filter(!(termino %in% c("Chances Are Slight", "Improbable", "Probably Not", "Probable", "Likely", "Very Good Chance")))

ggplot(datos_tidy, aes(x = prob, fill = termino)) +
  geom_histogram(bins = 12, alpha = 0.8) +
  scale_fill_brewer(palette = "RdBu") +
  facet_wrap(~termino, scale = "free_y", ncol = 4) +
  labs(x = "Probabilidad", y = "Frecuencia",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: histogramas

Nos aparecen desordenadas así que de nuevo podemos hacer uso del paquete {forcats}

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)), aes(x = prob, fill = termino)) +
  geom_histogram(bins = 12, alpha = 0.8) +
  scale_fill_brewer(palette = "RdBu") +
  facet_wrap(~termino, scale = "free_y", ncol = 4) +
  labs(x = "Probabilidad", y = "Frecuencia",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Paréntesis: tema y fuente

Vamos a añadir una fuente personaliza al gráfico anterior.

Con el paquete {showtext} podemos cargar fuentes de https://fonts.google.com/: con font_add_google() añadimos la fuente y con showtext_auto() habilitamos su uso.

Con theme_set() podemos fijar un tema base (en nuestro caso theme_minimal(base_family = ...)) y con theme_update() añadimos el resto de personalizaciones

library(showtext)
font_add_google(family = "Roboto", name = "Roboto")
showtext_auto()

theme_set(theme_minimal(base_family = "Roboto"))
theme_update(
  plot.title = element_text(color = "black", face = "bold", size = 27),
  legend.position = "bottom")

Distribuciones: densidades

Los histogramas en realidad son una aproximación discreta de los gráficos de densidad (asumiendo que los intervalos se pudieran ir haciendo tan pequeños como queramos).

Las densidades mejoran la robustez al histograma. Para ello usaremos geom_density()

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)), aes(x = prob, fill = termino)) +
  geom_density(alpha = 0.8) +
  scale_fill_brewer(palette = "RdBu") +
  facet_wrap(~termino, scale = "free_y", ncol = 4) +
  labs(x = "Probabilidad", y = "Frecuencia relativa",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: densidades

A veces puede ser interesante [superponer las densidades], lo cual lo podemos hacer con geom_density_ridges() del paquete {ggridges} (ahora sí necesitamos indicarle un y = ...)

library(ggridges)
ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = prob, y = termino, fill = termino, color = termino)) +
  geom_density_ridges(alpha = 0.5) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Probabilidad", y = "Términos",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: boxplot

Una opción muy habitual en variables continuas son los gráficos de cajas y bigotes o boxplots

Para realizar estos gráficos debemos usar la geometría geom_boxplot()

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_boxplot(alpha = 0.8) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: jitter

Los boxplot a veces pueden ser insuficiente y podemos usar geom_jitter() que nos añadirá puntos como un «gotelé aleatorio» (ver https://hausetutorials.netlify.app/posts/2019-02-22-why-we-should-never-use-barplots-use-geomquasirandom-instead/)

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_boxplot(alpha = 0.8) +
  geom_jitter(alpha = 0.3, size = 2) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: jitter

Si te fijas los outliers aparecen dos veces ya que el boxplot los marca. Dentro de geom_boxplot() podemos indicarle la forma, color y alpha de los atípicos.

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_boxplot(alpha = 0.8, outlier.shape = 23) +
  geom_jitter(alpha = 0.3, size = 2) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: jitter

Si te fijas los outliers aparecen dos veces ya que el boxplot los marca. Dentro de geom_boxplot() podemos indicarle la forma, color y alpha de los atípicos.

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_boxplot(alpha = 0.8, outlier.alpha = 0) +
  geom_jitter(alpha = 0.3, size = 2) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: geom_quasirandom

Podemos mejorar el «gotelé aleatorio» con geom_quasirandom() del paquete {ggbeeswarm} (con width = ... controlamos la anchura de lo aleatorio)

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_boxplot(alpha = 0.8, outlier.alpha = 0) +
  geom_quasirandom(size = 2, alpha = 0.4,  width = 0.7) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: violin

Para solventar los problemas de los box-plots, una alternativa muy popular son los gráficos de violín (en realidad es una densidad reflejada)

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_violin(alpha = 0.8) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Distribuciones: violin

Con el argumento scale = "count" las àreas son proporcionales al número de observaciones en cada violín (por defecto scale = "area", todos la misma área). Con bw = ... modulamos la suavidad del kernel usado (bandwidth).

ggplot(datos_tidy |> 
       mutate(termino = fct_reorder(termino, prob, .fun = mean)),
       aes(x = termino, y = prob, fill = termino, color = termino)) +
  geom_violin(alpha = 0.8, scale = "count", bw = 1.5) +
  scale_fill_brewer(palette = "RdBu") +
  scale_color_brewer(palette = "RdBu") +
  guides(color = "none") +
  labs(x = "Términos", y = "Probabilidad",
       title = "Percepción de la probabilidad") +
  theme_minimal()

Clase 16: dataviz

Visualización de datos

Evolución: gráficos de líneas

Otra categoría muy común de gráficos con variables continusa son los gráficos de evolución

El más simple es el [gráfico de líneas] {.hl-yellow}, que podemos construir con geom_line(), y para el que ahora sí necesitamos un x = ... y un y = ...

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)), aes(x = year, y = mean_gdp, color = continent)) +
  geom_line(alpha = 0.8, linewidth = 2) +
  scale_y_continuous(labels = scales::label_dollar()) +
  scale_color_colorblind() +
  labs(x = "Año", y = "Renta per cápita media",
       title = "Evolución en gapminder") +
  theme_minimal()

Evolución: gráficos de líneas

Fíjate que dando color = ... nos hace solo una gráfica por variable de grupo. Si usamos geom_step() en su lugar obtenemos un gráfico de escalera

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)), aes(x = year, y = mean_gdp, color = continent)) +
  geom_step(alpha = 0.8, linewidth = 1.2) +
  scale_y_continuous(labels = scales::label_dollar()) +
  scale_color_colorblind() +
  labs(x = "Año", y = "Renta per cápita media",
       title = "Evolución en gapminder") +
  theme_minimal()

Evolución: serie temporal

Un gráfico de línea muy particular son las series temporales, donde en el eje X hay una variable de fecha y/o hora

Por ejemplo, vamos a cargar el siguiente dataset de Github de la evolución del precio de bitcoins cuyo separado es el espacio

data <- read_table(file = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered.csv")
data
# A tibble: 1,822 × 2
   date       value
   <date>     <dbl>
 1 2013-04-28  136.
 2 2013-04-29  147.
 3 2013-04-30  147.
 4 2013-05-01  140.
 5 2013-05-02  126.
 6 2013-05-03  108.
 7 2013-05-04  115 
 8 2013-05-05  119.
 9 2013-05-06  125.
10 2013-05-07  113.
# ℹ 1,812 more rows

Evolución: serie temporal

La forma más sencilla es de nuevo usar geom_line(). Con scale_x_date(date_breaks = ...) podemos indicarle los saltos en las fechas de manera sencilla.

ggplot(data, aes(x = date, y = value)) +
  geom_line(alpha = 0.8, color = "#145412", linewidth = 1.2) +
  scale_x_date(date_breaks = "4 months") +
  scale_y_continuous(labels = scales::label_dollar()) +
  scale_color_colorblind() +
  labs(x = "Fecha", y = "Precio del bitcoin",
       title = "Evolución del precio del bitcoin") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 30))

Evolución: gráficos de área

Una mejora de los gráficos de línea son los gráficos de área (visualizando la curva con rellena)

La forma más sencilla es de nuevo usar geom_line() pero añadiendo la capa geom_area() (con fill en lugar de color)

ggplot(data, aes(x = date, y = value)) +
  geom_line(color = "#145412", linewidth = 1) +
  geom_area(alpha = 0.4, fill = "#145412") +
  scale_x_date(date_breaks = "4 months") +
  scale_y_continuous(labels = scales::label_dollar()) +
  labs(x = "Fecha", y = "Precio del bitcoin",
       title = "Evolución del precio del bitcoin") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 30))

Paréntesis: interactivos

Todo gráfico ggplot podemos hacerlo interactivo guardándonos la gráfico y haciendo uso de {plotly}

gg <-
  ggplot(data, aes(x = date, y = value)) +
  geom_line(color = "#145412", linewidth = 1) +
  geom_area(alpha = 0.4, fill = "#145412") +
  scale_x_date(date_breaks = "4 months") +
  scale_y_continuous(labels = scales::label_dollar()) +
  labs(x = "Fecha", y = "Precio del bitcoin",
       title = "Evolución del precio del bitcoin") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 30))
plotly::ggplotly(gg)

Evolución: áreas apiladas

Los gráficas de áreas, al igual que sucedía con los diagrmaas de barras, pueden ser de áreas apiladas, haciendo que fill() sea mapeado por aes()

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)), aes(x = year, y = mean_gdp, fill = continent)) +
  geom_area(alpha = 0.7) +
  scale_y_continuous(labels = scales::label_dollar()) +
  scale_fill_colorblind() +
  labs(x = "Año", y = "Renta per cápita media",
       title = "Evolución de gapminder") +
  theme_minimal()

Evolución: áreas apiladas

Los gráficas de áreas, al igual que sucedía con los diagrmaas de barras, pueden ser de áreas apiladas, haciendo que fill() sea mapeado por aes()

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)), aes(x = year, y = mean_gdp, fill = continent)) +
  geom_area(alpha = 0.7) +
  scale_y_continuous(labels = scales::label_dollar()) +
  scale_fill_colorblind() +
  labs(x = "Año", y = "Renta per cápita media",
       title = "Evolución de gapminder") +
  theme_minimal()

Evolución: áreas apiladas

Haciendo una modificación en el preprocesamiento podemos hacer un gráfico de áreas apiladas en relativo

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)) |> mutate(porc = 100 * mean_gdp/sum(mean_gdp), .by = year),
       aes(x = year, y = porc, fill = continent)) +
  geom_area(alpha = 0.7) +
  scale_y_continuous(labels = scales::label_number(suffix = "%")) +
  scale_fill_colorblind() +
  labs(x = "Año", y = "Renta per cápita mundial",
       title = "Evolución de gapminder") +
  theme_minimal()

Evolución: streamcharts

Una modificación de los gráficos de áreas apiladas son los conocidos como streamcharts

En ellos las formas son más suaves que en un gráfico de área al uso, con el paquete {ggstream} (y usando geom_stream())

library(ggstream)
ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)),  aes(x = year, y = mean_gdp, fill = continent)) +
  geom_stream(alpha = 0.7) +
  scale_y_continuous(labels = scales::label_dollar()) +
  scale_fill_colorblind() +
  labs(x = "Año", y = "Renta per cápita mundial",
       title = "Evolución de gapminder") +
  theme_minimal()

Evolución: streamcharts

Fíjate que por defecto lo hace en espejo, usando el eje y de manera reflejada. Con type = "ridge" lo haemos de manera apilada.

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)),  aes(x = year, y = mean_gdp, fill = continent, color = continent)) +
  geom_stream(alpha = 0.75, type = "ridge") +
  scale_fill_colorblind() +
  scale_color_colorblind() +
  scale_y_continuous(labels = scales::label_dollar()) +
  labs(x = "Año", y = "Renta per cápita mundial",
       title = "Evolución de gapminder") +
  theme_minimal()

Evolución: streamcharts

Con type = "proportional" lo haemos de manera relativa

ggplot(gapminder |> summarise(mean_gdp = mean(gdpPercap), .by = c(continent, year)),  aes(x = year, y = mean_gdp, fill = continent, color = continent)) +
  geom_stream(alpha = 0.75, type = "proportional") +
  scale_fill_colorblind() +
  scale_color_colorblind() +
  scale_y_continuous(labels = scales::label_dollar()) +
  labs(x = "Año", y = "Renta per cápita mundial",
       title = "Evolución de gapminder") +
  theme_minimal()

🐣 Caso práctico 16: visualizando Netflix

Visualizaremos el número de películas y series de instituto que se han estrenado en Netflix en cada año. Los datos provienen originalmente de Kaggle, y contienen las películas y series de Netflix hasta enero de 2021.

netflix <-
  read_csv('https://raw.githubusercontent.com/elartedeldato/datasets/main/netflix_titles.csv')
netflix
# A tibble: 7,787 × 12
   show_id type    title director   cast  country date_added release_year rating
   <chr>   <chr>   <chr> <chr>      <chr> <chr>   <chr>             <dbl> <chr> 
 1 s1      TV Show 3%    <NA>       João… Brazil  August 14…         2020 TV-MA 
 2 s2      Movie   7:19  Jorge Mic… Demi… Mexico  December …         2016 TV-MA 
 3 s3      Movie   23:59 Gilbert C… Tedd… Singap… December …         2011 R     
 4 s4      Movie   9     Shane Ack… Elij… United… November …         2009 PG-13 
 5 s5      Movie   21    Robert Lu… Jim … United… January 1…         2008 PG-13 
 6 s6      TV Show 46    Serdar Ak… Erda… Turkey  July 1, 2…         2016 TV-MA 
 7 s7      Movie   122   Yasir Al … Amin… Egypt   June 1, 2…         2019 TV-MA 
 8 s8      Movie   187   Kevin Rey… Samu… United… November …         1997 R     
 9 s9      Movie   706   Shravan K… Divy… India   April 1, …         2019 TV-14 
10 s10     Movie   1920  Vikram Bh… Rajn… India   December …         2008 TV-MA 
# ℹ 7,777 more rows
# ℹ 3 more variables: duration <chr>, listed_in <chr>, description <chr>

🐣 Caso práctico 16: visualizando Netflix

  • Paso 1: piensa como filtrar las películas y series que van sobre un instituto.
  • Paso 2: tras dicho filtro, añade el año en el que se estrenó y elimina aquellas sin año conocido.
  • Paso 3: obtén el número de series por año

🐣 Caso práctico 16: visualizando Netflix

  • Paso 4: replica el siguiente gráfico

🐣 Caso práctico 16: visualizando Netflix

  • Paso 5: replica el siguiente gráfico sabiendo que la fuente del título es “Bebas Neue” y la del subtítulo “Permanent Marker”

🐣 Caso práctico 16: visualizando Netflix

  • Paso 6: replica el siguiente gráfico sabiendo que la fuente de los ejes es “Permanent Marker” y busca la función annotate()

Clase n + 1: joins, expresiones de control, listas y depuración

Cruzando datos. Bucles

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, una herramienta que nos va a permitir cruzar una o variables 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

tabla_1 |>
  xxx_join(tabla_2, by = id)
  • inner_join(): solo sobreviven los registros con id en ambas tablas.

  • full_join(): mantiene todos los registros de ambas tablas.

  • 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).

  • right_join(): mantiene todos los registros de la segunda tabla, y busca cuales tienen id también en la primera.

Relacionando datos

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

Imagina que queremos incorporar a tb_1 la información de la tabla_2, identificando los registros por la columna key (indicando con by = "key" la columna por la que tiene que cruzar): queremos mantener todos los registros de la primera tabla y buscar cuales tienen id (mismo valor en key) también en la segunda tabla.

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

El right_join() realizará la operación contraria: vamos ahora a incorporar a tb_2 la información de la tabla_2, identificando los registros por la columna key (indicando con by = "key" la columna por la que tiene que cruzar): queremos mantener todos los registros de la segunda y buscar cuales tienen id (mismo valor en key) también en la primera tabla.

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

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

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 la tabla de aviones.

Code
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).

Code
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í

Code
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.

Code
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

Code
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

Code
flights |> 
  drop_na(tailnum) |>
  anti_join(planes, by = "tailnum") |>
  count(tailnum, sort = TRUE) # de mayor a menor ya de paso

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 bno 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
  • lo que sucede cuando no
  • un argumento opcional para cuando la condición a evaluar es NA

Por ejemplo, vamos a etiquetar sin son mayores/menores de edad y un “desconocido” cuando no conocemos la edad

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"      

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.

Por ejemplo, vamos a definir un vector x y vamos a imprimir sus elementos al cuadrado: definireos un índice i para imprimir, en cada paso, el valor i-ésimo x[i]^2. Esos índices irán dentro de for (indice in conjunto) { code } (por ejemplo, i in 1:4)

x <- c(0, -7, 1, 4)
for (i in 1:4) {
  
  print(x[i]^2)
  
}
[1] 0
[1] 49
[1] 1
[1] 16

Bucles for

for (i in 1:4) { 
  print(x[i]^2) 
}

Dentro del paréntesis del for () debemos tener una secuencia de índices (en este caso, números). Si queremos hacer lo mismo pero excluyendo el segundo elemento simplemente definimos el conjunto de valores c(1, 3, 4) entre los que i puede moverse.

for (i in c(1, 3, 4)) {
  
  print(x[i]^2)
  
}
[1] 0
[1] 1
[1] 16

Bucles for

Otra forma de usar un bucle es definir de cero un vector: primer inicializamos en ceros y <- rep(0, 4) para después modificar cada elemento i-ésimo definidos como x[i]^2.

y <- rep(0, 4)
for (i in 1:4) {
  
  y[i] <- x[i]^2
  
}
y
[1]  0 49  1 16

Lo anterior es equivalente vectorialmente a

y <- x^2
y
[1]  0 49  1 16

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    2.050    2.747    3.355932    3.157
 for (i in 1:100) {     y[i] <- x[i]^2 } 2075.994 2474.063 2928.653616 2988.838
       uq       max neval
    3.567    13.243   500
 3037.793 13209.298   500

Bucles for

Podemos ver otro ejemplo combinando números y textos: definimos un vector de edades y de nombres, e imprimimos el nomber 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

Fíjate que si no queremos estar pendientes de cuantos valores tenemos, podemos hacer uso de length() para acceder a la última posición (sea cual sea).

for (i in 1:length(nombres)) { 
  
  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

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 for

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

¿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

Por último, 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

💻 Tu turno

📝 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")
}
Code
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")
}
Code
# 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)
}
Code
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)
}
Code
count <- 1
while (count < 6) {
  
  print(count)
  count <- count + 1
  
}

Clase n + 2: depuración y Github

Depurando datos. Manejo de listas. Github

Depuración

En la bioestadística, como en cualquier otro ámbito de aplicación, no siempre los datos los tendremos en el formato deseado.

Ya hemos aprendido como pivotar nuestros datos para tenerlos en formato tidydata, algo indispensable para una correcta depuración y visualización. También hemos aprendido algunas operaciones básicas de filtrado, muestreo, selección de variables y recategorización

Veamos un nuevo paquete llamado {skimr}, cuya función skim() nos permitirá tener un primer análisis exploratorio de nuestros datos

install.packages("skimr")
library(skimr)
iris |> skim()

Depuración

Con dicha función podemos comprobar de manera rápida si nuestros datos presentan alguno de los siguientes problemas

  • Problemas de codificación o rango: los valores parecen valores permitidos según lo que representa la variable (por ejemplo, no hay edades negativas)

  • No tenemos datos ausentes: no hace falta decidir, de momento, que hacemos con ellos, ya que complete_rate sale en todas 1 (n_missing está a cero).

  • No parece que tengamos excesivos valores atípicos: a la vista de los pequeños histogramas y los percentiles, no parece que tengamos excesivos outliers (al menos muy evidentes)

  • Todas las salvo Species son numéricas: si tuviésemos que montar un modelo predictivo todas las variables predictoras son numéricas.

  • Casi todas parecen simétricas: salvo Petal.Length las variables parecen más o menos simétricas (media se parece a mediana).

Depuración

En general dado un dataset desde un punto de vista predictivo debemos preguntarnos

  • 1. Tipología de las variables. ¿Todas mis variables predictoras son numéricas o debo? ¿Mi variable objetivo es categórica? ¿Tienen la tipología adecuada para mi metodología?
  • 2. Codificación/rango de las variables. ¿Todas mis variables tienen un rango coherente (por ejemplo, que una variable de peso no sea negativa)? ¿Están bien codificadas?
  • 3. Atípicos y ausentes. ¿Tengo valores atípicos (outliers)? En caso afirmativo, ¿cómo tratarlos? Tras tratar atípicos, ¿tengo datos ausentes?

Depuración

  • 4. Selección de variables. ¿Necesito seleccionar variables? ¿Tengo alguna de varianza cero (es decir, sin información)? ¿Tengo problemas de dependencia o colinealidad? ¿Puedo resumir mi info con un conjunto nuevo de variables incorreladas (componentes principales)?
  • 5. Variables dummy. ¿Debo recategorizar variables que no sean numéricas?
  • 6. Añadir info. ¿Debo crear nuevas variables que nos aporte info extra?
  • 7. Normalizar variables. ¿Tengo ya mis variables preparadas (tras tratar lo anterior) para el algoritmo que vaya usar (estandarizadas por rango o tipificadas por media-varianza, por ejemplo)?

Depuración

Vamos a realizar un ejemplo sobre el dataset starwars del paquete {dplyr} dentro de tidyverse

library(tidyverse)
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>

El objetivo será predecir el peso (variable objetivo continua) mediante una regresión lineal.

Depuración

Para simplificarlo vamos primero a seleccionar solo las columnas de estatura, peso, edad y sexo.

starwars_lm <- 
  starwars |>
  select(height:mass, birth_year:sex)
starwars_lm
# A tibble: 87 × 4
   height  mass birth_year sex   
    <int> <dbl>      <dbl> <chr> 
 1    172    77       19   male  
 2    167    75      112   none  
 3     96    32       33   none  
 4    202   136       41.9 male  
 5    150    49       19   female
 6    178   120       52   male  
 7    165    75       47   female
 8     97    32       NA   none  
 9    183    84       24   male  
10    182    77       57   male  
# ℹ 77 more rows

1. Tipología

  • Tipología de las variables. Al ser una regresión, ¿todas mis variables predictoras y objetivo son numéricas?

En este caso no todas las predictoras son numéricas ya que tenemos una variable categórica como sex que, de momento, vamos a eliminar

starwars_lm <- 
  starwars_lm |>
  select(where(is.numeric))
starwars_lm
# A tibble: 87 × 3
   height  mass birth_year
    <int> <dbl>      <dbl>
 1    172    77       19  
 2    167    75      112  
 3     96    32       33  
 4    202   136       41.9
 5    150    49       19  
 6    178   120       52  
 7    165    75       47  
 8     97    32       NA  
 9    183    84       24  
10    182    77       57  
# ℹ 77 more rows

2. Codificación/rango

  • Codificación/rango de las variables. ¿Todas mis variables tienen un rango coherente (por ejemplo, que una variable de peso no sea negativa)? ¿Están bien codificadas?

En este caso todas tienen rangos permitidos (eso no significa que no tengamos outliers)

starwars_lm |> skim()

3. Atípicos/ausentes

  • Atípicos y ausentes. ¿Tengo valores atípicos (outliers)? En caso afirmativo, ¿cómo tratarlos? Tras tratar atípicos, ¿tengo datos ausentes? (puedes probar distintos métodos en https://odds.cs.stonybrook.edu/#table1)
starwars_lm |> skim()

En este caso parece obvio que debemos tratar ausentes y atípicos

3. Atípicos (media)

Una de las partes más importantes de la fase de exploración y modificación es la detección de outliers, pudiendo tener diferentes definiciones de valor atípico:

  • Atípico respecto a media: será un dato muy alejado de la media de la variable. ¿Cuánto de alejado? Una definición habitual es definir un dato atípico como aquel que se aleja de la media \(k\) veces la desviación típica (un valor habitual es \(k = 2.5\))

\[\left| x_i - \overline{x} \right| > k*s_{x}\]

Dicha definición de atípico solo tendrá sentido cuando la media sea representativa de tu distribución, es decir, siempre y cuando tengamos cierta simetría (en caos contrario la media, al ser poco robusta, se perturbará fácilmente).

3. Atípicos (media)

Para detectarlos usaremos el paquete {outliers} y su función scores(), que nos dará en cada caso una “puntuación” de cada observación (lo que se aleja). En caso de que queramos detectarlos respecto a la media, le indicaremos que type = "z" y nos devolverá precisamente el valor \(k\)

# install.packages("outliers")
library(outliers)
abs(scores(c(1, -1, 0, 5, 2, 1.5, 0.5, -0.3, 0, 2, 1.7, 0.2, -0.8), type = "z"))
 [1] 0.05794825 1.19759725 0.56982450 2.56903925 0.68572100 0.37183463
 [7] 0.25593812 0.75815632 0.56982450 0.68572100 0.49738918 0.44426995
[13] 1.07204270

De esta forma podemos detectar muy fácil los outliers en función de los estrictos que queramos ser con ese \(k\) (si supera un umbral, outlier). El tipo type = "chisq" nos hace algo parecido pero elevando las desviaciones al cuadrado y diviendo por la varianza.

3. Atípicos (media)

Vamos a aplicarlo a nuestro dataset de starwars (de momento quitando ausentes)

library(outliers)
k <- 2.5
starwars_outliers_mean <-
  starwars_lm |>
  drop_na() |> 
  mutate(across(everything(),
                function(x) {
                  if_else(abs(scores(x, type = "z")) > k, NA, x) }))
starwars_outliers_mean
# A tibble: 36 × 3
   height  mass birth_year
    <int> <dbl>      <dbl>
 1    172    77       19  
 2    167    75      112  
 3     NA    32       33  
 4    202   136       41.9
 5    150    49       19  
 6    178   120       52  
 7    165    75       47  
 8    183    84       24  
 9    182    77       57  
10    188    84       41.9
# ℹ 26 more rows

3. Atípicos (media)

Con if_any() dentro del filter() podemos mostrar todo los registros detectados como outlier en alguna variable.

starwars_outliers_mean |>
  filter(if_any(everything(), is.na))
# A tibble: 4 × 3
  height  mass birth_year
   <int> <dbl>      <dbl>
1     NA    32         33
2    175    NA         NA
3     NA    17         NA
4     NA    20          8

3. Atípicos (mediana)

  • Atípico respecto a mediana: será un dato muy alejado de la mediana de la variable. ¿Cuánto de alejado? Una definición habitual (conocido como filtro de Hampel) es definir un dato atípico como aquel que se aleja de la mediana \(k\) veces la mediana de las desviaciones absolutas (un valor habitual es \(k=3\)), conocida como \(MAD = Me \left(|x_i - Me_x| \right)\)

\[\left| x_i - Me_x \right| > k*MAD\]

Para ello nos bastará usar scores() con type = "mad" (y nos devolverá de nuevo ese \(k\)).

abs(scores(c(1, -1, 0, 5, 2, 1.5, 0.5, -0.3, 0, 2, 1.7, 0.2, -0.8), type = "mad"))
 [1] 0.3372454 1.0117361 0.3372454 3.0352084 1.0117361 0.6744908 0.0000000
 [8] 0.5395926 0.3372454 1.0117361 0.8093889 0.2023472 0.8768380

El valor a imputar sería la mediana

3. Atípicos (percentiles)

  • Atípico respecto a percentiles: será un dato muy alejado de los cuartiles de la variable. ¿Cuánto de alejado?. Una definición habitual es definir un dato atípico como aquel que se aleja de cuartiles 1 y 3 (percentiles 25 y 75) \(k\) veces el rango intercuartílico (\(IQR = Q_3 - Q_1\)). Un valor habitual es \(k=1.5\)

\[x_i > Q_3 + k*IQR \quad \text{ o bien } \quad x_i < Q_1 - k*IQR\]

Para ello nos bastará usar scores() con type = "iqr" (y nos devolverá de nuevo ese \(k\), siendo \(k = 0\) para lo que esté dentro del IQR).

abs(scores(c(1, -1, 0, 5, 2, 1.5, 0.5, -0.3, 0, 2, 1.7, 0.2, -0.8), type = "iqr"))
 [1] 0.0000000 0.5882353 0.0000000 1.9411765 0.1764706 0.0000000 0.0000000
 [8] 0.1764706 0.0000000 0.1764706 0.0000000 0.0000000 0.4705882

El valor a imputar sería la mediana

3. Atípicos (inferencia)

Existen otros procedimientos basados en inferencia estadística (muchos de ellos en el paquete {outliers})

  • Tests de Grubbs y Dixon: ambos test nos permiten detectar si el valor más alto (o bajo) de una variable es un outlier, pudiendo detectar un solo outlier en cada iteración (en caso de detectarlo, deberíamos tratarlo y volver a ejecutar el test)

\(H_0\): valor más alto/bajo no es outlier

\(H_1\): valor más alto/bajo sí es outlier

El test de Dixon (basado en una ordenación) suele funcionar mejor cuando tenemos poca muestra que el test de Grubbs (basado en la media).

Ver más documentación de su funcionamiento en https://www.itl.nist.gov/div898/handbook/eda/section3/eda35h1.htm y https://www.statisticshowto.com/dixons-q-test/

3. Atípicos (inferencia)

x <- c(1, -1, 0, 5, 2, 1.5, 0.5, -0.3, 0, 2, 1.7, 0.2, -0.8)
dixon.test(x, opposite = TRUE) # valor más bajo

    Dixon test for outliers

data:  x
Q = 0.23333, p-value = 0.8072
alternative hypothesis: lowest value -1 is an outlier
x <- c(1, -1, 0, 5, 2, 1.5, 0.5, -0.3, 0, 2, 1.7, 0.2, -0.8)
dixon.test(x, opposite = FALSE) # valor más alto

    Dixon test for outliers

data:  x
Q = 0.51724, p-value = 0.1055
alternative hypothesis: highest value 5 is an outlier

3. Atípicos (inferencia)

  • Test de Rosner: al contrario que los anteriores, nos permite detectar varios outliers a la vez, especialmente diseñado para evitar que un valor atípico nos perturbe tanto que nos enmascare otro (basado en la media). Podemos ejecutarlo con la función rosnerTest() del paquete {EnvStats}.

3. Atípicos (isolation trees)

También existen técnicas de Machine Learning para la identificación de outliers, como los conocidos como Isolation Forest.

La idea es un poco similar a la de un Random Forest, mediante la combinación de muchos árboles de clasificación/regresión conocidos como isolation trees, aunque en este caso los puntos de corte se hace de manera totalmente aleatoria.

De esta manera, aquellos individuos con características diferentes al resto quedarán aislados rápidamente, de manera que los outliers serán aquellos aislados a los que se llega más rápido que al resto

3. Atípicos (isolation trees)

El algoritmo se puede resumir con sigue:

  1. Se crea un nodo raíz con una submuestra de N observaciones.

  2. Se selecciona una variable aleatoriamente y un corte (valor) aleatorio a dentro de su rango

  3. Se crean dos nodos nuevos separando los observaciones (\(x_i \leq a\) vs \(x_i > a\))

  4. Se repiten los pasos 2 y 3 hasta que todas las observaciones quedan aisladas de forma individual.

  5. Se repite el proceso tomando otra submuestra (bootstrap), de manera que el output de cada observación será el promedio de divisiones necesarias para aislarla: cuanto más pequeño sea, más anómalo será.

3. Atípicos (isolation trees)

Es un algoritmo no supervisado, lo que significa que no hay un criterio óptimo objetivo para decidir que a partir de dicho valor una observación es un outlier (idea: usar percentiles de dicha distancia, por ejemplo el 5-10% con menor distancia)

En casos donde el tamaño muestral sea elevado, aislar cada observación puede ser costoso computacionalmente por lo que a veces se asume una profundidad máxima hasta donde puede crecer: las observaciones que sigan sin dividirse se les añade el número de divisiones teóricas promedio \(c(r)\) que se necesitarían para aislarlos mediante un árbol binario de búsqueda (BST) (con \(r\) observaciones).

\[c(r) = 2H(r-1)-{\frac {2(r-1)}{r}}, \quad H(i) = \ln(i) + \gamma, \quad \gamma = 0.577216\]

3. Atípicos (isolation trees)

Es importante que el dataset haya sido ya tratado por ausentes.

library(solitude)
# Modelo isolation forest
m <- as.integer(nrow(starwars_lm |> drop_na())/2)
isoforest <-
  isolationForest$new(sample_size = m, num_trees = 500,
                      replace = TRUE, seed = 1234567,
                      max_depth = 7)
isoforest$fit(dataset = starwars_lm |> drop_na())
INFO  [16:00:32.270] Building Isolation Forest ...
INFO  [16:00:32.327] done
INFO  [16:00:32.335] Computing depth of terminal nodes ...
INFO  [16:00:33.030] done
INFO  [16:00:33.042] Completed growing isolation forest

3. Atípicos (isolation trees)

Con el modelo entrenado, se predicen las distancias de aislamiento promedio de cada observación. Los resultados que calcula isoforest$predict() son la distancia promedio average_depth y una métrica que mide el grado de anomalía anomaly_score (valores próximos a 1 para outliers)

predicciones <-
  isoforest$predict(data =
                      starwars_lm |> drop_na())
predicciones |> arrange(average_depth)
    id average_depth anomaly_score
 1: 16         2.032     0.7515777
 2: 14         2.652     0.6888605
 3: 24         2.834     0.6714639
 4:  3         3.212     0.6367235
 5: 11         3.480     0.6131872
 6: 19         3.900     0.5780399
 7:  5         3.958     0.5733472
 8:  4         4.292     0.5470556
 9:  2         4.500     0.5312952
10: 31         4.718     0.5152642
11: 34         4.766     0.5117999
12: 25         4.854     0.5055091
13:  6         4.874     0.5040902
14: 33         4.880     0.5036653
15: 26         4.908     0.5016872
16: 35         4.914     0.5012643
17: 20         5.002     0.4951031
18: 29         5.142     0.4854567
19: 27         5.174     0.4832783
20: 17         5.204     0.4812450
21: 15         5.426     0.4664619
22:  1         5.440     0.4655450
23:  8         5.488     0.4624150
24:  7         5.494     0.4620252
25: 30         5.544     0.4587899
26: 32         5.550     0.4584032
27: 36         5.848     0.4396011
28: 10         5.916     0.4354199
29: 12         5.920     0.4351752
30: 13         5.956     0.4329790
31: 18         5.974     0.4318850
32: 21         6.006     0.4299471
33:  9         6.052     0.4271765
34: 28         6.068     0.4262170
35: 23         6.108     0.4238276
36: 22         6.138     0.4220444
    id average_depth anomaly_score

3. Atípicos (isolation trees)

Code
ggplot(data = predicciones, aes(x = average_depth)) +
  geom_density(fill = "#6D98ED", alpha = 0.5) +
  geom_vline(xintercept =
               quantile(predicciones$average_depth,
                        seq(0, 1, 0.1)),
             color = "#DC5B49", linetype = "dashed") +
  labs(title = "Distribución de distancias medias del Isolation Forest",
    subtitle = "Deciles marcados en rojo") +
  theme_minimal()

3. Ausentes

Tras marcar los outliers tenemos dos opciones

  • eliminar dichas observaciones (pasamos a NA y luego con drop_na(); problema: eliminas TODA la fila)

  • imputar (sin contar con los ausentes)

3. Ausentes

Antes de decidir podemos hacer uso del paquete {naniar} (ver https://naniar.njtierney.com/)

Por ejemplo con geom_miss_point() nos permite visualizar los valores ausentes (que por defecto ggplot los retira)

library(naniar)
ggplot(starwars_lm, 
       aes(x = height, y = mass)) + 
  geom_miss_point()

3. Ausentes

También podemos hacer uso de gg_miss_var para visualizar la cantidad de ausentes para cada variable.

gg_miss_var(starwars)

3. Ausentes

En gg_miss_var tenemos un argumento para facetar por una cualitativa.

gg_miss_var(starwars, facet = sex)

3. Ausentes

Una forma de representar los ausentes en un formato tidy es con la conocida como shadow matrix: una tabla con la misma dimensión que los datos pero con indicadores binarios sobre si tenemos (NA) o no missing (!NA), cuyas variables tiene de sufijo _NA

Esto lo podemos hacer con as_shadow()

as_shadow(starwars_lm)
# A tibble: 87 × 3
   height_NA mass_NA birth_year_NA
   <fct>     <fct>   <fct>        
 1 !NA       !NA     !NA          
 2 !NA       !NA     !NA          
 3 !NA       !NA     !NA          
 4 !NA       !NA     !NA          
 5 !NA       !NA     !NA          
 6 !NA       !NA     !NA          
 7 !NA       !NA     !NA          
 8 !NA       !NA     NA           
 9 !NA       !NA     !NA          
10 !NA       !NA     !NA          
# ℹ 77 more rows

3. Ausentes

Con bind_shadow() podemos añadir la shadow matrix al dataset original.

bind_shadow(starwars_lm)
# A tibble: 87 × 6
   height  mass birth_year height_NA mass_NA birth_year_NA
    <int> <dbl>      <dbl> <fct>     <fct>   <fct>        
 1    172    77       19   !NA       !NA     !NA          
 2    167    75      112   !NA       !NA     !NA          
 3     96    32       33   !NA       !NA     !NA          
 4    202   136       41.9 !NA       !NA     !NA          
 5    150    49       19   !NA       !NA     !NA          
 6    178   120       52   !NA       !NA     !NA          
 7    165    75       47   !NA       !NA     !NA          
 8     97    32       NA   !NA       !NA     NA           
 9    183    84       24   !NA       !NA     !NA          
10    182    77       57   !NA       !NA     !NA          
# ℹ 77 more rows

3. Ausentes

Lo anterior es equivalente a construirlo con nabular() (tabular + NA)

nabular(starwars) |> 
  summarise(media_peso = mean(mass, na.rm = ), .by = sex_NA)
# A tibble: 2 × 2
  sex_NA media_peso
  <fct>       <dbl>
1 !NA            NA
2 NA             NA

3. Ausentes

También podemos hacer uso de add_prop_miss() para añadir la proporción de ausentes en cada registro

starwars_lm |>
  add_prop_miss()
# A tibble: 87 × 4
   height  mass birth_year prop_miss_all
    <int> <dbl>      <dbl>         <dbl>
 1    172    77       19           0    
 2    167    75      112           0    
 3     96    32       33           0    
 4    202   136       41.9         0    
 5    150    49       19           0    
 6    178   120       52           0    
 7    165    75       47           0    
 8     97    32       NA           0.333
 9    183    84       24           0    
10    182    77       57           0    
# ℹ 77 more rows

3. Ausentes

Para imputar datos ausentes podemos hacer uso de diferentes paquetes, entre ellos {simpute} y {mice} (ver https://amices.org/mice/)

Por ejemplo en el segundo paquete contamos con la función md.pattern() podemos visualizar la distribución de los ausentes

mice::md.pattern(starwars_lm)

   height mass birth_year   
36      1    1          1  0
23      1    1          0  1
7       1    0          1  1
15      1    0          0  2
6       0    0          0  3
        6   28         44 78

3. Ausentes/atípicos

En el caso de starwars vamos a imputar ausentes iniciales con Random Forest (mice::mice.impute.rf())…

Code
starwars_lm_NA_init <-
  starwars_lm |> 
  # Imputamos NA iniciales
  mice::mice(ntree = 100, meth = "rf") |> 
  # Completamos NA
  complete()

 iter imp variable
  1   1  height  mass  birth_year
  1   2  height  mass  birth_year
  1   3  height  mass  birth_year
  1   4  height  mass  birth_year
  1   5  height  mass  birth_year
  2   1  height  mass  birth_year
  2   2  height  mass  birth_year
  2   3  height  mass  birth_year
  2   4  height  mass  birth_year
  2   5  height  mass  birth_year
  3   1  height  mass  birth_year
  3   2  height  mass  birth_year
  3   3  height  mass  birth_year
  3   4  height  mass  birth_year
  3   5  height  mass  birth_year
  4   1  height  mass  birth_year
  4   2  height  mass  birth_year
  4   3  height  mass  birth_year
  4   4  height  mass  birth_year
  4   5  height  mass  birth_year
  5   1  height  mass  birth_year
  5   2  height  mass  birth_year
  5   3  height  mass  birth_year
  5   4  height  mass  birth_year
  5   5  height  mass  birth_year

3. Ausentes/atípicos

… después detectar outliers con isolation tree (por ejemplo el 5% con distancia más baja)…

Code
m <- as.integer(nrow(starwars_lm_NA_init)/2)
isoforest <-
    isolationForest$new(sample_size = m, num_trees = 500,
                        replace = TRUE, seed = 1234567,
                        max_depth = 9)
isoforest$fit(dataset = starwars_lm_NA_init)
INFO  [16:00:36.035] Building Isolation Forest ...
INFO  [16:00:36.046] done
INFO  [16:00:36.047] Computing depth of terminal nodes ...
INFO  [16:00:36.718] done
INFO  [16:00:36.730] Completed growing isolation forest
Code
predicciones <-
    isoforest$predict(data = starwars_lm_NA_init)

id_outliers <- 
  predicciones |>
  slice_min(average_depth, prop = 0.05) |> 
  pull(id)

starwars_lm_sin_outliers <-
  starwars_lm_NA_init |> 
  rowid_to_column() |> 
  mutate(across(-rowid,
                function(x, y, z) {
                  if_else(y %in% z, NA, x) }, rowid, id_outliers)) |> 
  select(-rowid)

3. Ausentes/atípicos

… y luego de nuevo imputar outliers (pasados a ausentes) con Random Forest (mice::mice.impute.rf()).

Code
starwars_lm_depurado <-
  starwars_lm_sin_outliers |> 
  # Imputamos NA iniciales
  mice::mice(ntree = 100, meth = "rf") |> 
  # Completamos NA
  complete()

 iter imp variable
  1   1  height  mass  birth_year
  1   2  height  mass  birth_year
  1   3  height  mass  birth_year
  1   4  height  mass  birth_year
  1   5  height  mass  birth_year
  2   1  height  mass  birth_year
  2   2  height  mass  birth_year
  2   3  height  mass  birth_year
  2   4  height  mass  birth_year
  2   5  height  mass  birth_year
  3   1  height  mass  birth_year
  3   2  height  mass  birth_year
  3   3  height  mass  birth_year
  3   4  height  mass  birth_year
  3   5  height  mass  birth_year
  4   1  height  mass  birth_year
  4   2  height  mass  birth_year
  4   3  height  mass  birth_year
  4   4  height  mass  birth_year
  4   5  height  mass  birth_year
  5   1  height  mass  birth_year
  5   2  height  mass  birth_year
  5   3  height  mass  birth_year
  5   4  height  mass  birth_year
  5   5  height  mass  birth_year
Code
starwars_lm_depurado
   height  mass birth_year
1     172  77.0       19.0
2     167  75.0      112.0
3      96  32.0       33.0
4     202 136.0       41.9
5     150  49.0       19.0
6     178 120.0       52.0
7     165  75.0       47.0
8      97  32.0        8.0
9     183  84.0       24.0
10    182  77.0       57.0
11    188  84.0       41.9
12    180 110.0       64.0
13    228 112.0      200.0
14    180  80.0       29.0
15    173  74.0       44.0
16    188  87.0       22.0
17    170  77.0       21.0
18    180 110.0       52.0
19    185  84.0       64.0
20    170  75.0       82.0
21    183  78.2       31.5
22    200 140.0       15.0
23    190 113.0       53.0
24    177  79.0       31.0
25    175  79.0       37.0
26    180  83.0       41.0
27    150  32.0       48.0
28    193  80.0       92.0
29    196  90.0       67.0
30    160  68.0       72.0
31    193  89.0       92.0
32    191  90.0       72.0
33    170  82.0       91.0
34    185  45.0       46.0
35    196  66.0       52.0
36    224  82.0       92.0
37    206 112.0       15.0
38    183  79.0       46.0
39    137  65.0       33.0
40    112  40.0       33.0
41    183  75.0       62.0
42    163  55.0       72.0
43    175  80.0       54.0
44    180  55.0       57.0
45    178  55.0       48.0
46    175  79.0       22.0
47     94  45.0       33.0
48    122  45.0       48.0
49    163  65.0       82.0
50    188  84.0       72.0
51    198  82.0       92.0
52    196  87.0       92.0
53    171  77.0       82.0
54    184  50.0       46.0
55    188  87.0       52.0
56    264  88.0       67.0
57    188  80.0       22.0
58    196  90.0       92.0
59    185  85.0       41.9
60    157  56.2       48.0
61    183  77.0       82.0
62    183  80.0       24.0
63    170  56.2       58.0
64    166  50.0       40.0
65    165  75.0       58.0
66    193  80.0      102.0
67    191  84.0       67.0
68    183  79.0       66.0
69    168  55.0      112.0
70    198 102.0       41.9
71    229  88.0       52.0
72    213 102.0       41.9
73    167  17.0        8.0
74     96  32.0       48.0
75    193  48.0       52.0
76    191  80.0       22.0
77    178  57.0       48.0
78    216 159.0       15.0
79    234 136.0       41.9
80    188  79.0       22.0
81    178  48.0       33.0
82    206  80.0      102.0
83    198 140.0       15.0
84    185  66.0       62.0
85    216 136.0       15.0
86    163  49.0       46.0
87    170  75.0       57.0

4. Selección de variables

  • Selección de variables. ¿Necesito seleccionar variables? ¿Tengo alguna de varianza cero (es decir, sin información)? ¿Tengo problemas de dependencia o colinealidad? ¿Puedo resumir mi info con un conjunto nuevo de variables incorreladas (componentes principales)?

En este caso no tenemos altas correlaciones entre las variables predictoras: nos interesa predictoras lo más incorreladas posibles entre sí y lo más correladas con la objetivo (por ejemplo, altura alta correlación con peso)

starwars_lm_depurado |> corrr::correlate()
# A tibble: 3 × 4
  term       height    mass birth_year
  <chr>       <dbl>   <dbl>      <dbl>
1 height     NA      0.666      0.235 
2 mass        0.666 NA          0.0296
3 birth_year  0.235  0.0296    NA     

4. Selección de variables

El paquete {corrr} y {corrrplot} nos permite visualizar dichas correlaciones

corrplot::corrplot(starwars_lm_depurado |> cor())

5.6.7. Depuración

  • Variables dummy. ¿Debo recategorizar variables que no sean numéricas? (ver {fastDummies})

  • Añadir info. ¿Debo crear nuevas variables que nos aporte info extra?

  • Normalizar variables. ¿Tengo ya mis variables preparadas (tras tratar lo anterior) para el algoritmo que vaya usar (estandarizadas por rango o tipificadas por media-varianza, por ejemplo)?

En este caso no es necesario dummificar ya que hemos considerado solo las numéricas. Al no tener que seleccionar variables y aplicar una regresión lineal tampoco es indispensable normalizar y no vamos añadir info nueva

Regresión lineal

Vamos a comparar 3 modelos de regresión: sin tratar nada, eliminando ausentes, y el tratamiento de ausentes/atípicos realizados.

starwars_lm |> lm(formula = mass ~ .) |> summary()

Call:
lm(formula = mass ~ ., data = starwars_lm)

Residuals:
    Min      1Q  Median      3Q     Max 
-467.55  -36.85   -9.06    9.82  836.00 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -396.6645   198.4949  -1.998 0.053972 .  
height         2.5119     1.0830   2.319 0.026706 *  
birth_year     0.7985     0.1981   4.030 0.000309 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 180.5 on 33 degrees of freedom
  (51 observations deleted due to missingness)
Multiple R-squared:  0.3367,    Adjusted R-squared:  0.2965 
F-statistic: 8.377 on 2 and 33 DF,  p-value: 0.001142

Regresión lineal

Vamos a comparar 3 modelos de regresión: sin tratar nada, eliminando ausentes, y el tratamiento de ausentes/atípicos realizados.

starwars_lm |> drop_na() |> lm(formula = mass ~ .) |> summary()

Call:
lm(formula = mass ~ ., data = drop_na(starwars_lm))

Residuals:
    Min      1Q  Median      3Q     Max 
-467.55  -36.85   -9.06    9.82  836.00 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -396.6645   198.4949  -1.998 0.053972 .  
height         2.5119     1.0830   2.319 0.026706 *  
birth_year     0.7985     0.1981   4.030 0.000309 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 180.5 on 33 degrees of freedom
Multiple R-squared:  0.3367,    Adjusted R-squared:  0.2965 
F-statistic: 8.377 on 2 and 33 DF,  p-value: 0.001142

Regresión lineal

Vamos a comparar 3 modelos de regresión: sin tratar nada, eliminando ausentes, y el tratamiento de ausentes/atípicos realizados.

starwars_lm_depurado |> lm(formula = mass ~ .) |> summary()

Call:
lm(formula = mass ~ ., data = starwars_lm_depurado)

Residuals:
    Min      1Q  Median      3Q     Max 
-58.436  -8.523   0.117   6.550  52.643 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -31.84276   13.54574  -2.351   0.0211 *  
height        0.64810    0.07666   8.455 7.43e-13 ***
birth_year   -0.11935    0.07335  -1.627   0.1074    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 19.71 on 84 degrees of freedom
Multiple R-squared:  0.4602,    Adjusted R-squared:  0.4474 
F-statistic: 35.81 on 2 and 84 DF,  p-value: 5.667e-12

Regresión lineal

Vamos a comparar 3 modelos de regresión: sin tratar nada, eliminando ausentes, y el tratamiento de ausentes/atípicos realizados.

  • sin tratar nada: obtenemos \(R^2 = 0.3367\) con el intercepto no significativo

  • eliminando ausentes: obtenemos lo mismo (por defecto lm() se carga los ausentes)

  • tratamiento de ausentes/atípicos: obtenemos \(R^2 = 0.5551\) con edad no significativa

5. Variables dummies

Otra opción a veces es añadir información extra de manera los outliers nos ayuden mejor a predecir, y en este caso añadiremos la variable sexo haciéndola dummy (one-hot encoding): creamos \(k-1\) variables binarias con dummy_cols() del paquete fastDummies

Code
library(fastDummies)
starwars_lm_sex <- 
  starwars |>
  select(height:mass, birth_year:sex) |> 
  drop_na(sex) |> 
  # Imputamos NA iniciales
  mice::mice(ntree = 100, meth = "rf") |> 
  # Completamos NA
  complete()

 iter imp variable
  1   1  height  mass  birth_year
  1   2  height  mass  birth_year
  1   3  height  mass  birth_year
  1   4  height  mass  birth_year
  1   5  height  mass  birth_year
  2   1  height  mass  birth_year
  2   2  height  mass  birth_year
  2   3  height  mass  birth_year
  2   4  height  mass  birth_year
  2   5  height  mass  birth_year
  3   1  height  mass  birth_year
  3   2  height  mass  birth_year
  3   3  height  mass  birth_year
  3   4  height  mass  birth_year
  3   5  height  mass  birth_year
  4   1  height  mass  birth_year
  4   2  height  mass  birth_year
  4   3  height  mass  birth_year
  4   4  height  mass  birth_year
  4   5  height  mass  birth_year
  5   1  height  mass  birth_year
  5   2  height  mass  birth_year
  5   3  height  mass  birth_year
  5   4  height  mass  birth_year
  5   5  height  mass  birth_year

5. Variables dummies

Otra opción a veces es añadir información extra de manera los outliers nos ayuden mejor a predecir, y en este caso añadiremos la variable sexo haciéndola dummy (one-hot encoding): creamos \(k-1\) variables binarias con dummy_cols() del paquete fastDummies

Obtenemos un \(R^2 = 0.5498\) (similar a haber tratado de outliers)

starwars_lm_sex_depurado <-
  starwars_lm_sex |>
  dummy_cols(select_columns = "sex", remove_first_dummy = TRUE,
             remove_selected_columns = TRUE)

starwars_lm_sex_depurado |> lm(formula = mass ~ .) |> summary()

Call:
lm(formula = mass ~ ., data = starwars_lm_sex_depurado)

Residuals:
     Min       1Q   Median       3Q      Max 
-107.658   -8.035   -2.315    6.519   68.467 

Coefficients:
                     Estimate Std. Error t value Pr(>|t|)    
(Intercept)         -22.96928   15.26066  -1.505    0.136    
height                0.53776    0.07963   6.753 2.43e-09 ***
birth_year           -0.02482    0.02651  -0.936    0.352    
sex_hermaphroditic 1301.75221   28.29232  46.011  < 2e-16 ***
sex_male              8.62314    6.68648   1.290    0.201    
sex_none             13.25865   11.62790   1.140    0.258    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 23.56 on 77 degrees of freedom
Multiple R-squared:  0.9748,    Adjusted R-squared:  0.9731 
F-statistic: 595.3 on 5 and 77 DF,  p-value: < 2.2e-16

La imputación importa

El método de imputación será crucial: si imputamos los ausentes de las numéricas por la media, esto es lo que obtenemos (sobreajuste).

Code
library(fastDummies)
starwars_lm_sex <- 
  starwars |>
  select(height:mass, birth_year:sex) |> 
  drop_na(sex) |> 
  # Imputamos NA iniciales
  mice::mice(meth = "mean") |> 
  # Completamos NA
  complete()

 iter imp variable
  1   1  height  mass  birth_year
  1   2  height  mass  birth_year
  1   3  height  mass  birth_year
  1   4  height  mass  birth_year
  1   5  height  mass  birth_year
  2   1  height  mass  birth_year
  2   2  height  mass  birth_year
  2   3  height  mass  birth_year
  2   4  height  mass  birth_year
  2   5  height  mass  birth_year
  3   1  height  mass  birth_year
  3   2  height  mass  birth_year
  3   3  height  mass  birth_year
  3   4  height  mass  birth_year
  3   5  height  mass  birth_year
  4   1  height  mass  birth_year
  4   2  height  mass  birth_year
  4   3  height  mass  birth_year
  4   4  height  mass  birth_year
  4   5  height  mass  birth_year
  5   1  height  mass  birth_year
  5   2  height  mass  birth_year
  5   3  height  mass  birth_year
  5   4  height  mass  birth_year
  5   5  height  mass  birth_year

La imputación importa

El método de imputación será crucial: si imputamos los ausentes de las numéricas por la media, esto es lo que obtenemos (sobreajuste).

starwars_lm_sex_depurado <-
  starwars_lm_sex |>
  dummy_cols(select_columns = "sex", remove_first_dummy = TRUE,
             remove_selected_columns = TRUE)

starwars_lm_sex_depurado |>
  lm(formula = mass ~ height + birth_year) |>
  summary()

Call:
lm(formula = mass ~ height + birth_year, data = starwars_lm_sex_depurado)

Residuals:
    Min      1Q  Median      3Q     Max 
-524.03  -25.77    0.00   14.62  908.95 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -138.6097    75.6306  -1.833   0.0706 .  
height         1.0133     0.4066   2.492   0.0148 *  
birth_year     0.6839     0.1258   5.434 5.78e-07 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 122.4 on 80 degrees of freedom
Multiple R-squared:  0.2793,    Adjusted R-squared:  0.2612 
F-statistic:  15.5 on 2 and 80 DF,  p-value: 2.046e-06

La imputación importa

El método de imputación será crucial: si imputamos los ausentes de las numéricas por la media, esto es lo que obtenemos (sobreajuste).

Code
starwars_lm_sex_depurado |>
  lm(formula = mass ~ .) |>
  summary()

Call:
lm(formula = mass ~ ., data = starwars_lm_sex_depurado)

Residuals:
    Min      1Q  Median      3Q     Max 
-44.526 -12.756  -4.167  11.944  55.172 

Coefficients:
                     Estimate Std. Error t value Pr(>|t|)    
(Intercept)        -1.018e+01  1.422e+01  -0.716   0.4762    
height              4.914e-01  7.340e-02   6.694 3.13e-09 ***
birth_year         -8.717e-03  2.530e-02  -0.345   0.7314    
sex_hermaphroditic  1.287e+03  2.516e+01  51.171  < 2e-16 ***
sex_male            8.640e+00  5.842e+00   1.479   0.1432    
sex_none            2.201e+01  1.014e+01   2.169   0.0331 *  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 20.57 on 77 degrees of freedom
Multiple R-squared:  0.9804,    Adjusted R-squared:  0.9791 
F-statistic: 770.7 on 5 and 77 DF,  p-value: < 2.2e-16

El mundo Github

Trabajar ordenados, publicar resultados, replicabilidad de lo realizado

¿Qué es Github?

GitHub es la plataforma colaborativa más conocida basada en el sistema de control de versiones Git

  • ¿Qué es Git? Git es un sistema de control de versiones: una especie de Dropbox para facilitar la programación colaborativa entre un grupo de personas, permitiendo llevar la trazabilidad de los cambios realizados.
  • ¿Qué es Github? Nuestra plataforma/interfaz para ejecutar el control de versiones: nos servirá no solo para trabajar colaborativamente sino para hacer transparente el proceso de construcción de nuestros proyectos de código.

Importante

Desde el 4 de junio de 2018 Github es de Microsoft (ergo el código que subas también)

Visión general

Tras hacernos una cuenta en Github, arriba a la derecha tendremos un círculo, y haciendo click en Your Profile, veremos algo similar a esto

  • Edit profile: nos permite añadir una descripción y foto de perfil.

  • Overview: en ese panel de cuadrados se visualizará nuestra actividad a lo largo del tiempo.

  • Repositories: el códugo será subido a repositorios, el equivalente a nuestras carpetas compartidas en Dropbox.

Primer uso: consumidor

Antes de aprender como crear repositorios, Github también nos servirá para

  • Acceder a código ajeno
  • Proponer mejoras a otros usuarios, e incluso proponer correcciones de error que detectemos de software que usemos
  • Instalar paquetes de R. En muchas ocasiones los desarrolladores de paquetes suben las actualizaciones a CRAN cada cierto tiempo, y en otras el software no es suficientemente «amplio» para poder ser subido como paquete.

El código de paquetes que no tengamos subido en CRAN podremos instalarlo como código desde Github

Instalar desde Github

Por ejemplo, vamos a instalar un paquete llamado {peRReo}, cuya única función es darnos paletas de colores basadas en portadas de álbumes de música urbana

Para ello antes tendremos que instalar un conjunto de paquetes para desarrolladores llamado {devtools}, que nos permitirá la instalación desde Github

install.packages("devtools")

Instalar desde Github

Las instrucciones de instalación suelen venir detalladas en la portada del repositorio

En la mayoría de casos bastará con la función install_github() (del paquete que acabamos de instalar), pasándole como argumento la ruta del repositorio (sin “github.com/”).

devtools::install_github("jbgb13/peRReo")

Ya puedes perrear con ggplot ;)

Descargar desde Github

La mayoría de veces lo que subamos no será un paquete de R como tal sino que subiremos un código más o menos organizado y comentado. En ese caso podremos descargar el repo entero haciendo click Code y luego Download ZIP.

Por ejemplo, vamos a descargarnos los scripts de dataviz que han subido desde el Centre d’Estudis d’Opinió

Ideal

¿Lo ideal en caso de RTVE? Tener dos tipos de repositorios

  • Una colección de repositorios públicos (producción) donde hacer transparente el código y los datos (ya validados), coordinado por un nº reducido de personas.

  • Una colección de repositorios privados (desarrollo) donde esté todo el equipo colaborando y donde se haga el trabajo del día, con trazabilidad interna.

Nuestro primer repositorio

Vamos a crear nuestro primero repositorio que servirá además como carta de presentación de nuestro perfil en Github.

  1. Repositories: hacemos click en las pestaña de Repositories.

  2. New: hacemos click en el botón verde New para crear un nuevo repositorio

Nuestro primer repositorio

  • Repository name: el nombre del repositorio. En este caso vamos a crear un repositorio muy concreto: el nombre debe coincidir exactamente con tu nombre de usuario

  • Description: descripción de tu repositorio. En este caso será un repo de presentación.

Nuestro primer repositorio

  • Public vs private: con cada repositorio tendremos la opción de hacer el repositorio

    • público: todos los usuarios podrán ver el código así cómo la trazabilidad de su desarrollo (qué se añade y cuándo). Es para mí la opción más recomendable cuando quieres darle visibilidad y transparencia a tu trabajo
    • privado: solo tendrán acceso al repositorio aquellos usuarios a los que se lo permitas. No se podrá visualizar ni instalar nada de él fuera de Github.

En este caso concreto, dado que será un repositorio de presentación, lo haremos público.

Nuestro primer repositorio

  • Add a README file: un README file será el archivo donde incluiremos las instrucciones y detalles de uso a los demás (en el caso de {peRReo} era el archivo que contenía los detalles de instalación)

De momento ignoraremos los demás campos para este primer repositorio.

Nuestro primer repositorio

Por defecto Github asume que este repositorio, con el mismo nombre que nuestro usuario será el repositorio que querremos que se presente de inicio cuando alguien entra en nuestro perfil, y será el repositorio donde [incluir en el README.md] una presentación de nosotros y un índice de tu trabajo (si quieres).

Nuestro primer repositorio

Fíjate que ahora en nuestra portada tenemos dicho README.md que podemos personalizar a nuestro gusto haciendo uso de html y markdown.

Aquí puedes ver algunos ejemplos de README.MD

Repo de código

Una vez que tenemos nuestro README de presentación (recuerda que puedes personalizar a tu gusto con html y markdown) vamos a crear un repositorio de código.

Si ya era importante trabajar con proyectos en RStudio, cuando lo combinamos con Github es aún más crucial que creemos un proyecto antes de subir el código, así que vamos a crear uno de prueba que se llame repo-github-1.

En dicho proyecto vamos a crear un script (en mi caso llamado codigo.R) en el que deberás hacer los siguientes pasos:

Repo de código

  1. Carga directamente desde la página del ISCIII el archivo llamado casos_hosp_uci_def_sexo_edad_provres.csv
Code
# Carga de datos desde ISCIII
datos_covid <- read_csv(file = "https://cnecovid.isciii.es/covid19/resources/casos_hosp_uci_def_sexo_edad_provres.csv")
  1. Filtra datos de Madrid ("M"), de 2020 y con sexo conocido (hombre/mujer). Tras ello quédate con las columnas fecha, sexo, grupo_edad, num_casos (ese orden). Por último obtén la suma de casos diarios por fecha y sexo.
Code
# Depuración
datos_madrid <-
  datos_covid |>
  # Filtrado por Madrid y fecha
  filter(provincia_iso == "M" & fecha <= "2020-12-31" & sexo != "NC") |> 
  # Selección de columnas
  select(provincia_iso:fecha, num_casos) |> 
  # Resumen de casos diarios por fecha y sexo
  summarise(num_casos = sum(num_casos), .by = c(fecha, sexo))

Repo de código

  1. Exporta el dataset a un csv en una carpeta que se llame exportado
Code
# Exportamos datos
write_csv(datos_madrid, file = "./exportado/datos_madrid.csv")
  1. Crea una gráfica de líneas que tenga en el eje x fecha, en el eje y casos, con una curva por sexo (gráficas separadas).
Code
# Gráfica
ggplot(datos_madrid) +
  geom_line(aes(x = fecha, y = num_casos, color = sexo),
            alpha = 0.6, linewidth = 0.7) +
  scale_color_manual(values = c("#85519D", "#278862")) +
  facet_wrap(~sexo) +
  theme_minimal() +
  theme(legend.position = "bottom")

  1. Tras ello exportamos la gráfica
ggsave(filename = "./exportado/ggplot.png", plot = last_plot(),
       bg = "white", width = 12, height = 8)

Repo de código

¿Cómo subimos el proyecto? Vamos de nuevo a crear un proyecto de cero. Antes no hemos hablado de dos campos importantes:

  • Add .gitignore nos permitirá seleccionar el lenguaje en el que estará nuestro proyecto para que Github lo entienda al sincronizar (y no actualice cosas que no deba).

  • Choose a license nos permitirá seleccionar la licencia que determinará las condiciones en las que otros podrán reusar tu código.

Repo de código

Si te fijas traer crearlo tenemos solo 3 archivos: el de licencia, el .gitignore y el readme.md (donde deberíamos escribir una guía de uso de lo que hayamos subido)

Para subir los archivos vamos a clickar en Add file < Upload File y arrastraremos TODOS los archivos de la carpeta de nuestro proyecto.

Repo de código

Tras la subida de archivos tendremos un cuadro llamado Commit changes

Un commit es una modificación del repositorio con algo que se añade/elimine/modifique, y dicho cuadro es recomendable usarlo para resumir en qué consiste la modificación, de manera que quede trazado el cambio.

Repo de código

Haciendo click en el reloj donde indica el número de commits accedemos al histórico de commits (cambios) con hora, día, autor, comentarios, etc.

Repo de código

Vamos a realizar un cambio en nuestro código: en tu código local (local –> tu ordenador), en lugar de filtrar por Madrid haz el filtro por Barcelona, guarda el código y sube en el repositorio el nuevo archivo (con el mismo nombre, Github hará la sobrescritura)

datos_bcn <-
  datos_covid |>
  filter(provincia_iso == "B" & fecha <= "2020-12-31" & sexo != "NC") |> 
  select(fecha, sexo, grupo_edad, num_casos) |> 
  summarise(num_casos = sum(num_casos), .by = c(fecha, sexo))

Consulta de commits

Si ahora consultamos el commit, al lado hay un número que lo identifica, y clickando en él nos resume los cambios: no solo almacena todas las versiones pasadas sino que además nos muestra las diferencias entre los archivos cambiados

Trazabilidad de cambios

Tenemos dos modos de visualización de los cambios: el modo split nos muestra el antiguo y el nuevo, con las inclusiones en verde y lo que ya no está en rojo; y el modo unified nos muestra todo en un mismo documento.

Recuperación de commits

Github nos permite incluso recuperar una versión del pasado de nuestro repositorio, haciendo click en el tercer icono del commit.

Recuperación de commits

Si te fijas ahora al lado de 1 branch tenemos un menú desplegable en el que antes ponía main y ahora un número identificador del commit. Ya hablaremos de la idea de rama (branch)

Repo con rmd/qmd

Vamos a poner en práctica lo aprendido:

  1. Crea un nuevo repositorio en Github (llamado repo-github-2) donde habrá alojado con proyecto de R.

  2. Crea un proyecto en RStudio que se llame (por ejemplo) proyecto-qmd

  3. Una vez dentro del proyecto en RStudio haz click en File < New File < Quarto Document

Deberás tener un documento similar a este: un quarto markdown (.qmd), un documento que nos permitirá incluir markdown + código (puede ser R o puede ser Observable, D3, etc).

Repo con rmd/qmd

Este formato es ideal para:

  • Trabajar en equipo construyendo el borrador de una pieza.
  • Tomar apuntes o informes para uno mismo.
  • Presentar tu trabajo a tus compañeros.

Si te fijas ahora nuestro repositorio tiene un archivo con formato .html…es decir…

¡Es una web!

Github pages

¿Cómo convertir nuestro repositorio en una web?

  1. Haz click en Settings
  2. Ve al apartado Pages
  3. En el subapartado branch selecciona la única rama que tenemos ahora (main)
  4. Selecciona la carpeta donde tengas el .html (en web complejas estará como en cualquier web en docs, en algo simple estará en la ruta raiz del repositorio)
  5. Haz click en Save

Github pages

Si te fijas en la parte superior del repositorio ahora tenemos un icono naranja, que nos indica que la web está en proceso de ser desplegada (deploy)

Github pages

Pasados unos segundos (dependiendo del tamaño de la web y tu conexión a internet) ese icono pasará a ser un check verde: habemus web

El link de la web por defecto será {nombre_usuario}.github.io/{nombre_repo}

Github pages

¡Un momento! Ahora mismo nuestra web no nos está mostrando nuestro .qmd, sino por defecto el README.md.

Para que Github entienda que queremos visualizar ese .html que hemos generado a partir del .qmd vamos en nuestro proyecto local a borrar todo lo que no sea nuestro archivo .Rproj y nuestro archivo .qmd, y vamos a cambiar el nombre a este último llamándolo index.qmd, y volvemos a compilarlo para generar un index.html

Github pages

Vamos a subir a Github ese nuevo proyecto con el cambio de nombre (llamado repo-github-3) para ver luego las diferencias entre uno y otro

Github pages

Si repetimos el proceso para hacer una Page y esperamos al tick verde…

Si a tu .qmd ya le llamas de inicio index.qmd, automáticamente, al detectar Github un index.html, interpreta que ese archivo index.html es el que define la web (y puedes personalizar añadiendo un archivo css de estilos)

Habemus web simplemente clickando en Pages :)

Repo con diapositivas

Vamos a crear el último repositorio que se llamará repo-diapos, y crear un proyecto en RStudio del mismo nombre (por ejemplo). Una vez creado le daremos a File < New File < Quarto Presentation.

La forma de escribir será igual que un .qmd normal solo que ahora cada diapositiva la separaremos con un --- (usando archivos de estilos podemos personalizar lo que queramos)

Llama al archivo directamente index.qmd, súbelo a Github y con un click en Pages tienes una web con tus diapositivas

Uso de Gitkraken

La forma más sencilla para trabajar de manera colaborativa en Github, y tenerlo sincronizado con nuestro local, es hacer uso de Gitkraken

Una vez dentro clickamos en el icono de la carpeta (Repo Management) y si ya tenemos el repositorio en Github seleccionamos Clone, indicando donde queremos clonar (en nuestro local) y que repositorio de Github queremos clonar.

Uso de Gitkraken

Una vez clonado, la idea es que cada cambio que hagamos en local nos aparecerá en Gitkraken como View changes.

Uso de Gitkraken

Cuando tengas suficientes cambios como para actualizar el repositorio (tampoco tiene sentido actualizar con cada edición), verás algo similar a esto con todos los commits realizados

Podrás decidir cuáles de los commits locales quieres incluir en remoto, bien uno a uno o en Stage all changes (para todos)

Uso de Gitkraken

Tras incluir los commits deberás incluir un título y descripción del commit

Uso de Gitkraken

Tras hacerlo verás que ahora tenemos dos iconos separados en una especie de árbol (¿te acuerdas de la branch o rama?):

  • Ordenador: la versión del repositorio que tienes en tu ordenador.

  • Logo: la versión del repositorio que tienes subida en remoto

Uso de Gitkraken

Mientras eso suceda solo tendrás sincronizado tu ordenador con Gitkraken, pero no con Github. Para ello haremos click en Push (con Pull podrás forzar a tener en local lo mismo que en remoto).

Branchs

Como hemos mencionado ya en varias ocasiones, hay un elefante en la habitación que aún no hemos mentado: las ramas o branchs de un repositorio.

Imagina que estáis trabajando varios en un proyecto y teneís una versión que funciona pero que queréis modificar en paralelo a partir del estado actual del repositorio.

Las ramas nos permiten partir de una versión común del repositorio y hacer cambios que no afecten a los demás

Branchs

Para crear una rama a partir del estado actual de repositorio haremos click en Branch y le pondremos un nombre

Una vez creada verás dos iconos y un menú desplegable con las distintas ramas en las que quieres hacer el commit. Imagina que realizas un cambio pero no quieres añadirlo a la rama principal: puedes hacer el commit en tu rama propia en LOCAL (lo harás en la rama activa de tu menú de branchs).

Branchs

La primera vez te pedirá que escribas la rama en REMOTO con la quieres sincronizar tu rama en local. Consejo: ponle el mismo nombre en remoto que en local.

Branchs

Fíjate que ahora tenemos el ordenador y el logo en el mismo sitio. Esto no significa que tengas ambas ramas en tu local, solo que Gitkraken tiene ambas sincronizadas: clickando en cualquiera de ellas, tus archivos en tu ordenador cambiarán.

Pull request

Lo más recomendable es que solo se incorpore de una rama secundaria a la rama principal aquello que está validado por un/a coordinador/a del repositorio, asegurándose que todo funciona correctamente.

Cuando queramos incluirlo haremos click con botón derecho en el icono de la rama secundaria y seleccionamos Start a pull request to origin from...

Una pull request será una petición al responsable de la rama principal para incluir los cambios

Pull request

En el cuadro que no se abre deberemos escribir:

  • La rama a la que hacer el merge (normalmente la main)
  • Título y resumen de los cambios
  • Puedes incluso asignar un revisor entre los colaboradores del repo.
  • Puedes asignar etiquetas

Pull request

Mientras no se acepte aparecerá un icono de rama y un +1 en Pull Requests

Si somos al mantenedor del repositorio, haciendo click en el menú nos saldrán las ramas que nos quieren hacer hacer merge

Pull request

Al hacer click se abrirá un cuadro de Pull Request para decidir si

  • Revisar los cambios
  • Aprobar el merge
  • Añadir comentarios al que ha solicitado el merge por si queremos solicitar algún cambio antes de ser aprobado

Pull request

Tras revisar todo y aprobarlo clickaremos en Confirm merge, y tras ello podremos decidir si esa rama que era paralela a la principal la queremos eliminar o dejar visible a todos (consejo: dejar visible para tene trazabilidad del proyecto de trabajo)

Recursos extras

Algunos paquetes o recursos curiosos

Paletas de colores

Dataviz

Tras convertirlo a plotly se puede guardar automáticamente en html con https://plotly-r.com/saving.html y generar el código del iframe responsive con https://bhaskarvk.github.io/widgetframe/articles/Using_widgetframe.html

Dataviz

Mapas

Grafos y redes

Text mining

Datos curiosos

Informes, libros, diapositivas y webs

Tablas

¡GRACIAS!

Mi mail javalv09@ucm.es, Twitter @dadosdelaplace e Instagram @javieralvarezliebana para lo que queráis