5.2 Trattamento dei dati con dplyr

Il pacchetto dplyr include sei funzioni base: filter(), select(), mutate(), arrange(), group_by() e summarise(). Queste sei funzioni costituiscono i verbi del linguaggio di manipolazione dei dati. A questi sei verbi si aggiunge il pipe %>% che serve a concatenare più operazioni. In particolare, considerando una matrice osservazioni per variabili, select() e mutate() si occupano di organizzare le variabili, filter() e arrange() i casi, e group_by() e summarise() i gruppi.

Per introdurre le funzionalità di dplyr, utilizzeremo i dati msleep forniti dal pacchetto ggplot2. Tali dati descrivono le ore di sonno medie di 83 specie di mammiferi (Savage et al. 2007). Carichiamo il boundle tidyverse (che contiene ggplot2) e leggiamo nella memoria di lavoro l’oggetto msleep:

library("tidyverse")
data(msleep)
dim(msleep)
#> [1] 83 11

5.2.1 Operatore pipe

Prima di presentare le funzionalità di dplyr, introduciamo l’operatore pipe %>% del pacchetto magrittr – ma ora presente anche in base R nella versione |>. L’operatore pipe, %>% o |>, serve a concatenare varie funzioni insieme, in modo da inserire un’operazione dietro l’altra. Una spiegazione intuitiva dell’operatore pipe è stata fornita in un tweet di @andrewheiss. Consideriamo la seguente istruzione in pseudo-codice R:

leave_house(
  get_dressed(
    get_out_of_bed(
      wake_up(me, time = "8:00"),
      side = "correct"
    ),
    pants = TRUE,
    shirt = TRUE
  ),
  car = TRUE,
  bike = FALSE
)

Il listato precedente descrive una serie di (pseudo) funzioni concatenate, le quali costituiscono gli argomenti di altre funzioni. Scritto così, il codice è molto difficile da capire. Possiamo però ottenere lo stesso risultato utilizzando l’operatore pipe che facilita enormememnte la leggibilità del codice:

me %>%
  wake_up(time = "8:00") %>%
  get_out_of_bed(side = "correct") %>%
  get_dressed(pants = TRUE, shirt = TRUE) %>%
  leave_house(car = TRUE, bike = FALSE)

In questa seconda versione del (pseudo) codice R si capisce molto meglio ciò che vogliamo fare. Il tibble me viene passato alla funzione wake_up(). La funzione wake_up() ha come argomento l’ora del giorno: time = "8:00". Una volta “svegliati” (wake up) dobbiamo scendere dal letto. Quindi l’output di wake_up() viene passato alla funzione get_out_of_bed() la quale ha come argomento side = "correct" perché vogliamo scendere dal letto dalla parte giusta. E così via.

Questo pseudo-codice chiarisce il significato dell’operatore pipe. L’operatore %>% è “syntactic sugar” per una serie di chiamate di funzioni concatenate, ovvero, detto in altre parole, consente di definire la relazione tra una serie di funzioni nelle quali il risultato (output) di una funzione viene utilizzato come l’input di una funzione successiva.

5.2.2 Estrarre una singola colonna con pull()

Ritorniamo ora all’esempio precedente. Iniziamo a trasformare il data frame msleep in un tibble (che è identico ad un data frame ma viene stampato sulla console in un modo diverso):

msleep <- tibble(msleep)

Estraiamo da msleep la variabile sleep_total usando il verbo pull():

msleep %>%
  pull(sleep_total)
#>  [1] 12.1 17.0 14.4 14.9  4.0 14.4  8.7  7.0 10.1  3.0  5.3  9.4 10.0 12.5 10.3
#> [16]  8.3  9.1 17.4  5.3 18.0  3.9 19.7  2.9  3.1 10.1 10.9 14.9 12.5  9.8  1.9
#> [31]  2.7  6.2  6.3  8.0  9.5  3.3 19.4 10.1 14.2 14.3 12.8 12.5 19.9 14.6 11.0
#> [46]  7.7 14.5  8.4  3.8  9.7 15.8 10.4 13.5  9.4 10.3 11.0 11.5 13.7  3.5  5.6
#> [61] 11.1 18.1  5.4 13.0  8.7  9.6  8.4 11.3 10.6 16.6 13.8 15.9 12.8  9.1  8.6
#> [76] 15.8  4.4 15.6  8.9  5.2  6.3 12.5  9.8

5.2.3 Selezionare più colonne con select()

Se vogliamo selezionare da msleep un insieme di variabili, ad esempio name, vore e sleep_total, possiamo usare il verbo select():

dt <- msleep %>%
  dplyr::select(name, vore, sleep_total)
dt
#> # A tibble: 83 × 3
#>   name                       vore  sleep_total
#>   <chr>                      <chr>       <dbl>
#> 1 Cheetah                    carni        12.1
#> 2 Owl monkey                 omni         17  
#> 3 Mountain beaver            herbi        14.4
#> 4 Greater short-tailed shrew omni         14.9
#> 5 Cow                        herbi         4  
#> 6 Three-toed sloth           herbi        14.4
#> 7 Northern fur seal          carni         8.7
#> 8 Vesper mouse               <NA>          7  
#> # ℹ 75 more rows

laddove la sequenza di istruzioni precedenti significa che abbiamo passato msleep alla funzione select() contenuta nel pacchetto dplyr e l’output di select() è stato salvato (usando l’operatore di assegnazione, <-) nell’oggetto dt. Alla funzione select() abbiamo passato gli argomenti name, vore e sleep_total.

5.2.4 Filtrare le osservazioni (righe) con filter()

Il verbo filter() consente di selezionare da un tibble un sottoinsieme di righe (osservazioni). Per esempio, possiamo selezionare tutte le osservazioni nella variabile vore contrassegnate come carni (ovvero, tutti i carnivori):

dt %>%
  dplyr::filter(vore == "carni")
#> # A tibble: 19 × 3
#>   name                 vore  sleep_total
#>   <chr>                <chr>       <dbl>
#> 1 Cheetah              carni        12.1
#> 2 Northern fur seal    carni         8.7
#> 3 Dog                  carni        10.1
#> 4 Long-nosed armadillo carni        17.4
#> 5 Domestic cat         carni        12.5
#> 6 Pilot whale          carni         2.7
#> 7 Gray seal            carni         6.2
#> 8 Thick-tailed opposum carni        19.4
#> # ℹ 11 more rows

Per utilizzare il verbo filter() in modo efficace è neccessario usare gli operatori relazionali (Tabella ??) e gli operatori logici (Tabella ??) di R. Per un approfondimento, si veda il Capitolo Comparisons di R for Data Science.

5.2.5 Creare una nuova variabile con mutate()

Talvolta vogliamo creare una nuova variabile, per esempio, sommando o dividendo due variabili, oppure calcolandone la media. A questo scopo si usa il verbo mutate(). Per esempio, se vogliamo esprimere i valori di sleep_total in minuti, moltiplichiamo per 60:

dt %>%
  mutate(
    sleep_minutes = sleep_total * 60
  ) %>%
  dplyr::select(sleep_total, sleep_minutes)
#> # A tibble: 83 × 2
#>   sleep_total sleep_minutes
#>         <dbl>         <dbl>
#> 1        12.1           726
#> 2        17            1020
#> 3        14.4           864
#> 4        14.9           894
#> 5         4             240
#> 6        14.4           864
#> 7         8.7           522
#> 8         7             420
#> # ℹ 75 more rows

5.2.6 Ordinare i dati con arrange()

Il verbo arrange() ordina i dati in base ai valori di una o più variabili. Per esempio, possiamo ordinare la variabile sleep_total dal valore più alto al più basso in questo modo:

dt %>%
  arrange(
    desc(sleep_total)
  )
#> # A tibble: 83 × 3
#>   name                   vore    sleep_total
#>   <chr>                  <chr>         <dbl>
#> 1 Little brown bat       insecti        19.9
#> 2 Big brown bat          insecti        19.7
#> 3 Thick-tailed opposum   carni          19.4
#> 4 Giant armadillo        insecti        18.1
#> 5 North American Opossum omni           18  
#> 6 Long-nosed armadillo   carni          17.4
#> 7 Owl monkey             omni           17  
#> 8 Arctic ground squirrel herbi          16.6
#> # ℹ 75 more rows

5.2.7 Raggruppare i dati con group_by()

Il verbo group_by() raggruppa insieme i valori in base a una o più variabili. Lo vedremo in uso in seguito insieme a summarise().

Nota: con dplyr(), le operazioni raggruppate vengono iniziate con la funzione group_by(). È una buona norma utilizzare ungroup() alla fine di una serie di operazioni raggruppate, altrimenti i raggruppamenti verranno mantenuti nelle analisi successiva, il che non è sempre auspicabile.

5.2.8 Sommario dei dati con summarise()

Il verbo summarise() collassa il dataset in una singola riga dove viene riportato il risultato della statistica richiesta. Per esempio, la media del tempo totale del sonno è

dt %>%
  summarise(
    m_sleep = mean(sleep_total, na.rm = TRUE)
  )
#> # A tibble: 1 × 1
#>   m_sleep
#>     <dbl>
#> 1    10.4

5.2.9 Operazioni raggruppate

Sopra abbiamo visto come i mammiferi considerati dormano, in media, 10.4 ore al giorno. Troviamo ora il sonno medio in funzione di vore:

dt %>%
  group_by(vore) %>%
  summarise(
    m_sleep = mean(sleep_total, na.rm = TRUE),
    n = n()
  )
#> # A tibble: 5 × 3
#>   vore    m_sleep     n
#>   <chr>     <dbl> <int>
#> 1 carni     10.4     19
#> 2 herbi      9.51    32
#> 3 insecti   14.9      5
#> 4 omni      10.9     20
#> 5 <NA>      10.2      7

Si noti che, nel caso di 7 osservazioni, il valore di vore non era specificato. Per tali osservazioni, dunque, la classe di appartenenza è NA.

5.2.10 Applicare una funzione su più colonne: across()

È spesso utile eseguire la stessa operazione su più colonne, ma copiare e incollare è sia noioso che soggetto a errori:

df %>%
  group_by(g1, g2) %>%
  summarise(
    a = mean(a),
    b = mean(b),
    c = mean(c),
    d = mean(d)
  )

In tali circostanze è possibile usare la funzione across() che consente di riscrivere il codice precedente in modo più succinto:

df %>%
  group_by(g1, g2) %>%
  summarise(across(a:d, mean))

Per i dati presenti, ad esempio, possiamo avere:

msleep %>%
  group_by(vore) %>%
  summarise(across(starts_with("sleep"), ~ mean(.x, na.rm = TRUE)))
#> # A tibble: 5 × 4
#>   vore    sleep_total sleep_rem sleep_cycle
#>   <chr>         <dbl>     <dbl>       <dbl>
#> 1 carni         10.4       2.29       0.373
#> 2 herbi          9.51      1.37       0.418
#> 3 insecti       14.9       3.52       0.161
#> 4 omni          10.9       1.96       0.592
#> 5 <NA>          10.2       1.88       0.183

References

Savage, Van M, Andrew P Allen, James H Brown, James F Gillooly, Alexander B Herman, William H Woodruff, and Geoffrey B West. 2007. “Scaling of Number, Size, and Metabolic Rate of Cells with Body Size in Mammals.” Proceedings of the National Academy of Sciences 104 (11): 4718–23.