10  Introduzione a dplyr

In questo capitolo imparerai a
  • utlizzare le principali funzioni del pacchetto dplyr.
Preparazione del Notebook
here::here("code", "_common.R") |> source()

# Load packages
if (!requireNamespace("pacman")) install.packages("pacman")
pacman::p_load(tidyr, mice, missForest)

10.1 Introduzione

L’obiettivo di questo capitolo è fornire un’introduzione alle funzioni principali del pacchetto dplyr per le operazioni di data wrangling, cioè per il preprocessing e la pulizia dei dati. In R, queste operazioni sono strettamente legate al concetto di “data tidying”, che si riferisce all’organizzazione sistematica dei dati per facilitare l’analisi.

Per comprendere meglio il concetto di “data tidying”, possiamo rifarci a una citazione tratta dal testo di riferimento R for Data Science (2e):

“Happy families are all alike; every unhappy family is unhappy in its own way.” — Leo Tolstoy

“Tidy datasets are all alike, but every messy dataset is messy in its own way.” — Hadley Wickham

L’essenza del “data tidying” è organizzare i dati in un formato che sia facile da gestire e analizzare. Anche se gli stessi dati possono essere rappresentati in vari modi, non tutte le rappresentazioni sono ugualmente efficienti o facili da usare. Un dataset “tidy” segue tre principi fondamentali che lo rendono particolarmente pratico:

  1. Ogni variabile è una colonna: ogni colonna nel dataset rappresenta una singola variabile.
  2. Ogni osservazione è una riga: ogni riga nel dataset rappresenta un’unica osservazione.
  3. Ogni valore è una cella: ogni cella del dataset contiene un singolo valore.

Il pacchetto R {dplyr} e gli altri pacchetti del tidyverse sono progettati specificamente per lavorare con dati in formato “tidy”, permettendo agli utenti di eseguire operazioni di manipolazione e visualizzazione in modo più intuitivo ed efficiente.

10.2 Pipe

Il pacchetto dplyr, così come l’intero ecosistema tidyverse, fa largo uso dell’operatore pipe, che consente di concatenare una sequenza di operazioni in modo leggibile ed efficiente. In R, esistono due principali notazioni per il pipe:

  1. |>: introdotto nativamente a partire dalla versione 4.1.0 di R.
  2. %>%: introdotto dal pacchetto magrittr, ed è una delle componenti centrali del tidyverse.

Entrambi gli operatori permettono di ottenere risultati simili e, per la maggior parte degli utilizzi, possono essere considerati intercambiabili. Tuttavia, è importante sottolineare alcune differenze:

  • |> è integrato nel linguaggio R e non richiede pacchetti aggiuntivi.
  • %>%, essendo parte di magrittr, richiede che il pacchetto sia installato e caricato (library(magrittr) o automaticamente tramite tidyverse).

Consideriamo l’esempio seguente (che anticipa l’uso della funzione filter() che descriveremo in seguito). Un’operazione comune è filtrare un data frame e calcolare la media di una colonna. Con il pipe, questa sequenza di operazioni diventa più leggibile:

# Usando %>%
iris %>%
  dplyr::filter(Species == "setosa") |> 
  summarise(
    mean_sepal_length = mean(Sepal.Length)
  ) 
#>   mean_sepal_length
#> 1             5.006
# Usando |>
iris |> 
  dplyr::filter(Species == "setosa") |> 
  summarise(
    mean_sepal_length = mean(Sepal.Length)
  ) 
#>   mean_sepal_length
#> 1             5.006

10.2.1 Cosa Fa la Pipe?

La pipe è uno strumento potente che permette di collegare in modo diretto l’output di una funzione come input della funzione successiva. Questo approccio:

  • Riduce la necessità di creare variabili intermedie.
  • Migliora la leggibilità del codice.
  • Rende il flusso delle operazioni più chiaro e lineare.

Ogni funzione applicata con la pipe riceve automaticamente l’output della funzione precedente come suo primo argomento. Ciò consente di scrivere sequenze di operazioni in un formato compatto e intuitivo.

Ecco un altro esempio:

# Utilizzo della pipe per trasformare un dataset
df <- data.frame(
  id = 1:5,
  value = c(10, 20, 30, 40, 50)
)

# Filtra i dati, seleziona colonne e calcola nuovi valori
df_clean <- df |>
  dplyr::filter(value > 20) |>
  dplyr::select(id, value) |>
  mutate(squared_value = value^2)

In questa sequenza, il dataset originale df viene filtrato, le colonne desiderate vengono selezionate e viene aggiunta una nuova colonna con il valore al quadrato.

head(df_clean)
#>   id value squared_value
#> 1  3    30           900
#> 2  4    40          1600
#> 3  5    50          2500

In sintesi, la pipe è uno strumento fondamentale per scrivere codice R moderno e leggibile, indipendentemente dal fatto che si utilizzi |> o %>%.

10.3 Verbi

Le funzioni principali (“verbi) di dplyr sono le seguenti:

Verbo dplyr Descrizione
select() Seleziona colonne
filter() Filtra righe
arrange() Riordina o organizza le righe
mutate() Crea nuove colonne
summarise() Riassume i valori
group_by() Consente di eseguire operazioni di gruppo

I verbi di dplyr sono suddivisi in quattro gruppi, in base all’elemento su cui operano: righe, colonne, gruppi o tabelle.

Inoltre, le diverse funzioni bind_ e _joins permettono di combinare più tibbles (ovvero, data frame) in uno solo.

Per fare un esempio prarico, usiamo nuovamente il dataset msleep.

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

Esaminiamo i dati:

glimpse(msleep)
#> Rows: 83
#> Columns: 11
#> $ name         <chr> "Cheetah", "Owl monkey", "Mountain beaver", "Greater …
#> $ genus        <chr> "Acinonyx", "Aotus", "Aplodontia", "Blarina", "Bos", …
#> $ vore         <chr> "carni", "omni", "herbi", "omni", "herbi", "herbi", "…
#> $ order        <chr> "Carnivora", "Primates", "Rodentia", "Soricomorpha", …
#> $ conservation <chr> "lc", NA, "nt", "lc", "domesticated", NA, "vu", NA, "…
#> $ sleep_total  <dbl> 12.1, 17.0, 14.4, 14.9, 4.0, 14.4, 8.7, 7.0, 10.1, 3.…
#> $ sleep_rem    <dbl> NA, 1.8, 2.4, 2.3, 0.7, 2.2, 1.4, NA, 2.9, NA, 0.6, 0…
#> $ sleep_cycle  <dbl> NA, NA, NA, 0.1333, 0.6667, 0.7667, 0.3833, NA, 0.333…
#> $ awake        <dbl> 11.9, 7.0, 9.6, 9.1, 20.0, 9.6, 15.3, 17.0, 13.9, 21.…
#> $ brainwt      <dbl> NA, 0.01550, NA, 0.00029, 0.42300, NA, NA, NA, 0.0700…
#> $ bodywt       <dbl> 50.000, 0.480, 1.350, 0.019, 600.000, 3.850, 20.490, …

Le colonne, nell’ordine, corrispondono a quanto segue:

Nome colonna Descrizione
name Nome comune
genus Rango tassonomico
vore Carnivoro, onnivoro o erbivoro?
order Rango tassonomico
conservation Stato di conservazione del mammifero
sleep_total Quantità totale di sonno, in ore
sleep_rem Sonno REM, in ore
sleep_cycle Durata del ciclo di sonno, in ore
awake Quantità di tempo trascorso sveglio, in ore
brainwt Peso del cervello, in chilogrammi
bodywt Peso corporeo, in chilogrammi

10.4 Righe

I verbi più importanti che operano sulle righe di un dataset sono filter(), che seleziona le righe da includere senza modificarne l’ordine, e arrange(), che cambia l’ordine delle righe senza alterare la selezione delle righe presenti.

msleep |>
  dplyr::filter(sleep_total < 4) |>
  arrange(sleep_total)
#> # A tibble: 9 × 11
#>   name             genus         vore  order          conservation
#>   <chr>            <chr>         <chr> <chr>          <chr>       
#> 1 Giraffe          Giraffa       herbi Artiodactyla   cd          
#> 2 Pilot whale      Globicephalus carni Cetacea        cd          
#> 3 Horse            Equus         herbi Perissodactyla domesticated
#> 4 Roe deer         Capreolus     herbi Artiodactyla   lc          
#> 5 Donkey           Equus         herbi Perissodactyla domesticated
#> 6 African elephant Loxodonta     herbi Proboscidea    vu          
#> 7 Caspian seal     Phoca         carni Carnivora      vu          
#> 8 Sheep            Ovis          herbi Artiodactyla   domesticated
#> 9 Asian elephant   Elephas       herbi Proboscidea    en          
#> # ℹ 6 more variables: sleep_total <dbl>, sleep_rem <dbl>,
#> #   sleep_cycle <dbl>, awake <dbl>, brainwt <dbl>, bodywt <dbl>

Possiamo usare filter() speficicano più di una condizione logica.

msleep |>
  dplyr::filter((sleep_total < 4 & bodywt > 100) | brainwt > 1) |>
  arrange(sleep_total)
#> # A tibble: 7 × 11
#>   name             genus         vore  order          conservation
#>   <chr>            <chr>         <chr> <chr>          <chr>       
#> 1 Giraffe          Giraffa       herbi Artiodactyla   cd          
#> 2 Pilot whale      Globicephalus carni Cetacea        cd          
#> 3 Horse            Equus         herbi Perissodactyla domesticated
#> 4 Donkey           Equus         herbi Perissodactyla domesticated
#> 5 African elephant Loxodonta     herbi Proboscidea    vu          
#> 6 Asian elephant   Elephas       herbi Proboscidea    en          
#> 7 Human            Homo          omni  Primates       <NA>        
#> # ℹ 6 more variables: sleep_total <dbl>, sleep_rem <dbl>,
#> #   sleep_cycle <dbl>, awake <dbl>, brainwt <dbl>, bodywt <dbl>

10.5 Colonne

Esistono quattro verbi principali che modificano le colonne di un dataset senza cambiare le righe:

  • relocate() cambia la posizione delle colonne;
  • rename() modifica i nomi delle colonne;
  • select() seleziona le colonne da includere o escludere;
  • mutate() crea nuove colonne a partire da quelle esistenti.
msleep2 <- msleep |>
  mutate(
    rem_prop = sleep_rem / sleep_total * 100
  ) |>
  dplyr::select(name, vore, rem_prop, sleep_total) |>
  arrange(desc(rem_prop))

glimpse(msleep2)
#> Rows: 83
#> Columns: 4
#> $ name        <chr> "European hedgehog", "Thick-tailed opposum", "Giant ar…
#> $ vore        <chr> "omni", "carni", "insecti", "omni", "carni", "omni", "…
#> $ rem_prop    <dbl> 34.65, 34.02, 33.70, 29.21, 28.71, 27.22, 26.37, 26.21…
#> $ sleep_total <dbl> 10.1, 19.4, 18.1, 8.9, 10.1, 18.0, 9.1, 10.3, 12.5, 8.…

In questo esempio, utilizziamo mutate() per creare una nuova colonna rem_prop che rappresenta la percentuale di sonno REM sul totale del sonno. Successivamente, select() viene utilizzato per scegliere solo alcune colonne del dataset, e infine desc(rem_prop) ordina i valori di rem_prop in ordine decrescente, dal valore maggiore a quello minore.

Per cambiare il nome di una colonna possiamo usare rename(). Inoltre, possiamo cambiare l’ordine delle variabili con relocate().

msleep2 |>
  rename(rem_perc = rem_prop) |>
  relocate(rem_perc, .before = name)
#> # A tibble: 83 × 4
#>    rem_perc name                   vore    sleep_total
#>       <dbl> <chr>                  <chr>         <dbl>
#>  1     34.7 European hedgehog      omni           10.1
#>  2     34.0 Thick-tailed opposum   carni          19.4
#>  3     33.7 Giant armadillo        insecti        18.1
#>  4     29.2 Tree shrew             omni            8.9
#>  5     28.7 Dog                    carni          10.1
#>  6     27.2 North American Opossum omni           18  
#>  7     26.4 Pig                    omni            9.1
#>  8     26.2 Desert hedgehog        <NA>           10.3
#>  9     25.6 Domestic cat           carni          12.5
#> 10     25   Eastern american mole  insecti         8.4
#> # ℹ 73 more rows

10.6 Gruppi

Il verbo group_by() viene utilizzato per suddividere un dataset in gruppi, in base a una o più variabili, che siano rilevanti per l’analisi. Questo permette di eseguire operazioni di sintesi su ciascun gruppo separatamente, ottenendo informazioni aggregate.

Ad esempio, nel codice seguente:

msleep |>
  group_by(order) |>
  summarise(
    avg_sleep = mean(sleep_total),
    min_sleep = min(sleep_total),
    max_sleep = max(sleep_total),
    total = n()
  ) |>
  arrange(desc(avg_sleep))
#> # A tibble: 19 × 5
#>    order           avg_sleep min_sleep max_sleep total
#>    <chr>               <dbl>     <dbl>     <dbl> <int>
#>  1 Chiroptera          19.8       19.7      19.9     2
#>  2 Didelphimorphia     18.7       18        19.4     2
#>  3 Cingulata           17.8       17.4      18.1     2
#>  4 Afrosoricida        15.6       15.6      15.6     1
#>  5 Pilosa              14.4       14.4      14.4     1
#>  6 Rodentia            12.5        7        16.6    22
#>  7 Diprotodontia       12.4       11.1      13.7     2
#>  8 Soricomorpha        11.1        8.4      14.9     5
#>  9 Primates            10.5        8        17      12
#> 10 Erinaceomorpha      10.2       10.1      10.3     2
#> 11 Carnivora           10.1        3.5      15.8    12
#> 12 Scandentia           8.9        8.9       8.9     1
#> 13 Monotremata          8.6        8.6       8.6     1
#> 14 Lagomorpha           8.4        8.4       8.4     1
#> 15 Hyracoidea           5.67       5.3       6.3     3
#> 16 Artiodactyla         4.52       1.9       9.1     6
#> 17 Cetacea              4.5        2.7       5.6     3
#> 18 Proboscidea          3.6        3.3       3.9     2
#> 19 Perissodactyla       3.47       2.9       4.4     3
  1. group_by(order) suddivide il dataset msleep in gruppi, ciascuno corrispondente a un valore distinto della variabile order.

  2. Successivamente, summarise() calcola diverse statistiche per ogni gruppo:

    • avg_sleep è la media del totale del sonno (sleep_total) all’interno di ciascun gruppo.
    • min_sleep è il valore minimo di sleep_total in ogni gruppo.
    • max_sleep è il valore massimo di sleep_total in ogni gruppo.
    • total è il numero di osservazioni (o righe) per ciascun gruppo, calcolato con la funzione n().
  3. Infine, arrange(desc(avg_sleep)) ordina i risultati in ordine decrescente in base alla media del sonno totale (avg_sleep), mostrando prima i gruppi con la media di sonno più alta.

Questo tipo di approccio è utile quando si vuole analizzare come cambiano le caratteristiche dei dati a seconda dei gruppi specifici, fornendo una visione più dettagliata e utile.

10.7 Considerazioni Conclusive

Il data wrangling è una delle fasi più importanti in qualsiasi pipeline di analisi dei dati. In questo capitolo abbiamo introdotto l’uso del pacchetto tidyverse di R per la manipolazione dei dati e il suo utilizzo in scenari di base. Tuttavia, il tidyverse è un ecosistema ampio e qui abbiamo trattato solo gli elementi fondamentali. Per approfondire, si consiglia di consultare ulteriori risorse come quelle disponibili sul sito web del tidyverse e il libro R for Data Science (2e), di cui esiste anche una traduzione italiana.

10.8 Esercizi

In questo esercizio, utilizzerai il pacchetto dplyr per imparare a manipolare e trasformare i dati della SWLS (Satisfaction With Life Scale). Gli esercizi ti aiuteranno a consolidare la conoscenza dei principali verbi di dplyr, inclusi filter(), select(), mutate(), arrange() e group_by().

Parte 1: Comprensione Teorica

  1. Cos’è un dataset “tidy”?
    • Descrivi con parole tue cosa significa avere un dataset “tidy” e quali sono le sue tre caratteristiche principali.
  2. Cos’è la pipe (%>% o |>) e perché è utile?
    • Spiega a cosa serve l’operatore pipe e fornisci un esempio di utilizzo.
  3. Quali sono i verbi principali di dplyr?
    • Elenca e spiega brevemente i sei verbi principali di dplyr per la manipolazione dei dati.
  4. Cosa fa il verbo group_by()?
    • Spiega il suo scopo e come viene utilizzato in combinazione con summarise().

Parte 2: Applicazione Pratica con i Dati SWLS

  1. Caricamento dei dati SWLS
    • Crea un data frame in R contenente i punteggi SWLS che hai raccolto.
  2. Selezione delle colonne
    • Usa select() per mantenere solo le colonne con i punteggi degli item.
  3. Filtraggio dei dati
    • Usa filter() per selezionare solo gli individui che hanno un punteggio totale superiore a 20.
  4. Creazione di una nuova colonna
    • Usa mutate() per calcolare il punteggio totale della SWLS per ciascun individuo e salvarlo in una nuova colonna chiamata punteggio_totale.
  5. Riordinamento dei dati
    • Usa arrange() per ordinare il dataset in base al punteggio totale, dal più alto al più basso.
  6. Raggruppamento e sintesi dei dati
  • Usa group_by() e summarise() per calcolare la media e la deviazione standard del punteggio SWLS totale nel dataset.

Consegna

  • Scrivi le risposte della Parte 1 su carta.
  • Scrivi il codice e i risultati della Parte 2 in un file .R e invialo come consegna.

Parte 1: Comprensione Teorica

  1. Cos’è un dataset “tidy”?
    • Un dataset “tidy” è un dataset organizzato in modo sistematico per facilitare l’analisi. Le sue tre caratteristiche principali sono:
      1. Ogni variabile è una colonna.
      2. Ogni osservazione è una riga.
      3. Ogni valore è una cella.
  2. Cos’è la pipe (%>% o |>) e perché è utile?
    • La pipe (%>% o |>) permette di concatenare più operazioni di manipolazione dati in modo leggibile ed efficiente.

    • Esempio:

      df |> 
        filter(score > 20) |> 
        select(name, score)
  3. Quali sono i verbi principali di dplyr?
    • select(): Seleziona colonne.
    • filter(): Filtra righe.
    • arrange(): Riordina le righe.
    • mutate(): Crea nuove colonne.
    • summarise(): Riassume i dati.
    • group_by(): Permette di raggruppare i dati.
  4. Cosa fa il verbo group_by()?
    • group_by() suddivide i dati in gruppi, permettendo di applicare funzioni di aggregazione con summarise().

    • Esempio:

      df |> 
        group_by(gruppo) |> 
        summarise(media = mean(score), sd = sd(score))

Parte 2: Applicazione Pratica con i Dati SWLS

  1. Caricamento dei dati SWLS Per svolgere l’esercizio, simuliamo i dati di 10 individui su 5 item (numeri casuali da 1 a 7):

    set.seed(123)
    swls <- data.frame(
      id = 1:10,
      item1 = sample(1:7, 10, replace = TRUE),
      item2 = sample(1:7, 10, replace = TRUE),
      item3 = sample(1:7, 10, replace = TRUE),
      item4 = sample(1:7, 10, replace = TRUE),
      item5 = sample(1:7, 10, replace = TRUE)
    )
    print(swls)
  2. Selezione delle colonne

    swls_selected <- swls |> select(item1:item5)
  3. Filtraggio dei dati

    swls_filtered <- swls |> filter(rowSums(select(swls, item1:item5)) > 20)
  4. Creazione di una nuova colonna

    swls <- swls |> mutate(punteggio_totale = rowSums(select(swls, item1:item5)))
  5. Riordinamento dei dati

    swls_sorted <- swls |> arrange(desc(punteggio_totale))
  6. Raggruppamento e sintesi dei dati

swls_summary <- swls |> 
  summarise(media = mean(punteggio_totale), sd = sd(punteggio_totale))

Conclusione

Questi esercizi hanno mostrato come usare dplyr per manipolare dati in modo efficace e leggibile.

Informazioni sull’Ambiente di Sviluppo

sessionInfo()
#> R version 4.4.2 (2024-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Sequoia 15.3.1
#> 
#> Matrix products: default
#> BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0
#> 
#> locale:
#> [1] C/UTF-8/C/C/C/C
#> 
#> time zone: Europe/Rome
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] missForest_1.5   mice_3.17.0      thematic_0.1.6   MetBrewer_0.2.0 
#>  [5] ggokabeito_0.1.0 see_0.10.0       gridExtra_2.3    patchwork_1.3.0 
#>  [9] bayesplot_1.11.1 psych_2.4.12     scales_1.3.0     markdown_1.13   
#> [13] knitr_1.49       lubridate_1.9.4  forcats_1.0.0    stringr_1.5.1   
#> [17] dplyr_1.1.4      purrr_1.0.4      readr_2.1.5      tidyr_1.3.1     
#> [21] tibble_3.2.1     ggplot2_3.5.1    tidyverse_2.0.0  rio_1.2.3       
#> [25] here_1.0.1      
#> 
#> loaded via a namespace (and not attached):
#>  [1] tidyselect_1.2.1     farver_2.1.2         fastmap_1.2.0       
#>  [4] pacman_0.5.1         digest_0.6.37        rpart_4.1.24        
#>  [7] timechange_0.3.0     lifecycle_1.0.4      survival_3.8-3      
#> [10] magrittr_2.0.3       compiler_4.4.2       rngtools_1.5.2      
#> [13] rlang_1.1.5          tools_4.4.2          utf8_1.2.4          
#> [16] doRNG_1.8.6.1        htmlwidgets_1.6.4    mnormt_2.1.1        
#> [19] withr_3.0.2          itertools_0.1-3      nnet_7.3-20         
#> [22] grid_4.4.2           jomo_2.7-6           colorspace_2.1-1    
#> [25] iterators_1.0.14     MASS_7.3-65          cli_3.6.4           
#> [28] rmarkdown_2.29       reformulas_0.4.0     generics_0.1.3      
#> [31] rstudioapi_0.17.1    tzdb_0.4.0           minqa_1.2.8         
#> [34] splines_4.4.2        parallel_4.4.2       vctrs_0.6.5         
#> [37] boot_1.3-31          glmnet_4.1-8         Matrix_1.7-2        
#> [40] jsonlite_1.9.1       hms_1.1.3            mitml_0.4-5         
#> [43] foreach_1.5.2        glue_1.8.0           nloptr_2.1.1        
#> [46] pan_1.9              codetools_0.2-20     stringi_1.8.4       
#> [49] shape_1.4.6.1        gtable_0.3.6         lme4_1.1-36         
#> [52] munsell_0.5.1        pillar_1.10.1        htmltools_0.5.8.1   
#> [55] randomForest_4.7-1.2 R6_2.6.1             Rdpack_2.6.2        
#> [58] rprojroot_2.0.4      evaluate_1.0.3       lattice_0.22-6      
#> [61] rbibutils_2.3        backports_1.5.0      broom_1.0.7         
#> [64] Rcpp_1.0.14          nlme_3.1-167         xfun_0.51           
#> [67] pkgconfig_2.0.3

Bibliografia

Irizarry, R. A. (2024). Introduction to Data Science: Data Wrangling and Visualization with R. CRC Press.