1 Data Frame

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 colonne rappresentano le variabili
  • le righe rappresentano le unità statistiche

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.

1.1 Creazione

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:

str(dat)           # struttura: tipo di ogni colonna
#> '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 ...
is.data.frame(dat) # verifica il tipo
#> [1] TRUE
head(dat)          # prime 6 righe

💡 Nel pannello RStudio si può usare View(dat) per aprire una visualizzazione interattiva a griglia (non eseguibile in un documento Rmd).


1.2 Selezione di Elementi (Subsetting)

Il subsetting di un data frame usa la stessa notazione [riga, colonna] delle matrici. Lasciando vuota una delle due dimensioni si seleziona tutto.

dat[3, 2]       # elemento in riga 3, colonna 2
#> [1] 44
dat[1:3, 2:4]   # prime tre righe, colonne da 2 a 4
dat[3, ]        # intera terza riga

1.2.1 Selezione di Colonne

Esistono tre modi equivalenti per selezionare una o più colonne:

dat[, c("Age", "Sex")]   # per nome → restituisce data frame
dat[, c(2, 3)]           # per indice numerico
dat[, 2:3]               # per range di indici
dat$Sex                  # con $ → restituisce il vettore/factor direttamente
#>  [1] F F M F F M F M M M
#> Levels: F M

1.2.2 dat["Age"] vs dat[, "Age"]

⚠️ Questa distinzione è importante e spesso fonte di errori:

str(dat["Age"])     # → data.frame con 1 colonna (mantiene la struttura)
#> 'data.frame':    10 obs. of  1 variable:
#>  $ Age: num  47 44 44 40 38 36 42 34 34 44
str(dat[, "Age"])   # → vettore numerico (perde la struttura)
#>  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.

x <- dat[, 2]
str(x)   # vettore numerico, non più un data frame
#>  num [1:10] 47 44 44 40 38 36 42 34 34 44

1.3 Modificare un Data Frame

1.3.1 Eliminare una Colonna

Si assegna NULL alla colonna che si vuole rimuovere. In alternativa si può usare l’indicizzazione negativa.

dat$Under40 <- NULL   # elimina la colonna Under40
head(dat)
# Equivalente con indice negativo (non eseguito, sovrascrive dat): 
# dat <- dat[, -2] 

1.3.2 Aggiungere una Colonna

Si usa cbind.data.frame() oppure si assegna direttamente con $:

 cbind.data.frame(dat, under40) # aggiunge under40 com'è (logical) 
X <- cbind.data.frame(dat, Under40 = under40 * 1)    # converte logical → 0/1 
X 

1.3.3 Creare una Nuova Variabile con Condizione Logica

# TRUE se Country è "Italy", FALSE altrimenti
dat$CountryTF <- dat$Country == "Italia"
head(dat)

1.4 stringsAsFactors

Fino 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.

df <- data.frame(x = 1:5,
                 y = c("A", "B", "C", "D", "E")) 
str(df)   # y è character
#> '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

1.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
detach(dat)
dat$Age              # il data frame è stato aggiornato
#>  [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 sempre dat$variabile o il subsetting esplicito.


1.5.1 Esercizi

Esercizio 1 Esegui il codice seguente:

x <- runif(8)
y <- letters[1:8]
z <- sample(c(rep(TRUE, 5), rep(FALSE, 3)))

Definisci un data frame chiamato newdf con colonne z, y, x (in quest’ordine). Ispeziona la struttura con str() e verifica che is.data.frame(newdf) restituisca TRUE.

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 ...
is.data.frame(newdf)
#> [1] TRUE

Esercizio 2 Crea un data frame con 5 righe, con una colonna nome (character) e una colonna eta (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, ]
# Metodo 2: which() per ottenere prima gli indici
df[which(df$eta > 25), ]

2 Operazioni su Data Frame: R Base vs dplyr

Quando 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())
# install.packages("dplyr")  # solo la prima volta
library(dplyr)

2.1 Esempio su un Dataset Semplice

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")
)
df

2.1.1 filter() — Selezionare righe

In 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.

# R base
df[df$eta > 25, ]
# dplyr
filter(df, eta > 25)
# Con pipe
df |> filter(eta > 25)

Quindi vediamo come selezionare quelli aventi età superiore a 25 anni e voto maggiore o uguale a 28

# R base
df[df$eta > 25 & df$voto >= 28, ]
# oppure con subset()
subset(df, eta > 25 & voto >= 28)
# dplyr
filter(df, eta > 25, voto >= 28)
# Con pipe 
df |> filter(eta > 25, voto >= 28)

2.1.2 select() — Selezionare colonne

In 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.

# R base
df[, c("nome", "voto")]
# dplyr
select(df, nome, voto)
# pipe 
df |> select(-gruppo)

E possiamo utilizzare select anche per escludere colonne. Quindi escludiamo le colonne voto e gruppo

# R base
df[, -c(3,4)]
# dplyr
select(df, -voto, -gruppo)
# pipe 
df |> select(-voto, -gruppo)

2.1.3 mutate() — Creare o modificare colonne

Possiamo creare e/o modificare colonne. Ad esempio costruiamo la variabile voto_norm

# R base
df$voto_norm <- df$voto / 30
df
df <- df[, -5]

# dplyr 
mutate(df, voto_norm = voto / 30)
# pipe
df |> mutate(voto_norm = voto / 30)

Possiamo anche includere più colonne contemporaneamente

# pipe
df |> mutate(voto_norm = voto / 30,
               eta_class = ifelse(eta < 28, "giovane", "senior"))
# Più colonne contemporaneamente
df |> mutate(
  voto_norm = voto / 30,
  eta_class = ifelse(eta < 28, "giovane", "senior")
)
str(df)
#> '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" ...

2.1.4 rename() — Rinominare nomi colonne

# dplyr
rename(df, valutazione = voto)
# Con pipe
df |> rename(valutazione = voto)
# R base — si modifica names() direttamente
names(df)[names(df) == "voto"] <- "valutazione"

# Reimpostiamo voto 
names(df)[names(df) == "valutazione"] <- "voto"

2.1.5 arrange() — Ordinare righe

Supponiamo di voler ordinare le osservazioni secondo un criterio, ad esempio vogliamo ordinare le righe secondo in ordine (decrescente) di voto.

# R base
df[order(df$voto, decreasing = TRUE), ]
# dplyr
arrange(df, desc(voto))
# pipe 
df |> arrange(desc(voto))

Possiamo anche richiedere unordinamento multiplo, ad esempio ordinare per gruppo e per voto.

# R base
df[order(df$gruppo, -df$voto), ]
# Nota la differenza
df[order(df$gruppo, df$voto, decreasing = TRUE), ]
# dplyr
arrange(df, gruppo, desc(voto))
# Pipe
df |> arrange(gruppo, desc(voto))

2.1.6 summarise() + group_by() — Statistiche per gruppo

In 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.

# R base
aggregate(voto ~ gruppo, data = df, FUN = mean)
tapply(df$voto, df$gruppo, mean)
#>  A  B 
#> 29 26
# dplyr
df |>
  group_by(gruppo) |>
  summarise(
    media_voto = mean(voto),
    n          = n()
  )

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)
  )

2.1.7 Pipeline completa

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 frame df definito sopra:

  1. Seleziona solo gli studenti del gruppo "A"
  2. Aggiungi una colonna promosso = TRUE se voto >= 28
  3. Conta quanti sono promossi per gruppo
# 1. Studenti del gruppo A
df |> filter(gruppo == "A")
# 2. Colonna promosso
df |> mutate(promosso = voto >= 28)
# 3. Conteggio promossi per gruppo
df |>
  mutate(promosso = voto >= 28) |>
  group_by(gruppo) |>
  summarise(n_promossi = sum(promosso), totale = n())

3 Esempio

Usiamo il dataset CO2 incluso in R, che contiene misurazioni sull’assorbimento di CO₂ in piante di due origini geografiche:

data(CO2)
str(CO2)
#> 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)"

3.1 select() — Selezionare Colonne

💡 select() permette di selezionare colonne per indice, per nomi e range di nomi

CO2sub <- select(CO2, 2:5)
CO2sub
CO2sub <- select(CO2, Type, uptake)     # solo "Type" e "uptake"
CO2sub 
CO2sub <- select(CO2, Type:uptake)     # da "Type" a "uptake"
CO2sub

💡 select() permette anche di selezionare colonne di un certo tipo, ad esempio di tipo numeric

select(CO2, where(is.numeric))
CO2[, sapply(CO2, is.numeric)]

💡 select() mette a disposizione funzioni ausiliarie per selezionare colonne per pattern sul nome:

CO2sub <- select(CO2, ends_with("nt"))              # colonne che finiscono con "nt"
CO2sub 
CO2sub <- select(CO2, ends_with(c("nt", "e")))      # finiscono con "nt" o "e"
CO2sub 
CO2sub <- select(CO2, contains("on"))               # contengono "on"
CO2sub 
CO2sub <- select(CO2, matches("a[nk]"))             # regex: contengono "an" o "ak"
CO2sub 

3.2 filter() — Selezionare Righe

💡 filter() ci permetted di selezionare le righe aventi certe caratteristiche.

CO2filt <- filter(CO2, Type == "Mississippi" & uptake > 20)
CO2filt

3.3 arrange() — Riordinare le Righe

💡 arrange() ci permette di riordinare le righe (con ordinamento crescente/decrescente) per variabile/i

CO2arr <- arrange(CO2, Plant)            # crescente
CO2arr <- arrange(CO2, desc(Plant))      # decrescente

3.4 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)

3.5 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
summary(CO2$uptake)       # il data frame originale non è modificato
#>    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.

CO2mutate <- mutate(CO2, across(where(is.numeric), as.character))
str(CO2mutate)
#> 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" ...

3.5.1 Gestione dei valori mancanti in 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
# Proporzione di NA per colonna
colMeans(is.na(CO2NA))
#>     Plant      Type Treatment      conc    uptake    concNA 
#> 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.1428571
# C'è almeno un NA nell'intero data frame?
any(is.na(CO2NA))
#> [1] TRUE
# Quante righe sono complete (senza nessun NA)?
sum(complete.cases(CO2NA))
#> [1] 72
# Selezionare solo le righe complete
CO2clean <- CO2NA[complete.cases(CO2NA), ]
any(is.na(CO2clean))
#> [1] FALSE
CO2mutate <- mutate(CO2NA,
                    conc.scale = (concNA - mean(concNA)) /
                                    sd(concNA))
CO2mutate
# 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() e sd() restituiscono NA se il vettore contiene valori mancanti. Usare sempre na.rm = TRUE in presenza di NA.

3.5.2 transmute() e .keep = "none"

💡 Con transmute() o con mutate() .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 

3.6 group_by() e summarise() — Statistiche per Gruppo

CO2group <- group_by(CO2, Type)
str(CO2group)   # è ancora un data frame, ma "raggruppato"
#> 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
summarise(CO2group,
          uptMean   = mean(uptake),
          uptMedian = quantile(uptake, 0.5))