Capítulo 4 tidyverse

Hasta ahora hemos estado manipulando vectores reordenándolos y creando subconjuntos mediante la indexación. Sin embargo, una vez comencemos los análisis más avanzados, la unidad preferida para el almacenamiento de datos no es el vector sino el data frame. En este capítulo aprenderemos a trabajar directamente con data frames, que facilitan enormemente la organización de información. Utilizaremos data frames para la mayoría de este libro. Nos enfocaremos en un formato de datos específico denominado tidy y en una colección específica de paquetes que son particularmente útiles para trabajar con data tidy y que se denomina el tidyverse.

Podemos cargar todos los paquetes del tidyverse a la vez al instalar y cargar el paquete tidyverse:

library(tidyverse)

Aprenderemos cómo implementar el enfoque tidyverse a lo largo del libro, pero antes de profundizar en los detalles, en este capítulo presentamos algunos de los aspectos más utilizadas del tidyverse, comenzando con el paquete dplyr para manipular los data frames y el paquete purrr para trabajar con las funciones. Tengan en cuenta que el tidyverse también incluye un paquete para graficar, ggplot2, que presentaremos más adelante en el Capítulo 7 en la parte de visualización de datos del libro, el paquete readr discutido en el Capítulo 5 y muchos otros. En este capítulo, primero presentamos el concepto de datos tidy y luego demostramos cómo usamos el tidyverse para trabajar con data frames en este formato.

4.1 Datos tidy

Decimos que una tabla de datos está en formato tidy si cada fila representa una observación y las columnas representan las diferentes variables disponibles para cada una de estas observaciones. El set de datos murders es un ejemplo de un data frame tidy.

#>        state abb region population total
#> 1    Alabama  AL  South    4779736   135
#> 2     Alaska  AK   West     710231    19
#> 3    Arizona  AZ   West    6392017   232
#> 4   Arkansas  AR  South    2915918    93
#> 5 California  CA   West   37253956  1257
#> 6   Colorado  CO   West    5029196    65

Cada fila representa un estado con cada una de las cinco columnas proveyendo una variable diferente relacionada con estos estados: nombre, abreviatura, región, población y total de asesinatos.

Para ver cómo se puede proveer la misma información en diferentes formatos, consideren el siguiente ejemplo:

#>       country year fertility
#> 1     Germany 1960      2.41
#> 2 South Korea 1960      6.16
#> 3     Germany 1961      2.44
#> 4 South Korea 1961      5.99
#> 5     Germany 1962      2.47
#> 6 South Korea 1962      5.79

Este set de datos tidy ofrece tasas de fertilidad para dos países a lo largo de los años. Se considera un set de datos tidy porque cada fila presenta una observación con las tres variables: país, año y tasa de fecundidad. Sin embargo, este set de datos originalmente vino en otro formato y le cambiamos la forma para distribuir a través del paquete dslabs. Originalmente, los datos estaban en el siguiente formato:

#>       country 1960 1961 1962
#> 1     Germany 2.41 2.44 2.47
#> 2 South Korea 6.16 5.99 5.79

Se provee la misma información, pero hay dos diferencias importantes en el formato: 1) cada fila incluye varias observaciones y 2) una de las variables, año, se almacena en el encabezado. Para que los paquetes del tidyverse se utilicen de manera óptima, le tenemos que cambiar la forma a los datos para que estén en formato tidy, que aprenderán a hacer en la sección “Wrangling de datos” del libro. Hasta entonces, utilizaremos ejemplos de sets de datos que ya están en formato tidy.

Aunque no es inmediatamente obvio, a medida que avancen en el libro comenzarán a apreciar las ventajas de trabajar usando un acercamiento en el que las funciones usan formatos tidy tanto para inputs como para outputs. Verán cómo esto permite que los analistas de datos se enfoquen en los aspectos más importantes del análisis en lugar del formato de los datos.

4.2 Ejercicios

1. Examine el set de datos co2 incluidos en base R. ¿Cuál de los siguientes es cierto?

  1. co2 son datos tidy: tiene un año para cada fila.
  2. co2 no es tidy: necesitamos al menos una columna con un vector de caracteres.
  3. co2 no es tidy: es una matriz en lugar de un data frame.
  4. co2 no es tidy: para ser tidy tendríamos que cambiarle la forma (wrangle it en inglés) para tener tres columnas (año, mes y valor), y entonces cada observación de CO2 tendría una fila.

2. Examine el set de datos ChickWeight incluidos en base R. ¿Cuál de los siguientes es cierto?

  1. ChickWeight no es tidy: cada pollito tiene más de una fila.
  2. ChickWeight es tidy: cada observación (un peso) está representada por una fila. El pollito de donde provino esta medida es una de las variables.
  3. ChickWeight no es tidy: nos falta la columna del año.
  4. ChickWeight es tidy: se almacena en un data frame.

3. Examine el set de datos predefinido BOD. ¿Cuál de los siguientes es cierto?

  1. BOD no es tidy: solo tiene seis filas.
  2. BOD no es tidy: la primera columna es solo un índice.
  3. BOD es tidy: cada fila es una observación con dos valores (tiempo y demanda)
  4. BOD es tidy: todos los sets de datos pequeños son tidy por definición.

4. ¿Cuál de los siguientes sets de datos integrados es tidy? Puede elegir más de uno.

  1. BJsales
  2. EuStockMarkets
  3. DNase
  4. Formaldehyde
  5. Orange
  6. UCBAdmissions

4.3 Cómo manipular los data frames

El paquete dplyr del tidyverse ofrece funciones que realizan algunas de las operaciones más comunes cuando se trabaja con data frames y usa nombres para estas funciones que son relativamente fáciles de recordar. Por ejemplo, para cambiar la tabla de datos agregando una nueva columna, utilizamos mutate. Para filtrar la tabla de datos a un subconjunto de filas, utilizamos filter. Finalmente, para subdividir los datos seleccionando columnas específicas, usamos select.

4.3.1 Cómo añadir una columna con mutate

Queremos que toda la información necesaria para nuestro análisis se incluya en la tabla de datos. Entonces, la primera tarea es añadir las tasas de asesinatos a nuestro data frame de asesinatos. La función mutate toma el data frame como primer argumento y el nombre y los valores de la variable como segundo argumento usando la convención name = values. Entonces, para añadir tasas de asesinatos, usamos:

library(dslabs)
data("murders")
murders <- mutate(murders, rate = total/ population * 100000)

Recuerden que aquí usamos total y population dentro de la función, que son objetos no definidos en nuestro espacio de trabajo. Pero, ¿por qué no recibimos un error?

Esta es una de las principales características de dplyr. Las funciones en este paquete, como mutate, saben buscar variables en el data frame que el primer argumento les provee. En la llamada a mutate que vemos arriba, total tendrá los valores de murders$total. Este enfoque hace que el código sea mucho más legible.

Podemos ver que se agrega la nueva columna:

head(murders)
#>        state abb region population total rate
#> 1    Alabama  AL  South    4779736   135 2.82
#> 2     Alaska  AK   West     710231    19 2.68
#> 3    Arizona  AZ   West    6392017   232 3.63
#> 4   Arkansas  AR  South    2915918    93 3.19
#> 5 California  CA   West   37253956  1257 3.37
#> 6   Colorado  CO   West    5029196    65 1.29

Aunque hemos sobrescrito el objeto original murders, esto no cambia el objeto que se cargó con data(murders). Si cargamos los datos murders nuevamente, el original sobrescribirá nuestra versión mutada.

4.3.2 Cómo crear subconjuntos con filter

Ahora supongan que queremos filtrar la tabla de datos para mostrar solo las entradas para las cuales la tasa de asesinatos es inferior a 0.71. Para hacer esto, usamos la función filter, que toma la tabla de datos como primer argumento y luego la declaración condicional como el segundo. Igual que con mutate, podemos usar los nombres de variables sin comillas de murders dentro de la función y esta sabrá que nos referimos a las columnas y no a los objetos en el espacio de trabajo.

filter(murders, rate <= 0.71)
#>           state abb        region population total  rate
#> 1        Hawaii  HI          West    1360301     7 0.515
#> 2          Iowa  IA North Central    3046355    21 0.689
#> 3 New Hampshire  NH     Northeast    1316470     5 0.380
#> 4  North Dakota  ND North Central     672591     4 0.595
#> 5       Vermont  VT     Northeast     625741     2 0.320

4.3.3 Cómo seleccionar columnas con select

Aunque nuestra tabla de datos solo tiene seis columnas, algunas tablas de datos incluyen cientos. Si queremos ver solo algunas columnas, podemos usar la función select de dplyr. En el siguiente código, seleccionamos tres columnas, asignamos el resultado a un nuevo objeto y luego filtramos este nuevo objeto:

new_table <- select(murders, state, region, rate)
filter(new_table, rate <= 0.71)
#>           state        region  rate
#> 1        Hawaii          West 0.515
#> 2          Iowa North Central 0.689
#> 3 New Hampshire     Northeast 0.380
#> 4  North Dakota North Central 0.595
#> 5       Vermont     Northeast 0.320

En la llamada a select, el primer argumento murders es un objeto, pero state, region y rate son nombres de variables.

4.4 Ejercicios

1. Cargue el paquete dplyr y el set de datos de asesinatos de EE.UU.

library(dplyr)
library(dslabs)
data(murders)

Puede añadir columnas usando la función mutate de dplyr. Esta función reconoce los nombres de la columnas y dentro de la función puede llamarlos sin comillas:

murders <- mutate(murders, population_in_millions = population/ 10^6)

Podemos escribir population en vez de murders$population. La función mutate sabe que estamos agarrando columnas de murders.

Use la función mutate para añadir una columna de asesinatos llamada rate con la tasa de asesinatos por 100,000 como en el código del ejemplo anterior. Asegúrese de redefinir murders como se hizo en el código del ejemplo anterior (murders <- [su código]) para que podamos seguir usando esta variable.

2. Si rank(x) le da el rango de las entradas de x de menor a mayor, rank(-x) le da los rangos de mayor a menor. Use la función mutate para añadir una columna rank que contiene el rango de la tasa de asesinatos de mayor a menor. Asegúrese de redefinir murders para poder seguir usando esta variable.

3. Con dplyr, podemos usar select para mostrar solo ciertas columnas. Por ejemplo, con este código solo mostraríamos los estados y los tamaños de población:

select(murders, state, population) |> head()

Utilice select para mostrar los nombres de los estados y las abreviaturas en murders. No redefina murders, solo muestre los resultados.

4. La función filter de dplyr se utiliza para elegir filas específicas del data frame para guardar. A diferencia de select que es para columnas, filter es para filas. Por ejemplo, puede mostrar solo la fila de Nueva York así:

filter(murders, state == "New York")

Puede usar otros vectores lógicos para filtrar filas.

Utilice filter para mostrar los cinco estados con las tasas de asesinatos más altas. Después de añadir la tasa y el rango de asesinatos, no cambie el set de datos de asesinatos de EE. UU., solo muestre el resultado. Recuerde que puede filtrar basándose en la columna rank.

5. Podemos eliminar filas usando el operador !=. Por ejemplo, para eliminar Florida, haríamos esto:

no_florida <- filter(murders, state != "Florida")

Cree un nuevo data frame con el nombre no_south que elimina los estados del sur. ¿Cuántos estados hay en esta categoría? Puede usar la función nrow para esto.

6. También podemos usar %in% para filtrar con dplyr. Por lo tanto, puede ver los datos de Nueva York y Texas de esta manera:

filter(murders, state %in% c("New York", "Texas"))

Cree un nuevo data frame llamado murders_nw con solo los estados del noreste y oeste. ¿Cuántos estados hay en esta categoría?

7. Suponga que desea vivir en el noreste u oeste y desea que la tasa de asesinatos sea inferior a 1. Queremos ver los datos de los estados que satisfacen estas opciones. Tenga en cuenta que puede usar operadores lógicos con filter. Aquí hay un ejemplo en el que filtramos para mantener solo estados pequeños en la región noreste.

filter(murders, population < 5000000 & region == "Northeast")

Asegúrese que murders ha sido definido con rate y rank y todavía tiene todos los estados. Cree una tabla llamada my_states que contiene filas para los estados que satisfacen ambas condiciones: está localizado en el noreste u oeste y la tasa de asesinatos es inferior a 1. Use select para mostrar solo el nombre del estado, la tasa y el rango.

4.5 El pipe: |> o %>%

En R podemos realizar una serie de operaciones, por ejemplo select y entonces filter, enviando los resultados de una función a otra usando lo que se llama el pipe operator: |>. Esta función se hizo disponible a partir de la version 4.1.0 de R, pero antes de esto el tidyverse usaba el operador %>% del paquete magrittr. Algunos detalles se incluyen a continuación.

Escribimos el código anterior para mostrar tres variables (estado, región, tasa) para los estados que tienen tasas de asesinatos por debajo de 0.71. Para hacer esto, definimos el objeto intermedio new_table. En dplyr, podemos escribir código que se parece más a una descripción de lo que queremos hacer sin objetos intermedios:

\[ \mbox {original data } \rightarrow \mbox { select } \rightarrow \mbox { filter } \]

Para tal operación, podemos usar el pipe |>. El código se ve así:

murders |> select(state, region, rate) |> filter(rate <= 0.71)
#>           state        region  rate
#> 1        Hawaii          West 0.515
#> 2          Iowa North Central 0.689
#> 3 New Hampshire     Northeast 0.380
#> 4  North Dakota North Central 0.595
#> 5       Vermont     Northeast 0.320

Esta línea de código es equivalente a las dos líneas de código anteriores. ¿Qué está pasando aquí?

En general, el pipe envía el resultado que se encuentra en el lado izquierdo del pipe para ser el primer argumento de la función en el lado derecho del pipe. Aquí vemos un ejemplo sencillo:

16 |> sqrt()
#> [1] 4

Podemos continuar canalizando (piping en inglés) valores a lo largo de:

16 |> sqrt() |> log2()
#> [1] 2

La declaración anterior es equivalente a log2(sqrt(16)).

Recuerden que el pipe envía valores al primer argumento, por lo que podemos definir otros argumentos como si el primer argumento ya estuviera definido:

16 |> sqrt() |> log(base = 2)
#> [1] 2

Por lo tanto, al usar el pipe con data frames y dplyr, ya no necesitamos especificar el primer argumento requerido puesto que las funciones dplyr que hemos descrito toman todos los datos como el primer argumento. En el código que escribimos:

murders |> select(state, region, rate) |> filter(rate <= 0.71)

murders es el primer argumento de la función select y el nuevo data frame (anteriormente new_table) es el primer argumento de la función filter.

Tengan en cuenta que el pipe funciona bien con las funciones donde el primer argumento son los datos de entrada. Las funciones en los paquetes tidyverse y dplyr tienen este formato y se pueden usar fácilmente con el pipe.

4.6 Ejercicios

1. El pipe |> se puede usar para realizar operaciones secuencialmente sin tener que definir objetos intermedios. Comience redefiniendo murders para incluir la tasa y el rango.

murders <- mutate(murders, rate = total/ population * 100000,
                  rank = rank(-rate))

En la solución al ejercicio anterior, hicimos lo siguiente:

my_states <- filter(murders, region %in% c("Northeast", "West") &
                      rate < 1)

select(my_states, state, rate, rank)

El pipe |> nos permite realizar ambas operaciones secuencialmente sin tener que definir una variable intermedia my_states. Por lo tanto, podríamos haber mutado y seleccionado en la misma línea de esta manera:

mutate(murders, rate = total/ population * 100000,
       rank = rank(-rate)) |>
  select(state, rate, rank)

Note que select ya no tiene un data frame como primer argumento. Se supone que el primer argumento sea el resultado de la operación realizada justo antes de |>.

Repita el ejercicio anterior, pero ahora, en lugar de crear un nuevo objeto, muestre el resultado y solo incluya las columnas de estado, velocidad y rango. Use un pipe |> para hacer esto en una sola línea.

2. Reinicie murders a la tabla original usando data(murders). Use un pipe para crear un nuevo data frame llamado my_states que considera solo los estados del noreste u oeste que tienen una tasa de asesinatos inferior a 1 y contiene solo las columnas de estado, tasa y rango. El pipe también debe tener cuatro componentes separados por tres |>. El código debería verse algo similar a lo siguiente:

my_states <- murders |>
  mutate SOMETHING |>
  filter SOMETHING |>
  select SOMETHING

4.7 Cómo resumir datos

Una parte importante del análisis exploratorio de datos es resumir los datos. El promedio y la desviación estándar son dos ejemplos de estadísticas de resumen ampliamente utilizadas. A menudo se pueden obtener resúmenes más informativos dividiendo primero los datos en grupos. En esta sección, cubrimos dos nuevos verbos de dplyr que facilitan estos cálculos: summarize y group_by. Aprendemos a acceder a los valores resultantes utilizando la función pull.

4.7.1 summarize

La función summarize de dplyr ofrece una forma de calcular estadísticas de resumen con código intuitivo y legible. Comenzamos con un ejemplo sencillo basado en alturas. El set de datos heights incluye las alturas y el sexo reportado por los estudiantes en una encuesta en clase.

library(dplyr)
library(dslabs)
data(heights)

El siguiente código calcula el promedio y la desviación estándar para las hembras:

s <- heights |>
  filter(sex == "Female") |>
  summarize(average = mean(height), standard_deviation = sd(height))
s
#>   average standard_deviation
#> 1    64.9               3.76

Esto toma nuestra tabla de datos original como entrada, la filtra para incluir solo a las filas representando hembras y luego produce una nueva tabla resumida con solo el promedio y la desviación estándar de las alturas. Podemos elegir los nombres de las columnas de la tabla resultante. Por ejemplo, arriba decidimos usar average y standard_deviation, pero podríamos haber usado otros nombres de la misma manera.

Como la tabla resultante almacenada en s es un data frame, podemos acceder a los componentes con el operador de acceso $:

s$average
#> [1] 64.9
s$standard_deviation
#> [1] 3.76

Igual que con la mayoría de las otras funciones de dplyr, summarize conoce los nombres de las variables y podemos usarlos directamente. Entonces, cuando escribimos mean(height) dentro de la llamada a la función summarize, la función accede a la columna con el nombre “height”, o altura, y luego calcula el promedio del vector numérico resultante. Podemos calcular cualquier otro resumen que opera en vectores y devuelve un solo valor.

Para otro ejemplo de cómo podemos usar la función summarize, calculemos la tasa promedio de asesinatos en Estados Unidos. Recuerden que nuestra tabla de datos incluye los asesinatos totales y el tamaño de la población para cada estado y ya hemos usado dplyr para añadir una columna de índice de asesinatos:

murders <- murders |> mutate(rate = total/population*100000)

Recuerden que la tasa de asesinatos en EE. UU. no es el promedio de las tasas de asesinatos estatales:

summarize(murders, mean(rate))
#>   mean(rate)
#> 1       2.78

Esto se debe a que en el cálculo anterior, los estados pequeños tienen el mismo peso que los grandes. La tasa de asesinatos de Estados Unidos es el número total de asesinatos en Estados Unidos dividido por la población total. Entonces el cálculo correcto es:

us_murder_rate <- murders |>
  summarize(rate = sum(total)/ sum(population) * 100000)
us_murder_rate
#>   rate
#> 1 3.03

Este cálculo cuenta estados más grandes proporcionalmente a su tamaño, lo que da como resultado un valor mayor.

4.7.2 Resúmenes múltiples

Supongamos que queremos tres resúmenes de la misma variable, como las alturas mediana, mínima y máxima. La función quantile: quantile(x, c(0.5, 0, 1)) devuelve la mediana (percentil 50), el mínimo (percentil 0) y el máximo (percentil 100) del vector x. Podemos usarlo con summarize así:

heights |> 
  filter(sex == "Female") |>
  summarize(median_min_max = quantile(height, c(0.5, 0, 1)))
#>   median_min_max
#> 1             65
#> 2             51
#> 3             79

Sin embargo, observe que los resúmenes se devuelven en una fila cada uno. Para obtener los resultados en diferentes columnas, tenemos que definir una función que devuelva un marco de datos como este:

median_min_max <- function(x){
  qs <- quantile(x, c(0.5, 0, 1))
  data.frame(median = qs[1], minimum = qs[2], maximum = qs[3])
}
heights |> 
  filter(sex == "Female") |>
  summarize(median_min_max(height))
#>   median minimum maximum
#> 1     65      51      79

En la próxima sección veremos lo útil que esto puede ser cuando resumimos por grupo.

4.7.3 Cómo agrupar y luego resumir con group_by

Una operación común en la exploración de datos es dividir primero los datos en grupos y luego calcular resúmenes para cada grupo. Por ejemplo, podemos querer calcular el promedio y la desviación estándar para las alturas de hombres y mujeres por separado. La función group_by nos ayuda a hacer esto.

Si escribimos esto:

heights |> group_by(sex)
#> # A tibble: 1,050 × 2
#> # Groups:   sex [2]
#>   sex   height
#>   <fct>  <dbl>
#> 1 Male      75
#> 2 Male      70
#> 3 Male      68
#> 4 Male      74
#> 5 Male      61
#> # … with 1,045 more rows

El resultado no se ve muy diferente de heights, excepto que vemos Groups: sex [2] cuando imprimimos el objeto. Aunque no es inmediatamente obvio por su apariencia, esto ahora es un data frame especial llamado un grouped data frame y las funciones de dplyr, en particular summarize, se comportarán de manera diferente cuando actúan sobre este objeto. Conceptualmente, pueden pensar en esta tabla como muchas tablas, con las mismas columnas pero no necesariamente el mismo número de filas, apiladas juntas en un objeto. Cuando resumimos los datos después de la agrupación, esto es lo que sucede:

heights |>
  group_by(sex) |>
  summarize(average = mean(height), standard_deviation = sd(height))
#> # A tibble: 2 × 3
#>   sex    average standard_deviation
#>   <fct>    <dbl>              <dbl>
#> 1 Female    64.9               3.76
#> 2 Male      69.3               3.61

La función summarize aplica el resumen a cada grupo por separado.

Para ver otro ejemplo, calculemos la mediana, el mínimo y máximo de la tasa de asesinatos en las cuatro regiones del país usando la función median_min_max definida anteriormente:

murders |>
  group_by(region) |>
  summarize(median_min_max(rate))
#> # A tibble: 4 × 4
#>   region        median minimum maximum
#>   <fct>          <dbl>   <dbl>   <dbl>
#> 1 Northeast       1.80   0.320    3.60
#> 2 South           3.40   1.46    16.5 
#> 3 North Central   1.97   0.595    5.36
#> 4 West            1.29   0.515    3.63

4.8 pull

El objeto us_murder_rate definido anteriormente representa solo un número. Sin embargo, lo estamos almacenando en un data frame:

class(us_murder_rate)
#> [1] "data.frame"

ya que, como la mayoría de las funciones de dplyr, summarize siempre devuelve un data frame.

Esto podría ser problemático si queremos usar este resultado con funciones que requieren un valor numérico. Aquí mostramos un truco útil para acceder a los valores almacenados en los datos cuando usamos pipes: cuando un objeto de datos se canaliza (is piped en inglés), ese objeto y sus columnas se pueden acceder usando la función pull. Para entender lo que queremos decir, miren esta línea de código:

us_murder_rate |> pull(rate)
#> [1] 3.03

Esto devuelve el valor en la columna rate de us_murder_rate haciéndolo equivalente a us_murder_rate$rate.

Para obtener un número de la tabla de datos original con una línea de código, podemos escribir:

us_murder_rate <- murders |>
  summarize(rate = sum(total)/ sum(population) * 100000) |>
  pull(rate)

us_murder_rate
#> [1] 3.03

que ahora es numérico:

class(us_murder_rate)
#> [1] "numeric"

4.9 Cómo ordenar los data frames

Al examinar un set de datos, a menudo es conveniente ordenar, numérica o alfabéticamente, basado en una o más de las columnas de la tabla. Conocemos las funciones order y sort, pero para ordenar tablas enteras, la función arrange de dplyr es útil. Por ejemplo, aquí ordenamos los estados según el tamaño de la población:

murders |>
  arrange(population) |>
  head()
#>                  state abb        region population total   rate
#> 1              Wyoming  WY          West     563626     5  0.887
#> 2 District of Columbia  DC         South     601723    99 16.453
#> 3              Vermont  VT     Northeast     625741     2  0.320
#> 4         North Dakota  ND North Central     672591     4  0.595
#> 5               Alaska  AK          West     710231    19  2.675
#> 6         South Dakota  SD North Central     814180     8  0.983

Con arrange podemos decidir cuál columna usar para ordenar. Para ver los estados por tasa de asesinatos, desde menor a mayor, organizamos por el rate :

murders |>
  arrange(rate) |>
  head()
#>           state abb        region population total  rate
#> 1       Vermont  VT     Northeast     625741     2 0.320
#> 2 New Hampshire  NH     Northeast    1316470     5 0.380
#> 3        Hawaii  HI          West    1360301     7 0.515
#> 4  North Dakota  ND North Central     672591     4 0.595
#> 5          Iowa  IA North Central    3046355    21 0.689
#> 6         Idaho  ID          West    1567582    12 0.766

Tengan en cuenta que el comportamiento por defecto es ordenar en orden ascendente. En dplyr, la función desc transforma un vector para que esté en orden descendente. Para ordenar la tabla en orden descendente, podemos escribir:

murders |>
  arrange(desc(rate))

4.9.1 Cómo ordenar anidadamente

Si estamos ordenando una columna cuando hay empates, podemos usar una segunda columna para romper el empate. Del mismo modo, se puede usar una tercera columna para romper empates entre la primera y la segunda, y así sucesivamente. Aquí ordenamos por region y entonces, dentro de la región, ordenamos por tasa de asesinatos:

murders |>
  arrange(region, rate) |>
  head()
#>           state abb    region population total  rate
#> 1       Vermont  VT Northeast     625741     2 0.320
#> 2 New Hampshire  NH Northeast    1316470     5 0.380
#> 3         Maine  ME Northeast    1328361    11 0.828
#> 4  Rhode Island  RI Northeast    1052567    16 1.520
#> 5 Massachusetts  MA Northeast    6547629   118 1.802
#> 6      New York  NY Northeast   19378102   517 2.668

4.9.2 Los primeros \(n\)

En el código anterior, usamos la función head para evitar que la página se llene con todo el set de datos. Si queremos ver una mayor proporción, podemos usar la función top_n. Esta función toma un data frame como primer argumento, el número de filas para mostrar en el segundo y la variable para filtrar en el tercero. Aquí hay un ejemplo de cómo ver las 5 filas superiores:

murders |> top_n(5, rate)
#>                  state abb        region population total  rate
#> 1 District of Columbia  DC         South     601723    99 16.45
#> 2            Louisiana  LA         South    4533372   351  7.74
#> 3             Maryland  MD         South    5773552   293  5.07
#> 4             Missouri  MO North Central    5988927   321  5.36
#> 5       South Carolina  SC         South    4625364   207  4.48

Tengan en cuenta que las filas no están ordenadas por rate, solo filtradas. Si queremos ordenar, necesitamos usar arrange. Recuerden que si el tercer argumento se deja en blanco, top_n filtra por la última columna.

4.10 Ejercicios

Para estos ejercicios, utilizaremos los datos de la encuesta recopilada por el Centro Nacional de Estadísticas de Salud de Estados Unidos (NCHS por sus siglas en inglés). Este centro ha realizado una serie de encuestas de salud y nutrición desde la década de 1960. A partir de 1999, alrededor de 5,000 individuos de todas las edades han sido entrevistados cada año y completan el componente de examen de salud de la encuesta. Parte de los datos está disponible a través del paquete NHANES. Una vez que instale el paquete NHANES, puede cargar los datos así:

library(NHANES)
data(NHANES)

Los datos NHANES tienen muchos valores faltantes. Las funciones mean y sd devolverán NA si alguna de las entradas del vector de entrada es un NA. Aquí hay un ejemplo:

library(dslabs)
data(na_example)
mean(na_example)
#> [1] NA
sd(na_example)
#> [1] NA

Para ignorar los NAs, podemos usar el argumento na.rm:

mean(na_example, na.rm = TRUE)
#> [1] 2.3
sd(na_example, na.rm = TRUE)
#> [1] 1.22

Exploremos ahora los datos de NHANES.

1. Le ofrecemos algunos datos básicos sobre la presión arterial. Primero, seleccionemos un grupo para establecer el estándar. Utilizaremos hembras de 20 a 29 años. AgeDecade es una variable categórica con estas edades. Tenga en cuenta que la categoría está codificada como ” 20-29”, ¡con un espacio al frente! ¿Cuál es el promedio y la desviación estándar de la presión arterial sistólica según se guarda en la variable BPSysAve? Guárdela en una variable llamada ref.

Sugerencia: use filter y summarize y use el argumento na.rm = TRUE al calcular el promedio y la desviación estándar. También puede filtrar los valores de NA utilizando filter.

2. Usando un pipe, asigne el promedio a una variable numérica ref_avg. Sugerencia: use el código similar al anterior y luego pull.

3. Ahora indique los valores mínimo y máximo para el mismo grupo.

4. Calcule el promedio y la desviación estándar para las hembras, pero para cada grupo de edad por separado en lugar de una década seleccionada como en la pregunta 1. Tenga en cuenta que los grupos de edad se definen por AgeDecade. Sugerencia: en lugar de filtrar por edad y género, filtre por Gender y luego use group_by.

5. Repita el ejercicio 4 para los varones.

6. Podemos combinar ambos resúmenes para los ejercicios 4 y 5 en una línea de código. Esto es porque group_by nos permite agrupar por más de una variable. Obtenga una gran tabla de resumen usando group_by(AgeDecade, Gender).

7. Para los varones entre las edades de 40-49, compare la presión arterial sistólica según raza, como aparece en la variable Race1. Ordene la tabla resultante según la presión arterial sistólica promedio de más baja a más alta.

4.11 Tibbles

Los datos tidy deben almacenarse en data frames. Discutimos el data frame en la Sección 2.4.1 y hemos estado usando el data frame murders en todo el libro. En la sección 4.7.3, presentamos la función group_by, que permite estratificar los datos antes de calcular las estadísticas de resumen. Pero, ¿dónde se almacena la información del grupo en el data frame?

murders |> group_by(region)
#> # A tibble: 51 × 6
#> # Groups:   region [4]
#>   state      abb   region population total  rate
#>   <chr>      <chr> <fct>       <dbl> <dbl> <dbl>
#> 1 Alabama    AL    South     4779736   135  2.82
#> 2 Alaska     AK    West       710231    19  2.68
#> 3 Arizona    AZ    West      6392017   232  3.63
#> 4 Arkansas   AR    South     2915918    93  3.19
#> 5 California CA    West     37253956  1257  3.37
#> # … with 46 more rows

Observen que no hay columnas con esta información. Pero si miran el output anterior, verán la línea A tibble seguida por unas dimensiones. Podemos aprender la clase del objeto devuelto usando:

murders |> group_by(region) |> class()
#> [1] "grouped_df" "tbl_df"     "tbl"        "data.frame"

El tbl es un tipo especial de data frame. Las funciones group_by y summarize siempre devuelven este tipo de data frame. La función group_by devuelve un tipo especial de tbl, el grouped_df. Discutiremos esto más adelante. Por coherencia, los verbos de manipulación dplyr ( select, filter, mutate y arrange) preservan la clase del input: si reciben un data frame regular, devuelven un data frame regular, mientras que si reciben un tibble, devuelven un tibble. Pero los tibbles son el formato preferido por el tidyverse y, como resultado, las funciones tidyverse que producen un data frame desde cero devuelven un tibble. Por ejemplo, en el Capítulo 5, veremos que las funciones del tidyverse que se usan para importar datos crean tibbles.

Los tibbles son muy similares a los data frames. De hecho, pueden pensar en ellos como una versión moderna de data frames. Sin embargo, hay tres diferencias importantes que describiremos a continuación.

4.11.1 Los tibbles se ven mejor

El método de impresión para tibbles es más legible que el de un data frame. Para ver esto, comparen el output de escribir murders y el output de asesinatos si los convertimos en un tibble. Podemos hacer esto usando as_tibble(murders). Si usan RStudio, el output para un tibble se ajusta al tamaño de sus ventanas. Para ver esto, cambien el ancho de su consola R y observen cómo se muestran más/menos columnas.

4.11.2 Los subconjuntos de tibbles son tibbles

Si creamos subconjuntos de las columnas de un data frame, le pueden devolver un objeto que no es un data frame, como un vector o escalar. Por ejemplo:

class(murders[,4])
#> [1] "numeric"

no es un data frame. Con tibbles, esto no sucede:

class(as_tibble(murders)[,4])
#> [1] "tbl_df"     "tbl"        "data.frame"

Esto es útil en el tidyverse ya que las funciones requieren data frames como input.

Con tibbles, si desean acceder al vector que define una columna y no recuperar un data frame, deben usar el operador de acceso $:

class(as_tibble(murders)$population)
#> [1] "numeric"

Una característica relacionada es que tibbles les dará una advertencia si intentan acceder a una columna que no existe. Por ejemplo, si escribimos accidentalmente Population en lugar de population vemos que:

murders$Population
#> NULL

devuelve un NULL sin advertencia, lo que puede dificultar la depuración. Por el contrario, si intentamos esto con un tibble, obtenemos una advertencia informativa:

as_tibble(murders)$Population
#> Warning: Unknown or uninitialised column: `Population`.
#> NULL

4.11.3 Los tibbles pueden tener entradas complejas

Si bien las columnas del data frame deben ser vectores de números, cadenas o valores lógicos, los tibbles pueden tener objetos más complejos, como listas o funciones. Además, podemos crear tibbles con funciones:

tibble(id = c(1, 2, 3), func = c(mean, median, sd))
#> # A tibble: 3 × 2
#>      id func  
#>   <dbl> <list>
#> 1     1 <fn>  
#> 2     2 <fn>  
#> 3     3 <fn>

4.11.4 Los tibbles se pueden agrupar

La función group_by devuelve un tipo especial de tibble: un tibble agrupado. Esta clase almacena información que les permite saber qué filas están en qué grupos. Las funciones tidyverse, en particular summarize, están al tanto de la información del grupo.

4.11.5 Cómo crear un tibble usando tibble en lugar de data.frame

A veces es útil para nosotros crear nuestros propios data frames. Para crear un data frame en formato tibble, pueden utilizar la función tibble.

grades <- tibble(names = c("John", "Juan", "Jean", "Yao"),
                 exam_1 = c(95, 80, 90, 85),
                 exam_2 = c(90, 85, 85, 90))

Noten que la base R (sin paquetes cargados) tiene una función con un nombre muy similar, data.frame, que se puede usar para crear un data frame regular en vez de un tibble.

grades <- data.frame(names = c("John", "Juan", "Jean", "Yao"),
                     exam_1 = c(95, 80, 90, 85),
                     exam_2 = c(90, 85, 85, 90))

Para convertir un data frame normal en un tibble, pueden usar la función as_tibble.

as_tibble(grades) |> class()
#> [1] "tbl_df"     "tbl"        "data.frame"

4.12 El marcador de posición

Una de las ventajas de utilizar el pipe |> es que no tenemos que seguir nombrando nuevos objetos mientras manipulamos el data frame. El objeto del lado izquierdo del pipe se utiliza como primer argumento de la función del lado derecho del pipe. Pero, ¿y si queremos pasarlo como argumento a la función del lado derecho que no es la primera? La respuesta es el operador de marcador de posición _ (para la canalización %>%, el marcador de posición es .). A continuación se muestra un ejemplo simple que pasa el argumento base a la función log. Los tres siguientes son equivalentes:

tab_1 <- filter(murders, region == "South")
tab_2 <- mutate(tab_1, rate = total/ population * 10^5)
rates <- tab_2$rate
median(rates)
#> [1] 3.4

podemos evitar definir nuevos objetos intermedios escribiendo:

filter(murders, region == "South") |>
  mutate(rate = total/ population * 10^5) |>
  summarize(median = median(rate)) |>
  pull(median)
#> [1] 3.4

4.13 El paquete purrr

En la Sección 3.5, aprendimos sobre la función sapply, que nos permitió aplicar la misma función a cada elemento de un vector. Construimos una función y utilizamos sapply para calcular la suma de los primeros n enteros para varios valores de n así:

compute_s_n <- function(n){
  x <- 1:n
  sum(x)
}
n <- 1:25
s_n <- sapply(n, compute_s_n)

Este tipo de operación, que aplica la misma función o procedimiento a elementos de un objeto, es bastante común en el análisis de datos. El paquete purrr incluye funciones similares a sapply, pero que interactúan mejor con otras funciones del tidyverse. La principal ventaja es que podemos controlar mejor el tipo de resultado de las funciones. En cambio, sapply puede devolver varios tipos de objetos diferentes, convirtiéndolos cuando sea conveniente. Las funciones de purrr nunca harán esto: devolverán objetos de un tipo específico o devolverán un error si esto no es posible.

La primera función de purrr que aprenderemos es map, que funciona muy similar a sapply pero siempre, sin excepción, devuelve una lista:

library(purrr)
s_n <- map(n, compute_s_n)
class(s_n)
#> [1] "list"

Si queremos un vector numérico, podemos usar map_dbl que siempre devuelve un vector de valores numéricos.

s_n <- map_dbl(n, compute_s_n)
class(s_n)
#> [1] "numeric"

Esto produce los mismos resultados que la llamada sapply que vemos arriba.

Una función de purrr particularmente útil para interactuar con el resto del tidyverse es map_df, que siempre devuelve un tibble data frame. Sin embargo, la función que llamamos debe devolver un vector o una lista con nombres. Por esta razón, el siguiente código daría como resultado un error Argument 1 must have names:

s_n <- map_df(n, compute_s_n)

Necesitamos cambiar la función para arreglar esto:

compute_s_n <- function(n){
  x <- 1:n
  tibble(sum = sum(x))
}
s_n <- map_df(n, compute_s_n)

El paquete purrr ofrece mucha más funcionalidad no discutida aquí. Para obtener más detalles, pueden consultar recursos en línea16.

4.14 Los condicionales de tidyverse

Un análisis de datos típicos frecuentemente implicará una o más operaciones condicionales. En la Sección 3.1, describimos la función ifelse, que utilizaremos ampliamente en este libro. Ahora presentamos dos funciones de dplyr que ofrecen una funcionalidad adicional para realizar operaciones condicionales.

4.14.1 case_when

La función case_when es útil para vectorizar declaraciones condicionales. Esto es similar a ifelse, pero puede generar cualquier cantidad de valores, en lugar de solo TRUE o FALSE. Aquí hay un ejemplo que divide los números en negativo, positivo y 0:

x <- c(-2, -1, 0, 1, 2)
case_when(x < 0 ~ "Negative",
          x > 0 ~ "Positive",
          TRUE ~ "Zero")
#> [1] "Negative" "Negative" "Zero"     "Positive" "Positive"

Un uso común de esta función es definir unas variables categóricas basadas en variables existentes. Por ejemplo, supongan que queremos comparar las tasas de asesinatos en cuatro grupos de estados: New England, West Coast, South y Other. Para cada estado, primero preguntamos si está en New England. Si la respuesta es no, entonces preguntamos si está en el West Coast, y si no, preguntamos si está en el South y, si no, entonces asignamos ninguna de las anteriores (Other). Aquí vemos como usamos case_when para hacer esto:

murders |>
  mutate(group = case_when(
    abb %in% c("ME", "NH", "VT", "MA", "RI", "CT") ~ "New England",
    abb %in% c("WA", "OR", "CA") ~ "West Coast",
    region == "South" ~ "South",
    TRUE ~ "Other")) |>
  group_by(group) |>
  summarize(rate = sum(total)/ sum(population) * 10^5)
#> # A tibble: 4 × 2
#>   group        rate
#>   <chr>       <dbl>
#> 1 New England  1.72
#> 2 Other        2.71
#> 3 South        3.63
#> 4 West Coast   2.90

4.14.2 between

Una operación común en el análisis de datos es determinar si un valor cae dentro de un intervalo. Podemos verificar esto usando condicionales. Por ejemplo, para verificar si los elementos de un vector x están entre a y b, podemos escribir:

x >= a & x <= b

Sin embargo, esto puede volverse complicado, especialmente dentro del enfoque tidyverse. La función between realiza la misma operación:

between(x, a, b)

4.15 Ejercicios

1. Cargue el set de datos murders. ¿Cuál de los siguientes es cierto?

  1. murders está en formato tidy y se almacena en un tibble.
  2. murders está en formato tidy y se almacena en un data frame.
  3. murders no está en formato tidy y se almacena en un tibble.
  4. murders no está en formato tidy y se almacena en un data frame.

2. Utilice as_tibble para convertir la tabla de datos murders en un tibble y guárdelo en un objeto llamado murders_tibble.

3. Utilice la función group_by para convertir murders en un tibble que se agrupa por región.

4. Escriba el código tidyverse que es equivalente a este código:

exp(mean(log(murders$population)))

Escríbalo usando el pipe para que cada función se llame sin argumentos. Use el operador punto para acceder a la población. Sugerencia: el código debe comenzar con murders |>.

5. Utilice el map_df para crear un data frame con tres columnas que se denominan n, s_n y s_n_2. La primera columna debe contener los números del 1 al 100. La segunda y la tercera columna deben contener la suma del 1 al 100 \(n\) con \(n\) representando el número de fila.