here::here("code", "_common.R") |> source()
# Load packages
if (!requireNamespace("pacman")) install.packages("pacman")
pacman::p_load(tidyr, mice, missForest)
11 Usare dplyr
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.1
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:
- Ogni variabile è una colonna: ogni colonna nel dataset rappresenta una singola variabile.
- Ogni osservazione è una riga: ogni riga nel dataset rappresenta un’unica osservazione.
- 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.
11.1 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:
-
|>
: introdotto nativamente a partire dalla versione 4.1.0 di R. -
%>%
: introdotto dal pacchettomagrittr
, ed è una delle componenti centrali deltidyverse
.
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 dimagrittr
, richiede che il pacchetto sia installato e caricato (library(magrittr)
o automaticamente tramitetidyverse
).
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:
11.1.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 %>%
.
11.2 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
.
Esaminiamo i dati:
glimpse(msleep)
#> Rows: 83
#> Columns: 11
#> $ name <chr> "Cheetah", "Owl monkey", "Mountain beaver", "Greater shor…
#> $ genus <chr> "Acinonyx", "Aotus", "Aplodontia", "Blarina", "Bos", "Bra…
#> $ vore <chr> "carni", "omni", "herbi", "omni", "herbi", "herbi", "carn…
#> $ order <chr> "Carnivora", "Primates", "Rodentia", "Soricomorpha", "Art…
#> $ conservation <chr> "lc", NA, "nt", "lc", "domesticated", NA, "vu", NA, "dome…
#> $ sleep_total <dbl> 12.1, 17.0, 14.4, 14.9, 4.0, 14.4, 8.7, 7.0, 10.1, 3.0, 5…
#> $ sleep_rem <dbl> NA, 1.8, 2.4, 2.3, 0.7, 2.2, 1.4, NA, 2.9, NA, 0.6, 0.8, …
#> $ sleep_cycle <dbl> NA, NA, NA, 0.133, 0.667, 0.767, 0.383, NA, 0.333, NA, NA…
#> $ awake <dbl> 11.9, 7.0, 9.6, 9.1, 20.0, 9.6, 15.3, 17.0, 13.9, 21.0, 1…
#> $ brainwt <dbl> NA, 0.01550, NA, 0.00029, 0.42300, NA, NA, NA, 0.07000, 0…
#> $ bodywt <dbl> 50.000, 0.480, 1.350, 0.019, 600.000, 3.850, 20.490, 0.04…
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 |
11.3 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 sleep_total
#> <chr> <chr> <chr> <chr> <chr> <dbl>
#> 1 Giraffe Giraffa herbi Artiodactyla cd 1.9
#> 2 Pilot whale Globicephalus carni Cetacea cd 2.7
#> 3 Horse Equus herbi Perissodactyla domesticated 2.9
#> 4 Roe deer Capreolus herbi Artiodactyla lc 3
#> 5 Donkey Equus herbi Perissodactyla domesticated 3.1
#> 6 African elephant Loxodonta herbi Proboscidea vu 3.3
#> 7 Caspian seal Phoca carni Carnivora vu 3.5
#> 8 Sheep Ovis herbi Artiodactyla domesticated 3.8
#> 9 Asian elephant Elephas herbi Proboscidea en 3.9
#> sleep_rem sleep_cycle awake brainwt bodywt
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 0.4 NA 22.1 NA 900.
#> 2 0.1 NA 21.4 NA 800
#> 3 0.6 1 21.1 0.655 521
#> 4 NA NA 21 0.0982 14.8
#> 5 0.4 NA 20.9 0.419 187
#> 6 NA NA 20.7 5.71 6654
#> 7 0.4 NA 20.5 NA 86
#> 8 0.6 NA 20.2 0.175 55.5
#> 9 NA NA 20.1 4.60 2547
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 sleep_total
#> <chr> <chr> <chr> <chr> <chr> <dbl>
#> 1 Giraffe Giraffa herbi Artiodactyla cd 1.9
#> 2 Pilot whale Globicephalus carni Cetacea cd 2.7
#> 3 Horse Equus herbi Perissodactyla domesticated 2.9
#> 4 Donkey Equus herbi Perissodactyla domesticated 3.1
#> 5 African elephant Loxodonta herbi Proboscidea vu 3.3
#> 6 Asian elephant Elephas herbi Proboscidea en 3.9
#> 7 Human Homo omni Primates <NA> 8
#> sleep_rem sleep_cycle awake brainwt bodywt
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 0.4 NA 22.1 NA 900.
#> 2 0.1 NA 21.4 NA 800
#> 3 0.6 1 21.1 0.655 521
#> 4 0.4 NA 20.9 0.419 187
#> 5 NA NA 20.7 5.71 6654
#> 6 NA NA 20.1 4.60 2547
#> 7 1.9 1.5 16 1.32 62
11.4 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 armadi…
#> $ vore <chr> "omni", "carni", "insecti", "omni", "carni", "omni", "omni…
#> $ rem_prop <dbl> 34.7, 34.0, 33.7, 29.2, 28.7, 27.2, 26.4, 26.2, 25.6, 25.0…
#> $ sleep_total <dbl> 10.1, 19.4, 18.1, 8.9, 10.1, 18.0, 9.1, 10.3, 12.5, 8.4, 1…
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
11.5 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
group_by(order)
suddivide il datasetmsleep
in gruppi, ciascuno corrispondente a un valore distinto della variabileorder
.-
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 disleep_total
in ogni gruppo. -
max_sleep
è il valore massimo disleep_total
in ogni gruppo. -
total
è il numero di osservazioni (o righe) per ciascun gruppo, calcolato con la funzionen()
.
-
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.
Riflessioni 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.
Bibliografia
Per comprendere meglio il concetto di “data tidying”, possiamo rifarci a una citazione tratta dal testo di riferimento R for Data Science (2e): “Tidy datasets are all alike, but every messy dataset is messy in its own way.”↩︎