Manejo de datos con R (I):
the tidyverse way

Programación y manejo de datos con R
(Web del curso aquí)

Data munging: the tidyverse way



Aprendimos a cargar datos, pero es raro que los datos estén preparados para empezar nuestro análisis, así que hay que “arreglar/limpiar” los datos. Para ello, tenemos que:

  1. Hacer nuestros datos TIDY

  2. Arreglarlos/modificarlos para que sean útiles para nuestros propósitos

Arreglando los datos

  • Aprenderemos a limpiar y transformar datos en R. Priorizaremos la forma moderna de hacer las cosas en R (o workflow) conocido como tidyverse.
  • El procesado/limpieza de los datos suele ocupar un 80% del tiempo de un análisis de datos; así que el workflow sería más bien así:

  • Classroom data are like teddy bears; real data are like a grizzly with salmon blood dripping out its mouth. — Jenny Bryan

¿qué es el tidyverse?

  • Un conjunto de paquetes que trabajan en armonía y que permiten una nueva forma de escribir/programar en R.

principales pkgs del Tidyverse

  • readr: para importar datos

  • tidyr: convertir a tidy data

  • dplyr: para manipular datos

  • ggplot2: para hacer gráficos

  • stringr: para manipular strings

  • … y algunos más

aquí nos centraremos principalmente en dos paquetes: dplyr y tidyr

“Filosofía” del Tidyverse

Programs must be written for people to read, and only incidentally for machines to execute – Hal Abelson

dos principios del tidyverse

  • Los scripts deben ser “fácilmente” legibles por las personas

  • Resolver problemas complejos encadenando funciones simples con el operador pipe (%>%)


El operador %>% es crucial en el tidyverse

  • El operador pipe se lo debemos a Stefan Bache que lo introdujo en 2014 en su pkg magrittr
  • La pipe ha tenido tanto éxito que en 2021 apareció una pipe en R-base: |> (la veremos en una extensión)

The pipe ( %>% )



  • El operador %>% es crucial en el tidyverse. Hay que saber usarlo y entenderlo

  • Es fácil. Pronto os sentiréis cómodos con él.

The pipe

  • La pipe es un operador que pasa el elemento que está a su izquierda como un argumento de la función que tiene a la derecha.



Igual os resulta más fácil entenderlo con ejemplos


  • Las siguientes 3 expresiones hacen exactamente lo mismo:
head(iris, n = 4)         #- forma habitual de llamar/usar la función head()

iris %>% head(. , n = 4)  #- usando el operador pipe (con el punto actuando como placeholder)

iris %>% head(n = 4)      #- usando el operador pipe (SIN el punto)

The pipe: ¿lo has entendido?


¿Qué hace la siguiente expresión?

4 %>% head(iris, .)

¿por qué no funciona la siguiente expresión?

4 %>% head(iris)

Intenta descubrir/entender qué hace la siguiente expresión:

letters %>% paste0( "-----" ,  .  ,  "!!!" ) %>% toupper

Pipe nativa (|>)

Mejoras en la pipe nativa

  • En la versión 4.2.0 se mejora la pipe nativa introduciendo un “placeholder”: el guion bajo _

    • Ojo: solo funciona con argumentos con nombre: iris |> head(x = _, n = 4)
  • Aun así, la pipe nativa tiene algunas limitaciones: aquí, aquí y en este vídeo

¿qué pasará en el futuro?

  • Pues parece que ahora sí que esto va en serio: el tidyverse (y R4DS 2ª ed.) ya usan |>

%>% versus |>: ¿en qué se diferencian?

  • Verás las dos: %>% en código “antiguo” (y en muchas de nuestras transparencias) y |> en el código más moderno
  • En la mayoría de los casos son intercambiables, pero hay algunas diferencias:

    • El placeholder en %>%: es el punto .; mientas que en |>: es el guion bajo _

    • El placeholder de |> necesita que los argumentos tengan nombre

    • Paréntesis: %>% deja escribir la función sin paréntesis; mientras que |> exige ponerlos

    • Paquetes: %>% necesita cargar un paquete (magrittr, o tidyverse); |> no, ya está incluido en R-base

iris %>% head(n = 4)        #- funciona
iris %>% head(., n = 4)     #- funciona
iris %>% head               #- funciona
4 %>% head(iris, .)         #- funciona
iris |> head(n = 4)         #- funciona
iris |> head(x = _, n = 4)  #- funciona
iris |> head                #- ERROR: faltan los ()
4 |> head(iris, _)          #- ERROR: _ sin nombre


Truquillo: el atajo de teclado Ctrl + Shift + M escribe la pipe por ti

  • En RStudio puedes elegir que pipe escribe el atajo.

  • Para cambiarlo tienes que seguir esta ruta de menús: Tools > Global Options > Code > Use native pipe operator, |>

Tidy data



  • Para trabajar à la tidyverse es crucial que los datos sean “tidy”
  • El concepto de datos tidy es sencillo. Hacer los datos tidy no tanto, pero tenemos un paquete para hacerlo fácil: el pkg tidyr

Un aspecto importante del tidyverse es hacer los datos tidy

  • Unos datos son tidy si: (1) cada columna es una variable, (2) cada fila es una observación y (3) cada valor está en, o tiene, su propia celda.

Parece fácil, y lo es: realmente es la situación a la que estamos acostumbrados, pero …
… mejor desarrollarlo con unos ejemplos

Tidy data … ejemplo I

¿Estos datos son tidy?

df_1 <- data.frame(
            year  = c(2014, 2015, 2016),  
            Pedro = c(100, 500, 200), 
            Carla = c(400, 600, 250), 
            María = c(200, 700, 900)  )
gt::gt(df_1)
year Pedro Carla María
2014 100 400 200
2015 500 600 700
2016 200 250 900
  • No, no son tidy: los nombres de las columnas (Pedro, Carla, María) son valores de una variable (“persona”)

Tidy data … ejemplo II

¿Estos datos son tidy?

df_2 <- data.frame(students = c("Pedro", "Carla", "María"), 
                      w_2014 = c(100, 400, 200), 
                      w_2015 = c(500, 600, 700),
                      w_2016 = c(200, 250, 900)   )
gt::gt(df_2)
students w_2014 w_2015 w_2016
Pedro 100 500 200
Carla 400 600 250
María 200 700 900
  • No, tampoco son tidy: los años están en los nombres de las columnas (w_2014, …), pero son valores de la variable “año”

Tidy data … ejemplo III

¿Estos datos son tidy?

df_3 <- data.frame(
            students = rep(c("Pedro", "Carla", "María"), times = 3),  
            year = rep(c(2014, 2015, 2016), each = 3),
            salario = c(100, 400, 200, 500, 600, 700, 200, 250,900) )
gt::gt(df_3)
students year salario
Pedro 2014 100
Carla 2014 400
María 2014 200
Pedro 2015 500
Carla 2015 600
María 2015 700
Pedro 2016 200
Carla 2016 250
María 2016 900
  • Sí, estos sí son datos tidy

  • Son más difíciles de leer por las personas, pero es que los datos los leen las máquinas!!

Los datos tidy suelen estar en formato largo o long

Tidy data en formato LONG



  • Para trabajar à la tidyverse es crucial que los datos sean “tidy” (en formato LONG)

  • Es importante aprender a pasar de datos WIDE a LONG y viceversa

  • Con el pkg tidyr es sencillo pero …

De wide a LONG format con … tidyr::pivot_longer()


Tarea: convierte “df_wide” a formato LARGO

Aquí tienes un df en formato ANCHO, pásalo a formato LARGO

df_wide <- data.frame(students = c("Pedro", "Carla", "María"), 
                        w_2014 = c(100, 400, 200), 
                        w_2015 = c(500, 600, 700),
                        w_2016 = c(200, 250, 900) )

Está en formato ancho:

students w_2014 w_2015 w_2016
Pedro 100 500 200
Carla 400 600 250
María 200 700 900

Lo quiero en formato largo:

students name value
Pedro w_2014 100
Pedro w_2015 500
Pedro w_2016 200
Carla w_2014 400
Carla w_2015 600
Carla w_2016 250
María w_2014 200
María w_2015 700
María w_2016 900


  • Te ayudará visualizar df_wide y ver qué columnas contienen valores de la misma variable


  • Tienes que usar tidyr::pivot_longer() y en su primer argumento le tienes que decir qué columnas hay que convertir a formato “largo”


El df original en formato ancho:

students w_2014 w_2015 w_2016
Pedro 100 500 200
Carla 400 600 250
María 200 700 900
#- la función pivot_longer() transforma los datos de formato ancho(wide) a formato largo(long)
df_long <- df_wide %>% 
             tidyr::pivot_longer(cols = 2:4)

gt::gt(df_long)
students name value
Pedro w_2014 100
Pedro w_2015 500
Pedro w_2016 200
Carla w_2014 400
Carla w_2015 600
Carla w_2016 250
María w_2014 200
María w_2015 700
María w_2016 900
#- con names_to y values_to elegimos el nombre de las dos columnas nuevas
df_long <- df_wide %>% 
             tidyr::pivot_longer(cols = 2:4, 
                                 names_to = "periodo", 
                                 values_to = "salario")
gt::gt(df_long)
students periodo salario
Pedro w_2014 100
Pedro w_2015 500
Pedro w_2016 200
Carla w_2014 400
Carla w_2015 600
Carla w_2016 250
María w_2014 200
María w_2015 700
María w_2016 900
df_long <- df_wide %>% 
1  tidyr::pivot_longer(cols = starts_with("w_"),
2                      names_to = "year",
3                      values_to = "salario",
4                      names_prefix = "w_",
5                      names_transform = as.integer)
1
Elegimos las columnas por su nombre (no por su posición, que es más frágil). También valdría cols = -students
2
Nombre de la nueva columna que contendrá los antiguos nombres de columna
3
Nombre de la nueva columna que contendrá los valores
4
[🌶] Quitamos el prefijo w_ de los nombres: "w_2014" pasa a "2014"
5
[🌶] Convertimos el año a número entero
students year salario
Pedro 2014 100
Pedro 2015 500
Pedro 2016 200
Carla 2014 400
Carla 2015 600
Carla 2016 250
María 2014 200
María 2015 700
María 2016 900

De long a WIDE format con tidyr::pivot_wider()

¿Por qué querríamos convertirlo a formato ANCHO?

  • Para presentar tablas a personas: el formato ancho es más fácil de leer
  • Para hacer cálculos entre columnas: por ejemplo, la variación del salario entre 2014 y 2016
  • Porque algunas funciones o modelos necesitan los datos en formato ancho

Tarea: convierte “df_long” a formato ANCHO

  • df_long está en formato LARGO, pásalo a formato ANCHO

  • Tienes que distribuir los datos de salario en columnas: una columna para cada año

  • Partimos del df_long de la “Solución ampliada” de la tarea anterior (con las columnas year y salario)

Está en formato largo:

students year salario
Pedro 2014 100
Pedro 2015 500
Pedro 2016 200
Carla 2014 400
Carla 2015 600
Carla 2016 250
María 2014 200
María 2015 700
María 2016 900

Lo quiero en formato ancho: (con una columna para cada año)

students 2014 2015 2016
Pedro 100 500 200
Carla 400 600 250
María 200 700 900
  • Tienes que usar tidyr::pivot_wider()


  • En el argumento values_from le tienes que decir qué columna quieres que sus valores se distribuyan entre varias columnas


  • En el argumento names_from le tienes que decir qué columna quieres que sus valores aparezcan como los títulos de las nuevas columnas que se crearán

El df en formato largo:

gt::gt(df_long)
students year salario
Pedro 2014 100
Pedro 2015 500
Pedro 2016 200
Carla 2014 400
Carla 2015 600
Carla 2016 250
María 2014 200
María 2015 700
María 2016 900
df_wide2 <- df_long %>% 
              tidyr::pivot_wider(values_from = salario,  
                                 names_from = year)


gt::gt(df_wide2)
students 2014 2015 2016
Pedro 100 500 200
Carla 400 600 250
María 200 700 900

Tarea: convierte OTRA VEZ “df_long” a formato ANCHO

  • df_long está en formato LARGO, pásalo a formato ANCHO

  • Tienes que distribuir los datos de salario en columnas: PERO esta vez quiero que haya una columna para cada persona (Pedro, Carla, …)

Está en formato largo:

gt::gt(df_long)
students year salario
Pedro 2014 100
Pedro 2015 500
Pedro 2016 200
Carla 2014 400
Carla 2015 600
Carla 2016 250
María 2014 200
María 2015 700
María 2016 900

Lo quiero en formato ancho:

(con una columna para cada PERSONA)

year Pedro Carla María
2014 100 400 200
2015 500 600 700
2016 200 250 900
  • Tienes que usar tidyr::pivot_wider()


  • En el argumento values_from le tienes que decir qué columna quieres que sus valores se distribuyan entre varias columnas


  • CUIDADO!!! En el argumento names_from le tienes que decir qué columna quieres que sus valores aparezcan como los títulos de las nuevas columnas que se crearán
df_wide3 <- df_long %>% 
              tidyr::pivot_wider(values_from = salario, 
                                 names_from = students)


gt::gt(df_wide3)
year Pedro Carla María
2014 100 400 200
2015 500 600 700
2016 200 250 900

Dos funciones más de tidyr



  • El paquete tidyr tiene muchas funciones. Veremos dos: separate() y unite()

las funciones separate() y unite()


  • separate() y unite() nos facilitarán poder separar y unir columnas


  • Por ejemplo, fíjate en el siguiente dataframe: ¿y si quisiera ordenarlo por apellido?
personajes year
Pedro Navaja 1978
Bob Dylan 1941
Peter Pan 1904
  • Necesitaríamos tener el apellido en una columna propia: hay que separar la columna personajes

Tarea: Utilizando tidyr::separate()

  • Separa la columna personajes en dos columnas: nombre y apellido
df <- data.frame( personajes = c("Pedro Navaja", "Bob Dylan", "Peter Pan"), 
                  year  = c(1978, 1941, 1904) )
gt::gt(df)
personajes year
Pedro Navaja 1978
Bob Dylan 1941
Peter Pan 1904
  • Has de usar tidyr::separate()


  • Tendrás que darle los valores de 3 argumentos: col, into y sep

    • en el argumento col tienes que decir qué columna quieres separar

    • en into tienes que decir qué nombres quieres que tengan las nuevas columnas

    • en sep tienes que decir qué carácter separa los dos elementos

df_1 <- df %>% 
  tidyr::separate(col = personajes, 
                  into = c("nombre", "apellido"), 
                  sep = " ")


gt::gt(df)
personajes year
Pedro Navaja 1978
Bob Dylan 1941
Peter Pan 1904
gt::gt(df_1)
nombre apellido year
Pedro Navaja 1978
Bob Dylan 1941
Peter Pan 1904
  • ¿Y si un personaje tiene un apellido compuesto?
df_3 <- data.frame(personajes = c("Pedro Navaja", "Pedro de la Vega"), 
1                   year  = c(1978, 2000))
1
Un data.frame con un apellido compuesto: “de la Vega”
personajes year
Pedro Navaja 1978
Pedro de la Vega 2000


1df_3 %>%
  tidyr::separate(col = personajes, into = c("nombre", "apellido"), sep = " ")
1
R avisa (“Additional pieces discarded”) y se pierde parte del apellido: queda solo “de”
nombre apellido year
Pedro Navaja 1978
Pedro de 2000


df_3 %>% 
  tidyr::separate(col = personajes, into = c("nombre", "apellido"), sep = " ", 
1                  extra = "merge")
1
Con extra = "merge" el resto de trozos se quedan juntos en la última columna: “de la Vega”
nombre apellido year
Pedro Navaja 1978
Pedro de la Vega 2000

Tarea: Utilizando tidyr::unite()

  • Partimos de df_1, el resultado de la tarea anterior (con las columnas nombre y apellido)

  • Une las columnas nombre y apellido en una sola columna llamada nombre_y_apellido, con el formato "apellido, nombre"

Esto es lo que tenemos (df_1):

nombre apellido year
Pedro Navaja 1978
Bob Dylan 1941
Peter Pan 1904

Esto es lo que quiero:

nombre_y_apellido year
Navaja, Pedro 1978
Dylan, Bob 1941
Pan, Peter 1904
  • Has de usar tidyr::unite()


  • Tendrás que darle los valores de “3” argumentos:

    • en el argumento col tienes que decir el nombre de la nueva columna que se va a crear

    • después tienes que decir qué columnas quieres unir

    • en sep tienes que decir qué carácter separará los dos elementos que vamos a unir

df_2 <- df_1 %>% 
       tidyr::unite(col = nombre_y_apellido, 
                    c(apellido, nombre), 
                    sep = ", ")


gt::gt(df_1)
nombre apellido year
Pedro Navaja 1978
Bob Dylan 1941
Peter Pan 1904
gt::gt(df_2)
nombre_y_apellido year
Navaja, Pedro 1978
Dylan, Bob 1941
Pan, Peter 1904

Extensión: las nuevas funciones para separar (y unir) columnas

  • Desde la versión 1.3.0 de tidyr (2023), separate() está superseded: sigue funcionando, pero ya no se desarrolla. Sus sustitutas son la familia separate_wider_*() y separate_longer_*()
  • Hace lo mismo que separate(): separa una columna en varias columnas.

  • Cambian los nombres de los argumentos: cols, delim y names

df %>% 
  tidyr::separate(
    col  = personajes, 
    sep  = " ", 
    into = c("nombre", "apellido"))
df %>% 
  tidyr::separate_wider_delim(
    cols  = personajes, 
    delim = " ", 
    names = c("nombre", "apellido"))


  • Funciones hermanas:

    • separate_wider_position() (separa por posición)

    • separate_wider_regex() (con expresiones regulares)

  • Si hay más (o menos) trozos que nombres, separate() solo avisaba; separate_wider_delim() da un error, y hay que decirle qué hacer con too_many (y too_few)
df_3 %>% 
  tidyr::separate_wider_delim(
    cols  = personajes, 
    delim = " ", 
    names = c("nombre", "apellido"), 
1    too_many = "merge")
1
too_many = "merge" deja los trozos que sobran en la última columna. Con too_many = "debug" añade columnas para ver dónde está el problema
nombre apellido year
Pedro Navaja 1978
Pedro de la Vega 2000
  • Separa una columna en varias filas (sustituye a separate_rows())
df_4 <- data.frame(student = c("Pedro", "Carla"), 
                   cursos  = c("R, Python", "R, SQL, Excel"))
student cursos
Pedro R, Python
Carla R, SQL, Excel


df_4 %>% 
  tidyr::separate_longer_delim(
    cols  = cursos, 
    delim = ", ")
student cursos
Pedro R
Pedro Python
Carla R
Carla SQL
Carla Excel
  • unite() no ha cambiado: no está superseded y no tiene una función nueva que la sustituya

  • La alternativa habitual es crear la columna con dplyr::mutate() (lo veremos en el siguiente tema) y una función que pegue textos, como paste() o stringr::str_c():

1df_1 %>% dplyr::mutate(nombre_y_apellido = paste(apellido, nombre, sep = ", "))
1
A diferencia de unite(), mutate() no elimina las columnas originales
  nombre apellido year nombre_y_apellido
1  Pedro   Navaja 1978     Navaja, Pedro
2    Bob    Dylan 1941        Dylan, Bob
3  Peter      Pan 1904        Pan, Peter

Valores perdidos (NA) con tidyr



  • Los datos reales casi siempre tienen valores perdidos (NA). tidyr tiene varias funciones para tratarlos

Algunas funciones de tidyr para los NA

  • Partimos de este data.frame, que tiene varios NA:
df_na <- data.frame(students = c("Pedro", NA, "Carla"), 
                    year     = c(2014, 2015, 2014), 
                    salario  = c(NA, 500, 400))
students year salario
Pedro 2014 NA
NA 2015 500
Carla 2014 400
  • Elimina las filas que tienen algún NA …
df_na %>% tidyr::drop_na()
students year salario
Carla 2014 400
  • … o solo las que tienen NA en las columnas que le digamos
df_na %>% tidyr::drop_na(salario)
students year salario
NA 2015 500
Carla 2014 400
  • Rellena los NA con el valor anterior (muy útil con ficheros Excel con celdas combinadas)
df_na %>% tidyr::fill(students)
students year salario
Pedro 2014 NA
Pedro 2015 500
Carla 2014 400
  • Sustituye los NA por el valor que le digamos
df_na %>% tidyr::replace_na(list(students = "desconocido"))
students year salario
Pedro 2014 NA
desconocido 2015 500
Carla 2014 400
  • ⚠️ Cuidado: rellenar un NA numérico con 0 (p. ej. un salario desconocido) cambia los resultados (medias, totales, …). Un salario desconocido no es un salario 0
  • Hace explícitos los NA “ocultos”: filas que faltan en los datos. En df_na no hay ninguna fila para Carla en 2015

  • complete() crea las combinaciones de students y year que faltan (antes rellenamos students con fill())

df_na %>% 
  tidyr::fill(students) %>% 
  tidyr::complete(students, year)
students year salario
Carla 2014 400
Carla 2015 NA
Pedro 2014 NA
Pedro 2015 500

Práctica: datos reales

  • El paquete tidyr trae el data.frame billboard: la posición de canciones en la lista Billboard del año 2000, semana a semana
df <- tidyr::billboard
artist track date.entered wk1 wk2 wk3 wk4 wk5
2 Pac Baby Don't Cry (Keep... 2000-02-26 87 82 72 77 87
2Ge+her The Hardest Part Of ... 2000-09-02 91 87 92 NA NA
3 Doors Down Kryptonite 2000-04-08 81 70 68 67 66
3 Doors Down Loser 2000-10-21 76 76 72 69 67
  1. ¿Son datos tidy? ¿Por qué?

  2. Pásalos a formato largo, con una columna week (que sea un número) y otra rank

  3. [🌶] Elimina las filas con NA (las semanas en las que la canción ya no estaba en la lista)

  • Las columnas wk1, wk2, … wk76 son valores de la variable “semana”

  • En pivot_longer(): cols = starts_with("wk"), y fíjate en los argumentos names_prefix y names_transform

  • Para los NA: la función drop_na()

  1. No son tidy: las columnas wk1, wk2, … no son variables, son valores de la variable “semana”

  2. Con pivot_longer(). Para que week sea un número: names_prefix quita el "wk" y as.integer() lo convierte

  3. [🌶] Para quitar los NA: la función drop_na() o el argumento values_drop_na = TRUE

Con funciones:

df_long <- df %>% 
  tidyr::pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank", 
    names_prefix = "wk") %>% 
  dplyr::mutate(week = as.integer(week)) %>% 
  tidyr::drop_na(rank)

Con argumentos de pivot_longer():

df_long <- df %>% 
  tidyr::pivot_longer(
    cols = starts_with("wk"), 
    names_to = "week", 
    values_to = "rank", 
    names_prefix = "wk", 
    names_transform = as.integer, 
    values_drop_na = TRUE)
artist track date.entered week rank
2 Pac Baby Don't Cry (Keep... 2000-02-26 1 87
2 Pac Baby Don't Cry (Keep... 2000-02-26 2 82
2 Pac Baby Don't Cry (Keep... 2000-02-26 3 72
2 Pac Baby Don't Cry (Keep... 2000-02-26 4 77
2 Pac Baby Don't Cry (Keep... 2000-02-26 5 87

Resumen



Para … usamos …
encadenar funciones la pipe: %>% o |>
pasar de ancho a largo tidyr::pivot_longer()
pasar de largo a ancho tidyr::pivot_wider()
separar una columna en varias tidyr::separate() (o la nueva separate_wider_delim())
unir varias columnas en una tidyr::unite()
tratar los NA tidyr::drop_na(), fill(), replace_na(), complete()



En el siguiente tema: manipular datos con dplyr