Un data frame è la struttura dati più utilizzata nell’analisi statistica in R. Si può pensare come una matrice le cui colonne possono avere tipi diversi: alcune numeriche, altre character, altre ancora fattori o logiche.
Più formalmente, un data frame è una lista di vettori (o fattori) di uguale lunghezza, dove:
Le componenti possono essere vettori (numerici, character, logici), fattori, matrici numeriche, liste o altri data frame, ma tutti i vettori devono avere la stessa lunghezza.
Riutilizziamo i dati costruiti in precedenza, necessari per gli esempi.
country <- c("Italia", "Germania", "Francia", "Germania", "Germania", "Germania",
"Francia", "Italia", "Italia", "Francia")
countryf <- factor(country)
age <- c(47, 44, 44, 40, 38, 36, 42, 34, 34, 44)
gender <- c(1, 1, 2, 1, 1, 2, 1, 2, 2, 2)
genderf <- factor(gender)
levels(genderf) <- c("F", "M")Si crea con la funzione data.frame(), passando le
colonne come argomenti nominati:
under40 <- age < 40
dat <- data.frame(Country = countryf, Age = age, Sex = genderf,
Under40 = under40)Le principali funzioni per ispezionare un data frame:
#> 'data.frame': 10 obs. of 4 variables:
#> $ Country: Factor w/ 3 levels "Francia","Germania",..: 3 2 1 2 2 2 1 3 3 1
#> $ Age : num 47 44 44 40 38 36 42 34 34 44
#> $ Sex : Factor w/ 2 levels "F","M": 1 1 2 1 1 2 1 2 2 2
#> $ Under40: logi FALSE FALSE FALSE FALSE TRUE TRUE ...
#> [1] TRUE
💡 Nel pannello RStudio si può usare
View(dat)per aprire una visualizzazione interattiva a griglia (non eseguibile in un documento Rmd).
Il subsetting di un data frame usa la stessa notazione
[riga, colonna] delle matrici. Lasciando vuota una delle
due dimensioni si seleziona tutto.
#> [1] 44
Esistono tre modi equivalenti per selezionare una o più colonne:
#> [1] F F M F F M F M M M
#> Levels: F M
dat["Age"] vs dat[, "Age"]⚠️ Questa distinzione è importante e spesso fonte di errori:
#> 'data.frame': 10 obs. of 1 variable:
#> $ Age: num 47 44 44 40 38 36 42 34 34 44
#> num [1:10] 47 44 44 40 38 36 42 34 34 44
La prima forma con parentesi singola senza virgola restituisce sempre un data frame; la seconda con la virgola restituisce un vettore. Questa differenza può influenzare le funzioni successive che ricevono il risultato.
#> num [1:10] 47 44 44 40 38 36 42 34 34 44
Si assegna NULL alla colonna che si vuole rimuovere. In
alternativa si può usare l’indicizzazione negativa.
Si usa cbind.data.frame() oppure si assegna direttamente
con $:
stringsAsFactorsFino a R 3.x, i vettori character venivano automaticamente convertiti
in fattori all’interno di un data frame. Da R 4.0 il comportamento di
default è cambiato: i character rimangono character. Si può forzare la
conversione con l’argomento stringsAsFactors = TRUE.
#> 'data.frame': 5 obs. of 2 variables:
#> $ x: int 1 2 3 4 5
#> $ y: chr "A" "B" "C" "D" ...
df <- data.frame(x = 1:5,
y = c("A", "B", "C", "D", "E"),
stringsAsFactors = TRUE)
str(df) # y è ora un factor#> 'data.frame': 5 obs. of 2 variables:
#> $ x: int 1 2 3 4 5
#> $ y: Factor w/ 5 levels "A","B","C","D",..: 1 2 3 4 5
attach()
e detach()attach() aggiunge le colonne di un data frame
all’environment globale, rendendo le variabili
accessibili direttamente per nome senza il prefisso dat$.
detach() annulla l’operazione.
# Age # ❌ ERRORE: Age non esiste nell'environment globale
attach(dat)
Age # ✅ ora è accessibile direttamente #> [1] 47 44 44 40 38 36 42 34 34 44
⚠️
attach()crea però una copia delle variabili nell’environment. Le modifiche al data frame originale non si riflettono automaticamente sulla copia, e viceversa:
dat$Age <- Age + 1 # modifica la colonna nel data frame...
Age # ...ma la copia nell'environment non cambia#> [1] 47 44 44 40 38 36 42 34 34 44
#> [1] 48 45 45 41 39 37 43 35 35 45
💡 Raccomandazione:
attach()è utile per sessioni esplorative rapide, ma è sconsigliato in script e analisi strutturate perché può generare conflitti di nomi difficili da individuare. Preferire sempredat$variabileo il subsetting esplicito.
Esercizio 1 Esegui il codice seguente:
Definisci un data frame chiamato
newdfcon colonnez,y,x(in quest’ordine). Ispeziona la struttura constr()e verifica cheis.data.frame(newdf)restituiscaTRUE.
x <- runif(8)
y <- letters[1:8]
z <- sample(c(rep(TRUE, 5), rep(FALSE, 3)))
newdf <- data.frame(z = z, y = y, x = x)
str(newdf)#> 'data.frame': 8 obs. of 3 variables:
#> $ z: logi TRUE TRUE FALSE TRUE TRUE TRUE ...
#> $ y: chr "a" "b" "c" "d" ...
#> $ x: num 0.339 0.783 0.252 0.112 0.453 ...
#> [1] TRUE
Esercizio 2 Crea un data frame con 5 righe, con una colonna
nome(character) e una colonnaeta(numeric). Seleziona con il subsetting solo i soggetti con età superiore a 25, usando due metodi diversi (indice numerico e condizione logica).
df <- data.frame(
nome = c("Alice", "Bob", "Carlo", "Diana", "Eva"),
eta = c(23, 31, 28, 22, 35)
)
# Metodo 1: condizione logica sulla colonna
df[df$eta > 25, ]dplyrQuando si lavora con data frame, dplyr offre una
sintassi molto più espressiva. Il pacchetto dplyr fa parte
del tidyverse e offre un insieme di funzioni — chiamate
verbi — per manipolare data frame in modo leggibile e
componibile. Ogni verbo fa una cosa sola e fa bene quella cosa.
I principali verbi sono:
| Verbo | Operazione |
|---|---|
filter() |
seleziona righe secondo una condizione |
select() |
seleziona colonne |
rename() |
rinomina colonne |
mutate() |
aggiunge o trasforma colonne |
arrange() |
riordina le righe |
summarise() |
calcola statistiche (anche per gruppo con
group_by()) |
Vediamo il funzionamento di tutti i verbi riportati sopra riprendendo il data frame dell’Esercizio 2 (dei cinque studenti con nome ed età, aggiungiamo voto e gruppo). Applichiamo ogni operazione in parallelo tra R base e dplyr.
df <- data.frame(
nome = c("Alice", "Bob", "Carlo", "Diana", "Eva"),
eta = c(23, 31, 28, 22, 35),
voto = c(28, 25, 30, 27, 29),
gruppo = c("A", "B", "A", "B", "A")
)
dffilter() — Selezionare righeIn R base selezionano le righe con [indici, ] o
[condizioni logiche,]. filter() permette di
ottenere il risultato in modo semplice.
Il pipe %>% (o |> da R 4.1) passa il
risultato di un’espressione come primo argomento di
quella successiva. Rende il codice leggibile da sinistra a destra, come
una sequenza di istruzioni in linguaggio naturale.
Vediamo come selezionare soltanto gli studenti aventi età superiore a 25 anni.
Quindi vediamo come selezionare quelli aventi età superiore a 25 anni e voto maggiore o uguale a 28
select() — Selezionare colonneIn R base si selezionano le colonne con [, indici] o
[, nomi]. select() offre una sintassi più
espressiva.
Sull’esempio, vediamo come selezionare le colonne nome e
voto.
E possiamo utilizzare select anche per escludere
colonne. Quindi escludiamo le colonne voto e
gruppo
mutate() — Creare o modificare colonnePossiamo creare e/o modificare colonne. Ad esempio costruiamo la
variabile voto_norm
Possiamo anche includere più colonne contemporaneamente
# Più colonne contemporaneamente
df |> mutate(
voto_norm = voto / 30,
eta_class = ifelse(eta < 28, "giovane", "senior")
)#> 'data.frame': 5 obs. of 4 variables:
#> $ nome : chr "Alice" "Bob" "Carlo" "Diana" ...
#> $ eta : num 23 31 28 22 35
#> $ voto : num 28 25 30 27 29
#> $ gruppo: chr "A" "B" "A" "B" ...
rename() — Rinominare nomi colonnearrange() — Ordinare righeSupponiamo di voler ordinare le osservazioni secondo un criterio, ad esempio vogliamo ordinare le righe secondo in ordine (decrescente) di voto.
Possiamo anche richiedere unordinamento multiplo, ad esempio ordinare per gruppo e per voto.
summarise() + group_by() — Statistiche per
gruppoIn R base poosiamo calcolare statistiche per gruppo usando: (i)
aggregate() che usa una formula (y ~ gruppo) e restituisce
un data frame, rendendola comoda quando si vogliono più variabili o si
vuole passare il risultato ad altre funzioni; (ii) tapply()
applica una funzione a un vettore suddiviso per i livelli di un fattore
e restituisce un array — più compatta ma meno flessibile quando si
vogliono statistiche multiple.
In dplyr invece i due passaggi sono separati e
espliciti: prima si dichiara il raggruppamento con
group_by(), poi si calcolano le statistiche con
summarise(). Il vantaggio è che si possono calcolare più
statistiche in un colpo solo, usando nomi personalizzati per le colonne
risultanti. La funzione n() conta semplicemente il numero
di righe per gruppo.
#> A B
#> 29 26
Inoltre con dplyr possiamo agevolmente calcolare simultaneamente la media per gruppo di più variabili.
# dplyr
df |>
group_by(gruppo) |>
summarise(
n = n(),
media_voto = mean(voto),
media_eta = mean(eta)
)Obiettivo: Ottenere la media voti per gruppo per solo studenti con età > 22, e che sia garantito un ordinamento per media decrescente
# R base — operazioni separate, difficile da leggere
subset_df <- df[df$eta > 22, ]
agg <- aggregate(voto ~ gruppo, data = subset_df, FUN = mean)
agg[order(agg$voto, decreasing = TRUE), ]# dplyr con pipe — si legge come una frase
df |>
filter(eta > 22) |>
group_by(gruppo) |>
summarise(media_voto = mean(voto)) |>
arrange(desc(media_voto))Esercizio 3 — dplyr
Usando il data framedfdefinito sopra:
- Seleziona solo gli studenti del gruppo
"A"- Aggiungi una colonna
promosso=TRUEsevoto >= 28- Conta quanti sono promossi per gruppo
# 3. Conteggio promossi per gruppo
df |>
mutate(promosso = voto >= 28) |>
group_by(gruppo) |>
summarise(n_promossi = sum(promosso), totale = n())Usiamo il dataset CO2 incluso in R, che contiene
misurazioni sull’assorbimento di CO₂ in piante di due origini
geografiche:
#> Classes 'nfnGroupedData', 'nfGroupedData', 'groupedData' and 'data.frame': 84 obs. of 5 variables:
#> $ Plant : Ord.factor w/ 12 levels "Qn1"<"Qn2"<"Qn3"<..: 1 1 1 1 1 1 1 2 2 2 ...
#> $ Type : Factor w/ 2 levels "Quebec","Mississippi": 1 1 1 1 1 1 1 1 1 1 ...
#> $ Treatment: Factor w/ 2 levels "nonchilled","chilled": 1 1 1 1 1 1 1 1 1 1 ...
#> $ conc : num 95 175 250 350 500 675 1000 95 175 250 ...
#> $ uptake : num 16 30.4 34.8 37.2 35.3 39.2 39.7 13.6 27.3 37.1 ...
#> - attr(*, "formula")=Class 'formula' language uptake ~ conc | Plant
#> .. ..- attr(*, ".Environment")=<environment: R_EmptyEnv>
#> - attr(*, "outer")=Class 'formula' language ~Treatment * Type
#> .. ..- attr(*, ".Environment")=<environment: R_EmptyEnv>
#> - attr(*, "labels")=List of 2
#> ..$ x: chr "Ambient carbon dioxide concentration"
#> ..$ y: chr "CO2 uptake rate"
#> - attr(*, "units")=List of 2
#> ..$ x: chr "(uL/L)"
#> ..$ y: chr "(umol/m^2 s)"
select()
— Selezionare Colonne💡
select()permette di selezionare colonne per indice, per nomi e range di nomi
💡
select()permette anche di selezionare colonne di un certo tipo, ad esempio di tipo numeric
💡
select()mette a disposizione funzioni ausiliarie per selezionare colonne per pattern sul nome:
filter()
— Selezionare Righe💡
filter()ci permetted di selezionare le righe aventi certe caratteristiche.
arrange() — Riordinare le Righe💡
arrange()ci permette di riordinare le righe (con ordinamento crescente/decrescente) per variabile/i
rename()
— Rinominare Colonne💡
rename()ci permette di rinominare il nome di colonne, ad esempio modifichiamo il nome conc in Concentration e uptake in UptakeRates
# dplyr — nuovo_nome = vecchio_nome
CO2rename <- rename(CO2, Concentration = conc, UptakeRates = uptake)mutate()
— Aggiungere o Trasformare Colonne💡
mutate()ci permette di aggiungere colonne che possono essere trasformazioni di quelle originali, ad esempio cnsideriamo la variabile standardizzata di conc e la variabile centrata di uptake
riordinare le righe (con ordinamento crescente/decrescente) per variabile/i
# dplyr — più colonne in un solo passaggio
CO2mutate <- mutate(CO2,
conc.scale = (conc - mean(conc)) / sd(conc),
uptake = uptake - mean(uptake))
summary(CO2mutate$uptake)#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> -19.513 -9.313 1.087 0.000 9.912 18.287
#> Min. 1st Qu. Median Mean 3rd Qu. Max.
#> 7.70 17.90 28.30 27.21 37.12 45.50
💡 Con
mutate()il data frame originale non viene mai modificato: il risultato è sempre un nuovo oggetto.
#> Classes 'nfnGroupedData', 'nfGroupedData', 'groupedData' and 'data.frame': 84 obs. of 5 variables:
#> $ Plant : Ord.factor w/ 12 levels "Qn1"<"Qn2"<"Qn3"<..: 1 1 1 1 1 1 1 2 2 2 ...
#> $ Type : Factor w/ 2 levels "Quebec","Mississippi": 1 1 1 1 1 1 1 1 1 1 ...
#> $ Treatment: Factor w/ 2 levels "nonchilled","chilled": 1 1 1 1 1 1 1 1 1 1 ...
#> $ conc : chr "95" "175" "250" "350" ...
#> $ uptake : chr "16" "30.4" "34.8" "37.2" ...
mutate()CO2NA <- mutate(CO2, concNA = ifelse(conc < 100, NA, conc))
# Quanti NA per colonna?
colSums(is.na(CO2NA))#> Plant Type Treatment conc uptake concNA
#> 0 0 0 0 0 12
#> Plant Type Treatment conc uptake concNA
#> 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.1428571
#> [1] TRUE
#> [1] 72
# Selezionare solo le righe complete
CO2clean <- CO2NA[complete.cases(CO2NA), ]
any(is.na(CO2clean))#> [1] FALSE
# na.rm = TRUE è necessario quando la colonna contiene NA
CO2mutateNA <- mutate(CO2NA,
conc.scale = (concNA - mean(concNA, na.rm = TRUE)) /
sd(concNA, na.rm = TRUE))
CO2mutateNA⚠️ Con R base,
mean()esd()restituisconoNAse il vettore contiene valori mancanti. Usare semprena.rm = TRUEin presenza di NA.
transmute() e .keep = "none"💡 Con
transmute()o conmutate().keep = "none"possiamo mantenere solo le colonne trasformate (scartando tutto il resto):
# transmute(): equivale a mutate() + select() delle sole colonne create
CO2trmute <- transmute(CO2,
conc = (conc - mean(conc)) / sd(conc),
uptake = uptake - mean(uptake))
CO2trmute # oppure con mutate() e .keep = "none"
CO2mutateKeepNone <- mutate(CO2,
conc = (conc - mean(conc)) / sd(conc),
uptake = uptake - mean(uptake),
.keep = "none")
CO2mutateKeepNone group_by() e summarise() — Statistiche per
Gruppo#> gropd_df [84 × 5] (S3: grouped_df/tbl_df/tbl/data.frame)
#> $ Plant : Ord.factor w/ 12 levels "Qn1"<"Qn2"<"Qn3"<..: 1 1 1 1 1 1 1 2 2 2 ...
#> $ Type : Factor w/ 2 levels "Quebec","Mississippi": 1 1 1 1 1 1 1 1 1 1 ...
#> $ Treatment: Factor w/ 2 levels "nonchilled","chilled": 1 1 1 1 1 1 1 1 1 1 ...
#> $ conc : num [1:84] 95 175 250 350 500 675 1000 95 175 250 ...
#> $ uptake : num [1:84] 16 30.4 34.8 37.2 35.3 39.2 39.7 13.6 27.3 37.1 ...
#> - attr(*, "formula")=Class 'formula' language uptake ~ conc | Plant
#> .. ..- attr(*, ".Environment")=<environment: R_EmptyEnv>
#> - attr(*, "outer")=Class 'formula' language ~Treatment * Type
#> .. ..- attr(*, ".Environment")=<environment: R_EmptyEnv>
#> - attr(*, "labels")=List of 2
#> ..$ x: chr "Ambient carbon dioxide concentration"
#> ..$ y: chr "CO2 uptake rate"
#> - attr(*, "units")=List of 2
#> ..$ x: chr "(uL/L)"
#> ..$ y: chr "(umol/m^2 s)"
#> - attr(*, "groups")= tibble [2 × 2] (S3: tbl_df/tbl/data.frame)
#> ..$ Type : Factor w/ 2 levels "Quebec","Mississippi": 1 2
#> ..$ .rows: list<int> [1:2]
#> .. ..$ : int [1:42] 1 2 3 4 5 6 7 8 9 10 ...
#> .. ..$ : int [1:42] 43 44 45 46 47 48 49 50 51 52 ...
#> .. ..@ ptype: int(0)
#> ..- attr(*, ".drop")= logi TRUE
dat["col"] restituisce un data frame;
dat[, "col"] e dat$col restituiscono un
vettore.NULL:
dat$col <- NULL.stringsAsFactors = TRUE è necessario solo se si vuole
convertire automaticamente i character in factor (da R 4.0 non è più il
default).attach() con cautela: le modifiche al data frame
originale non si propagano alla copia nell’environment.mutate() e gli altri verbi dplyr non modificano
mai il data frame originale: restituiscono sempre un nuovo
oggetto.%>% si legge come “poi”: rende le
pipeline di operazioni leggibili dall’alto verso il basso.# DATA FRAME
data.frame(col1 = v1, col2 = v2)
is.data.frame(df) # Verifica
str(df); dim(df) # struttura e dimensione
head(df); tail(df) #Prime e ultime righe
names(df) # nomi variabili
# Accesso a elementi e variabili
df[i, j]; df[, "col"]; df$col
df$nuova <- ... # nuova colonna
df$col <- NULL # elimina colonna
cbind.data.frame(df, nuova_col) #aggiungi nuova_col a df
df[df$col > 5] # Filtra rughe su condizione
# Filtra righe si condizione (col1>5) e
# seleziona colonne (col1 e col2)
subset(df, subset = col1 > 5,
select = c(col1,col2))
attach() # Aggiungere variabili in environment
detach() # Ripristina
# DPLYR — VERBI PRINCIPALI
library(dplyr)
select(df, col1, col2) # seleziona colonne
select(df, starts_with("x")) # ... per pattern
select(df, ends_with("x")) # ... per pattern
select(df, contains("x")) # ... per pattern
select(df, matches("x")) # ... per pattern
select(df, where(is.numeric)) # ... per tipo
filter(df, col > 5) # filtra righe per condizione
arrange(df, col) # ordina crescente
arrange(df, desc(col)) # ordina decrescente
rename(df, nuovo = vecchio) # rinomina colonne
mutate(df, nuova = col * 2) # aggiunge/trasforma colonne
mutate(df, nuova = col * 2, .keep = "none") # aggiunge/trasforma colonne + seleziona quelle
transmute(df, nuova= col * 2) # mutate() + select()
group_by(df, gruppo) # raggruppa
summarise(df, m = mean(col)) # statistiche per gruppo
n(); count() # Funzioni di conteggio
across() # permette di applicare una funzione a più colonne
# PIPE
df |> filter(...) |> select(...) |> mutate(...)
# Altro
summary(df) # Riassunti numerici variabili
colSums(df); colMeans(df) # Somma e media per colonna (per continue)
rowSums(df); rowMeans(df) # Somma e media per riga (per continue)
df[, sapply(df, is.numeric)] #seleziona colonne per tipo
# Funzione sui valori di x per gruppo
aggregate(x ~ gruppo, data = df, FUN)
# Gestione NA
# somma e proporzione NA per le variaibli in df
colSums(is.na(df)); colMeans(is.na(df))
any(is.na(df)) # Almeno un NA nel data frame?
complete.cases(df) # righe che non presentano NA
# Omissione di tutte le righe aventi un NA per almeno una variabile
na.omit(df); df[complete.cases(df),]