Capítulo 21 Cómo cambiar el formato de datos

Como hemos visto a través del libro, tener datos en formato tidy es lo que hace que el tidyverse fluya. Después del primer paso en el proceso de análisis de datos, la importación de datos, un siguiente paso común es cambiar la forma de los datos a una que facilite el resto del análisis. El paquete tidyr incluye varias funciones útiles para poner los datos en formato tidy.

Utilizaremos el set de datos en formato ancho fertility, descrito en la Sección 4.1, como ejemplo en esta sección.

library(tidyverse)
library(dslabs)
path <- system.file("extdata", package="dslabs")
filename <- file.path(path, "fertility-two-countries-example.csv")
wide_data <- read_csv(filename)

21.1 pivot_longer

Una de las funciones más usadas del paquete tidyr es pivot_longer, que nos permite convertir datos anchos (wide data en inglés) en datos tidy.

Igual que con la mayoría de las funciones de tidyverse, el primer argumento de la función pivot_longer es el data frame que será procesado. Aquí queremos cambiar la forma del set de datos wide_data para que cada fila represente una observación de fertilidad, que implica que necesitamos tres columnas para almacenar el año, el país y el valor observado. En su forma actual, los datos de diferentes años están en diferentes columnas con los valores de año almacenados en los nombres de las columnas. A través de los argumentos names_to y values_to, le daremos a pivot_longer los nombres de columna que le queremos asignar a las columnas que contienen los nombres de columna y las observaciones actuales, respectivamente. Por defecto, estos nombres son name (nombre) y value (valor), los cuales son buenas opciones en general. En este caso, una mejor opción para estos dos argumentos serían year y fertility. Noten que ninguna parte del archivo nos dice que se trata de datos de fertilidad. En cambio, desciframos esto del nombre del archivo. A través cols, el segundo argumento, especificamos las columnas que contienen los valores observados; estas son las columnas que serán pivotadas. La acción por defecto es recopilar todas las columnas, por lo que, en la mayoría de los casos, tenemos que especificar las columnas. En nuestro ejemplo queremos las columnas 1960, 1961 hasta 2015.

El código para recopilar los datos de fertilidad se ve así:

new_tidy_data <- pivot_longer(wide_data, `1960`:`2015`, 
                              names_to = "year", values_to = "fertility")

También podemos usar el pipe de esta manera:

new_tidy_data <- wide_data |> 
  pivot_longer(`1960`:`2015`, names_to = "year", values_to = "fertility")

Podemos ver que los datos se han convertido al formato tidy con columnas year y fertility:

head(new_tidy_data)
#> # A tibble: 6 × 3
#>   country year  fertility
#>   <chr>   <chr>     <dbl>
#> 1 Germany 1960       2.41
#> 2 Germany 1961       2.44
#> 3 Germany 1962       2.47
#> 4 Germany 1963       2.49
#> 5 Germany 1964       2.49
#> # … with 1 more row

y que cada año resultó en dos filas ya que tenemos dos países y la columna de los países no se recopiló. Una forma un poco más rápida de escribir este código es especificar qué columna no se recopilará, en lugar de todas las columnas que se recopilarán:

new_tidy_data <- wide_data |>
  pivot_longer(-country, names_to = "year", values_to = "fertility")

El objeto new_tidy_data se parece al original tidy_data que definimos de esta manera:

data("gapminder")
tidy_data <- gapminder |>
  filter(country %in% c("South Korea", "Germany") & !is.na(fertility)) |>
  select(country, year, fertility)

con solo una pequeña diferencia. ¿La pueden ver? Miren el tipo de datos de la columna del año:

class(tidy_data$year)
#> [1] "integer"
class(new_tidy_data$year)
#> [1] "character"

La función pivot_longer supone que los nombres de columna son caracteres. Así que necesitamos un poco más de wrangling antes de poder graficar. Necesitamos convertir la columna con los años en números. La función pivot_longer incluye el argumento convert para este propósito:

new_tidy_data <- wide_data |>
  pivot_longer(-country, names_to = "year", values_to = "fertility") |>
  mutate(year = as.integer(year))

Tengan en cuenta que también podríamos haber utilizado mutate y as.numeric.

Ahora que los datos están tidy, podemos usar este código relativamente sencillo de ggplot2:

new_tidy_data |> ggplot(aes(year, fertility, color = country)) + 
  geom_point()

21.2 pivot_wider

Como veremos en ejemplos posteriores, a veces es útil convertir datos tidy en datos anchos para fines de wrangling de datos. A menudo usamos esto como un paso intermedio para convertir los datos en formato tidy. La función pivot_wider es básicamente la inversa de pivot_longer. El primer argumento es para los datos, pero como estamos usando el pipe, no lo mostramos. El argumento names_from le dice a pivot_longer qué variable usar como nombre de columna. El argumento names_to especifica qué variable usar para completar las celdas:

new_wide_data <- new_tidy_data |> 
  pivot_wider(names_from = year, values_from = fertility)
select(new_wide_data, country, `1960`:`1967`)
#> # A tibble: 2 × 9
#>   country     `1960` `1961` `1962` `1963` `1964` `1965` `1966` `1967`
#>   <chr>        <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>  <dbl>
#> 1 Germany       2.41   2.44   2.47   2.49   2.49   2.48   2.44   2.37
#> 2 South Korea   6.16   5.99   5.79   5.57   5.36   5.16   4.99   4.85

Similar a pivot_wider, names_from y values_from son name and value por defecto.

21.3 separate

El wrangling de datos que mostramos arriba es sencillo en comparación con lo que generalmente se requiere. En nuestros archivos de hoja de cálculo que usamos como ejemplo, incluimos una ilustración que es un poco más complicada. Contiene dos variables: esperanza de vida y fertilidad. Sin embargo, la forma en que se almacena no es tidy y, como explicaremos, no es óptima.

path <- system.file("extdata", package = "dslabs")

filename <- "life-expectancy-and-fertility-two-countries-example.csv"
filename <- file.path(path, filename)

raw_dat <- read_csv(filename)
select(raw_dat, 1:5)
#> # A tibble: 2 × 5
#>   country     `1960_fertility` `1960_life_expectancy` `1961_fertility`
#>   <chr>                  <dbl>                  <dbl>            <dbl>
#> 1 Germany                 2.41                   69.3             2.44
#> 2 South Korea             6.16                   53.0             5.99
#> # … with 1 more variable: `1961_life_expectancy` <dbl>

Primero, tengan en cuenta que los datos están en formato ancho. Además, observen que esta tabla incluye valores para dos variables, fertilidad y esperanza de vida, con el nombre (en inglés) de la columna codificando qué columna representa qué variable. No recomendamos codificar la información en los nombres de las columnas, pero, desafortunadamente, es algo bastante común. Usaremos nuestras habilidades de wrangling para extraer esta información y almacenarla de manera tidy.

Podemos comenzar el wrangling de datos con la función pivot_longer, pero ya no deberíamos usar el nombre de la columna year para la nueva columna, dado que también contiene el tipo de variable. La nombraremos name, el valor predeterminado, por ahora:

dat <- raw_dat |> pivot_longer(-country)
head(dat)
#> # A tibble: 6 × 3
#>   country name                 value
#>   <chr>   <chr>                <dbl>
#> 1 Germany 1960_fertility        2.41
#> 2 Germany 1960_life_expectancy 69.3 
#> 3 Germany 1961_fertility        2.44
#> 4 Germany 1961_life_expectancy 69.8 
#> 5 Germany 1962_fertility        2.47
#> # … with 1 more row

El resultado no es exactamente lo que llamamos tidy ya que cada observación está asociada con dos filas en vez de una. Queremos tener los valores de las dos variables, fertility y life_expectancy, en dos columnas separadas. El primer reto para lograr esto es separar la columna name en año y tipo de variable. Observen que las entradas en esta columna separan el año del nombre de la variable con una barra baja:

dat$name[1:5]
#> [1] "1960_fertility"       "1960_life_expectancy" "1961_fertility"      
#> [4] "1961_life_expectancy" "1962_fertility"

Codificar múltiples variables en el nombre de una columna es un problema tan común que el paquete readr incluye una función para separar estas columnas en dos o más. Aparte de los datos, la función separate toma tres argumentos: el nombre de la columna que se separará, los nombres que se utilizarán para las nuevas columnas y el carácter que separa las variables. Entonces, un primer intento de hacer esto es:

dat |> separate(name, c("year", "name"), "_")

separate supone por defecto que_ es el separador y, por eso, no tenemos que incluirlo en el código:

dat |> separate(name, c("year", "name"))
#> Warning: Expected 2 pieces. Additional pieces discarded in 112 rows [2,
#> 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38,
#> 40, ...].
#> # A tibble: 224 × 4
#>   country year  name      value
#>   <chr>   <chr> <chr>     <dbl>
#> 1 Germany 1960  fertility  2.41
#> 2 Germany 1960  life      69.3 
#> 3 Germany 1961  fertility  2.44
#> 4 Germany 1961  life      69.8 
#> 5 Germany 1962  fertility  2.47
#> # … with 219 more rows

La función separa los valores, pero nos encontramos con un nuevo problema. Recibimos la advertencia Too many values at 112 locations: y la variable life_expectancy se corta a life. Esto es porque el _ se usa para separar life y expectancy, no solo el año y el nombre de la variable. Podríamos añadir una tercera columna para guardar esto y dejar que la función separate sepa cual columna llenar con los valores faltantes, NA, cuando no hay un tercer valor. Aquí le decimos que llene la columna de la derecha:

var_names <- c("year", "first_variable_name", "second_variable_name")
dat |> separate(name, var_names, fill = "right")
#> # A tibble: 224 × 5
#>   country year  first_variable_name second_variable_name value
#>   <chr>   <chr> <chr>               <chr>                <dbl>
#> 1 Germany 1960  fertility           <NA>                  2.41
#> 2 Germany 1960  life                expectancy           69.3 
#> 3 Germany 1961  fertility           <NA>                  2.44
#> 4 Germany 1961  life                expectancy           69.8 
#> 5 Germany 1962  fertility           <NA>                  2.47
#> # … with 219 more rows

Sin embargo, si leemos el archivo de ayuda de separate, encontramos que un mejor enfoque es fusionar las dos últimas variables cuando hay una separación adicional:

dat |> separate(name, c("year", "name"), extra = "merge")
#> # A tibble: 224 × 4
#>   country year  name            value
#>   <chr>   <chr> <chr>           <dbl>
#> 1 Germany 1960  fertility        2.41
#> 2 Germany 1960  life_expectancy 69.3 
#> 3 Germany 1961  fertility        2.44
#> 4 Germany 1961  life_expectancy 69.8 
#> 5 Germany 1962  fertility        2.47
#> # … with 219 more rows

Esto logra la separación que queríamos. Sin embargo, aún no hemos terminado. Necesitamos crear una columna para cada variable. Como aprendimos, la función pivot_wider hace eso:

dat |>
  separate(name, c("year", "name"), extra = "merge") |>
  pivot_wider()
#> # A tibble: 112 × 4
#>   country year  fertility life_expectancy
#>   <chr>   <chr>     <dbl>           <dbl>
#> 1 Germany 1960       2.41            69.3
#> 2 Germany 1961       2.44            69.8
#> 3 Germany 1962       2.47            70.0
#> 4 Germany 1963       2.49            70.1
#> 5 Germany 1964       2.49            70.7
#> # … with 107 more rows

Los datos ahora están en formato tidy con una fila para cada observación con tres variables: año, fertilidad y esperanza de vida.

21.4 unite

A veces es útil hacer el inverso de separate, es decir, unir dos columnas en una. Para demostrar cómo usar unite, mostramos un código que, aunque no es el acercamiento óptimo, sirve como ilustración. Supongan que no supiéramos sobre extra y usáramos este comando para separar:

var_names <- c("year", "first_variable_name", "second_variable_name")
dat |>
  separate(name, var_names, fill = "right")
#> # A tibble: 224 × 5
#>   country year  first_variable_name second_variable_name value
#>   <chr>   <chr> <chr>               <chr>                <dbl>
#> 1 Germany 1960  fertility           <NA>                  2.41
#> 2 Germany 1960  life                expectancy           69.3 
#> 3 Germany 1961  fertility           <NA>                  2.44
#> 4 Germany 1961  life                expectancy           69.8 
#> 5 Germany 1962  fertility           <NA>                  2.47
#> # … with 219 more rows

Podemos lograr el mismo resultado final uniendo las segunda y tercera columnas, luego esparciendo las columnas usando pivot_wider y renombrando fertility_NA a fertility:

dat |>
  separate(name, var_names, fill = "right") |>
  unite(name, first_variable_name, second_variable_name) |>
  pivot_wider() |>
  rename(fertility = fertility_NA)
#> # A tibble: 112 × 4
#>   country year  fertility life_expectancy
#>   <chr>   <chr>     <dbl>           <dbl>
#> 1 Germany 1960       2.41            69.3
#> 2 Germany 1961       2.44            69.8
#> 3 Germany 1962       2.47            70.0
#> 4 Germany 1963       2.49            70.1
#> 5 Germany 1964       2.49            70.7
#> # … with 107 more rows

21.5 Ejercicios

1. Ejecute el siguiente comando para definir el objeto co2_wide:

co2_wide <- data.frame(matrix(co2, ncol = 12, byrow = TRUE)) |>
  setNames(1:12) |>
  mutate(year = as.character(1959:1997))

Utilice la función pivot_longer para wrangle esto en un set de datos tidy. Nombre a la columna con las mediciones de CO2 co2 y nombre a la columna de mes month. Nombre al objeto resultante co2_tidy.

2. Grafique CO2 versus mes con una curva diferente para cada año usando este código:

co2_tidy |> ggplot(aes(month, co2, color = year)) + geom_line()

Si no se realiza el gráfico esperado, probablemente es porque co2_tidy$month no es numérico:

class(co2_tidy$month)

Reescriba el código y que asegúrese que la columna de mes será numérica. Luego haga el gráfico

3. ¿Qué aprendemos de este gráfico?

  1. Las medidas de CO2 aumentan monotónicamente de 1959 a 1997.
  2. Las medidas de CO2 son más altas en el verano y el promedio anual aumentó de 1959 a 1997.
  3. Las medidas de CO2 parecen constantes y la variabilidad aleatoria explica las diferencias.
  4. Las medidas de CO2 no tienen una tendencia estacional.

4. Ahora cargue el set de datos admissions, que contiene información de admisión para hombres y mujeres en seis concentraciones y mantenga solo la columna de porcentaje admitido:

load(admissions)
dat <- admissions |> select(-applicants)

Si pensamos en una observación como una concentración, y que cada observación tiene dos variables (porcentaje de hombres admitidos y porcentaje de mujeres admitidas), entonces esto no es tidy. Utilice la función pivot_wider para wrangle en la forma tidy que queremos: una fila para cada concentración.

5. Ahora intentaremos un reto más avanzado de wrangling. Queremos wrangle los datos de admisión para cada concentración para tener 4 observaciones: admitted_men, admitted_women, applicants_men y applicants_women. El “truco” que hacemos aquí es realmente bastante común: primero usamos pivot_longer para generar un data frame intermedio y luego usamos pivot_wider para obtener los datos tidy que queremos. Iremos paso a paso en este y en los próximos dos ejercicios.

Utilice la función pivot_longer para crear un data frame tmp con una columna que contiene el tipo de observación admitted o applicants. Nombre a las nuevas columnas name y value.

6. Ahora tiene un objeto tmp con columnas major, gender, name y value. Tenga en cuenta que si combina name y gender, se obtienen los nombres de columna que queremos: admitted_men, admitted_women, applicants_men y applicants_women. Use la función unite para crear una nueva columna llamada column_name.

7. Ahora use la función pivot_wider para generar los datos tidy con cuatro variables para cada concentración.

8. Ahora use el pipe para escribir una línea de código que convierta admissions en la tabla producida en el ejercicio anterior.