Let us load the data:

library(rio)
setwd("/folder/where/you/keep/the/data")
cses <- import("cses2018small.xlsx")

Data management con dplyr

dplyr è una libreria che ci semplifica di molto le cose quando si tratta di lavorare con database in R. dplyr è parte di una collezione di librerie chiamata tidyverse, che abbiamo già menzionato qualche giorno fa. Si può installare nel solito modo:

install.packages("tidyverse", dependencies = T)

Naturalmente è possibile installare anche solo dplyr individualmente, senza le altre librerie del tidyverse, tuttavia useremo anche altre librerie presenti nel tidyverse, e inoltre alcune funzionalità che si utilizzano in dplyr provengono in ogni caso da altre librerie della collezione. Da un certo punto di vista possiamo considerare tidyverse come un “ecosistema” di funzioni, che da il meglio di sé quando viene utilizzato in modo integrato.

Nota1: per interagire al meglio da dplyr e con altre funzioni del tidyverse dobbiamo utilizzare un linguaggio leggermente diverso rispetto a quello che utilizziamo con R base. Metaforicamente, pensate a un gergo tecnico che in un ambito ben specifico aiuta a esprimersi in modo molto più efficiente. Non è completamente diverso dal linguaggio di R base, ma ci sono alcune differenze importanti che vedremo.

Nota 2: tutto quello che vedremo fatto con dplyr oggi può naturalmente essere fatto anche con R base (molte di queste cose le abbiamo viste nella scorsa sessione). Ovviamente questo vale per ogni funzione da ogni libreria di R. Il modo in cui le librerie come dplyr ci aiutano è rendendo il processo più veloce e, in molti casi, facendoci scrivere meno codice.

PEr incominciare carichiamo il tidyverse:

library(tidyverse)

Ora guarderemo alcune delle funzioni più comuni di dplyr.

Selezionare variabili con select()

La funzione select() serve per selezionare una o più colonne in un dataset, esattamente come le parentesi quadre [. Per esempio, possiamo selezionare solo le variabili mip1 e mip2 dal nostro dataset cses.

cses_pg <- select(cses, mip1, mip2)
head(cses_pg)
##             mip1           mip2
## 1      la scuola      il lavoro
## 2           <NA>           <NA>
## 3 poverta,lavoro   immigrazione
## 4      il lavoro   immigrazione
## 5         lavoro   immigrazione
## 6      il lavoro l'immigrazione

Per renderci la vita più facile, con dplyr abbiamo a disposizione delle funzioni che ci permettono di selezionare gruppi di variabili senza dover scrivere tutti i nomi uno per uno.

  • starts_with(): seleziona le variabili basate su un prefisso comune
cses_pg <- select(cses, starts_with("mip"))
  • ends_with(): seleziona le variabili basate su un suffisso comune
cses_pg <- select(cses, ends_with("1"), ends_with("2"))
  • contains(): più in generale, seleziona le variabili sulla base di una sequenza di caratteri presenti all’interno dei loro nomi
cses_pg <- select(cses, contains("ea"))

cses_pg <- select(cses, contains("e"), contains("a"))

Naturalmente possono essere combinati

cses_pg <- select(cses, starts_with("eta"), ends_with("gr"))

Nota: gli oggetti prodotti da dplyr sembrano data frame, ma in realtà appartengono a un formato leggermente diverso chiamato “tibble”. Questo si nota perchè se chiedete a R di mostrarvi il dataset, l’output includerà la frase A tibble: n x n (il numero di righe e colonne) e altre informazioni sul tipo di valore (<dbl>, <chr>) sotto ai nomi delle variabili.

Il formato tibble è quasi identico al data frame, l’unica differenza è che è ottimizzato per essere usato con le funzioni che sono parte del tidyverse. Per la maggior parte degli usi che se ne possono fare, non c’è alcuna differenza tra i formati tibble e data frame. Inoltre, in caso di necessità, è sempre possibile convertire un tibble in data frame con la funzione as.data.frame().

cses_pg <- as.data.frame(cses_pg)

Selezionare osservazioni con filter() e slice()

La funzione filter() serve per selezionare una o più righe in un dataset, e anche in questo caso fa quello che faremmo in R base usando le parentesi quadre [. Per esempio, possiamo selezionare solo gli intervistati che vivono nelle regioni del nord Italia, hanno più di 35 anni e pensano che durante l’anno prima delle elezioni la situazione economica del paese sia peggiorata.

cses_pg <- filter(
  cses,
  area %in% c("Nord-Ovest", "Nord-Est"), 
  eta > 35,
  eco_eval == -1
)
head(cses_pg)
##   id     sex eta eta_gr           mip1            mip2 eco_eval       area
## 1  3 Femmina  68    55+ poverta,lavoro    immigrazione       -1   Nord-Est
## 2  5 Femmina  54  35 54         lavoro    immigrazione       -1 Nord-Ovest
## 3 20 Femmina  65    55+         lavoro crisi economica       -1 Nord-Ovest
## 4 27 Femmina  57    55+     burocrazia        politica       -1   Nord-Est
## 5 35 Femmina  45  35 54 disoccupazione    immigrazione       -1 Nord-Ovest
## 6 88 Maschio  51  35 54      sicurezza    immigrazione       -1   Nord-Est

La funzione slice() (e tutte le sue versioni specifiche), invece è l’equivalente di utilizzare le parentesi quadre [ con i numeri di indice:

# Alcune osservazioni a caso nel dataset
slice(cses, c(1, 2, 10, 35, 3))
##   id     sex eta eta_gr              mip1         mip2 eco_eval       area
## 1  1 Femmina  60    55+         la scuola    il lavoro       -1        Sud
## 2  2 Maschio  62    55+              <NA>         <NA>       -1     Centro
## 3 10 Femmina  37  35 54 la disoccupazione   la poverta       -1        Sud
## 4 35 Femmina  45  35 54    disoccupazione immigrazione       -1 Nord-Ovest
## 5  3 Femmina  68    55+    poverta,lavoro immigrazione       -1   Nord-Est
  • slice_sample() permette di estrarre alcune osservazioni casuali:
# 5 osservazioni casuali
slice_sample(cses, n = 5)
##     id     sex eta eta_gr                     mip1         mip2 eco_eval   area
## 1  185 Maschio  52  35 54            l'occupazione la sicurezza       -1    Sud
## 2 1303 Maschio  62    55+                 legalita       lavoro        0    Sud
## 3  127 Maschio  29  18 34             Immigrazione   Corruzione        0    Sud
## 4  237 Maschio  64    55+                   Lavoro   Corruzione        0    Sud
## 5  101 Femmina  77    55+ disoccupazione giovanile         <NA>        1 Centro
  • slice_head() e slice_tail() permettono di estrarre le prime o ultime n osservazioni:
# Prime 5 osservazioni nel dataset
slice_head(cses, n = 5)
##   id     sex eta eta_gr           mip1         mip2 eco_eval       area
## 1  1 Femmina  60    55+      la scuola    il lavoro       -1        Sud
## 2  2 Maschio  62    55+           <NA>         <NA>       -1     Centro
## 3  3 Femmina  68    55+ poverta,lavoro immigrazione       -1   Nord-Est
## 4  4 Maschio  62    55+      il lavoro immigrazione       -1        Sud
## 5  5 Femmina  54  35 54         lavoro immigrazione       -1 Nord-Ovest
# Ultime 5 osservazioni nel dataset
slice_tail(cses, n = 5)
##     id     sex eta eta_gr                                mip1                    mip2 eco_eval       area
## 1 1997 Femmina  67    55+                           il lavoro                   farlo        1 Nord-Ovest
## 2 1998 Maschio  48  35 54                   la disoccupazione la negatività nel paese        0        Sud
## 3 1999 Maschio  73    55+                              lavoro educazione dei citadini       -1        Sud
## 4 2000 Femmina  44  35 54 razzismo al livello degli attentati               il lavoro       -1     Centro
## 5 2001 Femmina  67    55+                              lavoro                pensioni       -1 Nord-Ovest

Rinominare le variabili con rename()

Una funzione abbastanza pratica per cambiare i nomi alle variabili, come faremmo con Stata

cses_pg <- rename(
  cses_pg,
  eta_groups = eta_gr
)
names(cses_pg)
## [1] "id"         "sex"        "eta"        "eta_groups" "mip1"       "mip2"       "eco_eval"   "area"

Aggiungere variabili con _join()

dplyr offre una serie di funzioni per aggiungere variabili a un dataset (ovvero quello che viene chiamato comunemente merge o nel linguaggio di SQL, “join”) seguendo diversi criteri. Tutte queste funzioni hanno lo stesso suffisso e sono strutturate nello stesso modo: _join(x, y, by = var), dove x è il dataset che stiamo utilizzando e che vogliamo usare come “base”, y è il dataset le cui variabili vogliamo aggiungere a x, e var è la variabile “ponte” che viene usata per assegnare i valori corretti alle osservazioni corrette.

La lista completa delle possibili soluzioni che dplyr offre per unire diversi dataset può essere trovata qui. Le più comuni sono:

  • left_join(): mantiene tutte le osservazioni in x. Se y ha meno osservazioni di x, le variabili in y saranno NA per le righe in x che non sono presenti in y. Se y ha più osservazioni di x, le righe in eccesso verranno eliminate. Il numero di osservazioni nel dataset risultante sarà lo stesso numero di osservazioni in x.
  • right_join(): l’esatto contrario, ovvero mantiene tutte le osservazioni in y, ecc.
  • inner_join(): mantiene tutte le osservazioni che sono sia in x che in y (intersezioni di insiemi).
  • full_join(): mantiene le osservazioni che sono in x oppure in y (quindi tutte le osservazioni in entrambi i dataset, unione di insiemi).

Vediamo il solito esempio con il file cses2018edu.dta:

cses <- left_join(
  cses,
  import("cses2018edu.dta"),
  by = "id"
)
head(cses)
##   id     sex eta eta_gr           mip1           mip2 eco_eval       area titstu
## 1  1 Femmina  60    55+      la scuola      il lavoro       -1        Sud      9
## 2  2 Maschio  62    55+           <NA>           <NA>       -1     Centro      5
## 3  3 Femmina  68    55+ poverta,lavoro   immigrazione       -1   Nord-Est      5
## 4  4 Maschio  62    55+      il lavoro   immigrazione       -1        Sud      6
## 5  5 Femmina  54  35 54         lavoro   immigrazione       -1 Nord-Ovest      5
## 6  6 Femmina  71    55+      il lavoro l'immigrazione        0 Nord-Ovest      4

Creare e trasformare variabili con mutate()

Questa è una delle funzioni più utilizzate in dplyr. Può essere usata sia per operazioni di ricodifica molto semplici e anche cose più difficili. Per esempio, possiamo standardizzare la variabile eta, come abbiamo fatto l’ultima volta:

cses_pg <- mutate(
  cses, 
  eta_std = scale(eta)
)
head(cses_pg)
##   id     sex eta eta_gr           mip1           mip2 eco_eval       area titstu   eta_std
## 1  1 Femmina  60    55+      la scuola      il lavoro       -1        Sud      9 0.5124959
## 2  2 Maschio  62    55+           <NA>           <NA>       -1     Centro      5 0.6317059
## 3  3 Femmina  68    55+ poverta,lavoro   immigrazione       -1   Nord-Est      5 0.9893360
## 4  4 Maschio  62    55+      il lavoro   immigrazione       -1        Sud      6 0.6317059
## 5  5 Femmina  54  35 54         lavoro   immigrazione       -1 Nord-Ovest      5 0.1548658
## 6  6 Femmina  71    55+      il lavoro l'immigrazione        0 Nord-Ovest      4 1.1681511

Per le variabili categoriche, dplyr offre la comoda (seppur non sempre intuitiva) funzione recode(). Alcuni esempi:

cses_pg <- mutate(
  cses, 
  area2 = recode(area,              # Categoria singola per "Nord"
                 "Nord-Ovest" = "Nord",
                 "Nord-Est" = "Nord",
                 "Centro" = "Centro",
                 "Sud" = "Sud"),
  area3 = recode(area,              # Le osservazioni del "Nord-Ovest" diventano missing
                 "Nord-Ovest" = NA_character_,
                 "Nord-Est" = "Nord",
                 "Centro" = "Centro",
                 "Sud" = "Sud"),
  eco_neg = recode(eco_eval,        # Variabile dummy per valutazione negativa dell'economia
                   `-1` = 1,
                   .default = 0)
)
# Controllare le ricodifiche
table(cses_pg$area, cses_pg$area2)
##             
##              Centro Nord Sud
##   Centro        446    0   0
##   Nord-Est        0  349   0
##   Nord-Ovest      0  553   0
##   Sud             0    0 653
table(cses_pg$area, cses_pg$area3)
##             
##              Centro Nord Sud
##   Centro        446    0   0
##   Nord-Est        0  349   0
##   Nord-Ovest      0    0   0
##   Sud             0    0 653
table(cses_pg$eco_eval, cses_pg$eco_neg)
##     
##        0   1
##   -1   0 646
##   0  823   0
##   1  509   0

Naturalmente si può sempre utilizzare la cara vecchia funzione ifelse():

cses_pg <- mutate(
  cses, 
  eta_gr2 = ifelse(cses$eta <= 30, "<=30",
                   ifelse(cses$eta <= 50, "31-50",
                          "51+"))
)

Mettere tutto assieme: l’operatore “pipe” %>%

L’operatore pipe è quello che rende dplyr speciale. È una funzione che serve a concatenare altre funzioni. Tuttavia questo cambia completamente la logica del linguaggio di R.

Per darvi un’idea della logica, Andrew Heiss fa un ottimo esempio in questo tweet. Immaginate che dovete scrivere un programma con le istruzioni per svegliarsi la mattina e uscire di casa per andare al lavoro. In R, utilizzando gli oggetti, questo verrebbe scritto nel seguente modo (in Inglese, perchè in Italiano viene male):

me
me_awake <- wake_up(me)
me_out_of_bed <- get_out_of_bed(me_awake)
me_dressed <- get_dressed(me_out_of_bed)
me_out <- leave_house(me_dressed)

Quindi, prima usiamo la funzione wake_up() con l’oggetto me, poi usiamo la funzione get_out_of_bed() con l’oggetto me_awake, risultante dall’operazione precedente, e così via.

Tutta questa sequenza può anche essere scritta inserendo le funzioni una dentro l’altra:

me_out <- leave_house(get_dressed(get_out_of_bed(wake_up(me))))

In tal caso iniziamo a scrivere partendo dall’ultima funzione, e man mano che scriviamo procediamo al contrario, da fuori a dentro, fino ad arrivare all’oggetto di partenza.

Ora, l’operatore pipe ci permette di concatenare tutte queste funzioni in modo che (1) non dobbiamo creare troppi oggetti–come faremmo se volessimo programmare in sequenza, e (2) non dobbiamo scrivere la sequenza dalla fine verso l’inizio–come faremmo se volessimo mettere le funzioni una dentro l’altra. Utilizzando il pipe, la nostra sequenza sarebbe:

me_out <- me %>%
  wake_up() %>%
  get_out_of_bed() %>%
  get_dressed() %>%
  leave_house()

In altre parole, l’operatore pipe permette di svolgere le nostre operazioni di data management in modo sequenziale. Per esempio, possiamo fare tutte le operazioni che abbiamo visto partendo dal dataset originale e creando un oggetto alla fine:

cses_pg <- cses %>%                     # Partiamo dal dataset "cses"
  select(eta, area) %>%                 # Selezionare variabili
  filter(eta > 40) %>%                  # Selezionare osservazioni
  mutate(
    area2 = recode(area,                # Creare una nuova variabile
                 "Nord-Ovest" = "Nord",
                 "Nord-Est" = "Nord",
                 "Centro" = "Centro",
                 "Sud" = "Sud")
    )
head(cses_pg)
##   eta       area  area2
## 1  60        Sud    Sud
## 2  62     Centro Centro
## 3  68   Nord-Est   Nord
## 4  62        Sud    Sud
## 5  54 Nord-Ovest   Nord
## 6  71 Nord-Ovest   Nord

Fare operazioni per gruppi con group_by()

Ora che sappiamo come funziona l’operatore pipe, possiamo tirare fuori il meglio da dplyr. Una delle caratteristiche più utili di dplyr (e probabilmente la ragione per cui molte persone iniziano ad usarlo) è la possibilità di eseguire operazioni per diversi gruppi all’interno dei dati. Questo può sembrare banale (SPSS e Stata lo rendono piuttosto facile da fare), tuttavia farlo con R base non è affatto intuitivo.

Per esempio, possiamo centrare la variabile eta intorno alla media come abbiamo fatto prima, ma invece di prendere la media dell’intero campione, possiamo centrare i valori degli intervistati in ogni area geografica sulla media della loro area.

cses_pg <- cses %>%
  group_by(area) %>%
  mutate(
      eta_std = scale(eta)
  )
plot(cses_pg$eta, cses_pg$eta_std)

Notare che in questo caso non abbiamo una sola linea (e quindi un solo gruppo di osservazioni) ma più di una. Questo perchè ora il valore \(0\), la media, è diversa in ogni gruppo, dato che l’età media cambia tra aree geografiche.

Nota: ricordate che ogni volta che utilizzate la funzione group_by(), il dataset rimarrà diviso in gruppi! Questa è una caratteristica che distingue il tibble dal data frame: il fatto che mantenga in memoria informazione riguardo l’eventuale divisione in gruppi di un dataset. Se volete fare altre operazioni su tutti i dati (per esempio calcolare la media di una variabile in tutto il dataset) dovete “annullare” il raggruppamento utilizzando la funzione ungroup().

cses_pg <- cses %>%
  group_by(area) %>%
  mutate(
      eta_std = scale(eta)    # Per gruppi
  ) %>%
  ungroup() %>%
  mutate(
      eta_std_2 = scale(eta)  # In tutto il campione
  )

Estrarre informazioni dai dati: summarize()

summarize() è una delle più importanti funzionalità di dplyr, che ci permette di “riassumere” il contenuto di una o più variabili utilizzando statistiche di vario tipo e possibilmente salvando queste informazioni in un oggetto (notare che la stessa funzione può anche essere chiamata summarise(), in British English).

Per fare un esempio, possiamo creare una tabella che include media e deviazione standard della variabile eta in ogni area geografica:

cses_gr <- cses %>%
  group_by(area) %>%
  summarize(
    eta_m = mean(eta, na.rm = T),
    eta_sd = sd(eta, na.rm = T)
  )
cses_gr
## # A tibble: 4 x 3
##   area       eta_m eta_sd
##   <chr>      <dbl>  <dbl>
## 1 Centro      51.8   16.5
## 2 Nord-Est    53.6   16.6
## 3 Nord-Ovest  52.8   16.9
## 4 Sud         48.8   16.7

Usare mutate() e summarize() su più di una variabile alla volta: across()

Un’ulteriore caratteristica che rende dplyr molto conveniente è la possibilità di generalizzare le funzioni mutate() e summarize() a più di una variabile. In questo modo non occorrerà ripetere lo stesso comando più volte nel caso si debba applicare la stessa funzione a più variabili (naturalmente la funzione deve essere la stessa).

A partire dalla versione più recente di dplyr, è possibile ottenere questo con la funzione across(), che può essere utilizzata sia con mutate che con summarize.

Per esempio, proviamo a usare grepl() per creare due variabili dummy che identificano le persone che hanno risposto “immigrazione” alle due domande sul “problema più importante in Italia”, mip1 e mip2:

cses_pg <- cses %>%
  mutate(
    across(
      starts_with("mip"),
      ~ifelse(grepl("migr", .), 1, 0),
      .names = "{.col}_r"
    )
  )
# Per quante persone l'immigrazione è il problema più importante?
mean(cses_pg$mip1_r)
## [1] 0.05847076
# E per quante persone l'immigrazione è il secondo problema più importante?
mean(cses_pg$mip2_r)
## [1] 0.1009495

Nelle versioni precedenti di dplyr questa operazione veniva svolta dalle funzioni mutate_at() e summarize_at(), che funzionano ancora.

È ancora possibile utilizzare mutate_all() e summarize_all() per applicare la stessa funzione su tutte le variabili, con una logica molto simile ad applicare la funzione apply() per colonne. Per esempio, se vogliamo sapere quante osservazioni mancanti abbiamo in ogni variabile, possiamo usare summarize_all():

cses %>%
  summarize_all(~sum(is.na(.)))
##   id sex eta eta_gr mip1 mip2 eco_eval area titstu
## 1  0   0   0      0   62  195       23    0      0

Notare che il modo più recente per fare questo è utilizzare summarize() e across():

cses %>%
  summarize(
    across(
       everything(),
       ~sum(is.na(.))
    )
  )
##   id sex eta eta_gr mip1 mip2 eco_eval area titstu
## 1  0   0   0      0   62  195       23    0      0