1.3 Le principali strutture di dati disponibili in R
Come abbiamo già detto, in R esistono tipi diversi di oggetti e in questa sezione daremo una breve descrizione di quelli che ci serviranno nel resto del manuale.
1.3.1 Tipi di dati di base
In R è possibile creare oggetti contenenti dati numerici, testuali o logici, che in R sono indicati rispettivamente come num
, chr
e logical
11.
Abbiamo già visto esempi di dati numerici e testuali, mentre i dati di tipo logico sono di solito ottenuti come risultato della verifica di una o più condizioni logiche. Ad esempio, dopo aver definito il vettore \(x = (10, -2, 0.3, -1, 5)\), ovvero
> x <- c(10, -2, 0.3, -1, 5)
supponiamo di voler verificare quali dei suoi elementi siano positivi e quali no. Per farlo è sufficiente eseguire il seguente codice, in cui il risultato della verifica viene memorizzato nel nuovo oggetto z
:
> z <- (x > 0)
> z
[1] TRUE FALSE TRUE FALSE TRUE
> str(z)
logi [1:5] TRUE FALSE TRUE FALSE TRUE
Come vediamo, z
contiene la risposta alla nostra domanda, risposta che viene data sotto forma di un vettore di valori TRUE
e FALSE
a seconda che i vari elementi di x
siano positivi o negativi.
1.3.2 Vettori
Si tratta della struttura di dati più importante in R rappresentata da un insieme di elementi tutti dello stesso tipo, ovvero numeri, stringhe di testo o valori logici.
Abbiamo già visto nei precedenti esempi che possiamo creare un vettore con la funzione c()
, la quale consente di concatenare più elementi in un unico oggetto. Molte funzioni di R restituiscono in output dei vettori, pertanto è importante imparare bene a manipolare questo tipo di oggetti.
La funzione length()
restituisce la dimensione di un vettore, ovvero il numero di elementi che lo compongono:
> length(z)
[1] 5
Per selezionare uno o più elementi di un vettore dobbiamo usare l’operatore []
(parentesi quadre). Ad esempio, possiamo estrarre il terzo elemento del vettore x
con il codice
> x[3]
[1] 0.3
dove il numero 3
nelle parentesi quadre sta ad indicare la posizione dell’elemento che vogliamo estrarre. Allo stesso modo possiamo selezionare un sottoinsieme qualsiasi di elementi usando la funzione c()
. Per estrarre da x
gli elementi con posizione 1, 4 e 5 possiamo eseguire il codice
> w <- x[c(1, 4, 5)]
> w
[1] 10 -1 5
Notate come in questo caso abbiamo estratto un sottoinsieme di elementi, il cui risultato è stato immediatamente memorizzato in un nuovo oggetto che abbiamo chiamato w
. In R è piuttosto comune, infatti, creare nuovi oggetti come risultato di una qualche operazione su oggetti già esistenti.
Possiamo indicare quali elementi estrarre anche attraverso la verifica di condizioni logiche. Ad esempio, il codice seguente consente di estrarre da x
solo gli elementi pari:
> x[(x %% 2) == 0]
[1] 10 -2
Notate che in questo esempio abbiamo usato due operatori nuovi, l’operatore %%
che restituisce il resto della divisione per il numero che si trova a destra dell’operatore (in questo caso, il resto della divisione di ognuno degli elementi di x
per 2), e l’operatore logico ==
, il quale invece verifica se il lato di sinistra è uguale a quello di destra e restituisce i valori logici TRUE
o FALSE
a seconda dei casi12. Ne approfittiamo anche per sottolineare che R applica automaticamente le varie operazioni a tutti gli elementi di un vettore.
E’ possibile assegnare dei nomi agli elementi di un vettore attraverso la funzione names()
. Ciò consente di riferirsi agli elementi del vettore attraverso i loro nomi invece che le loro posizioni:
> x
[1] 10.0 -2.0 0.3 -1.0 5.0
> names(x) <- c("A", "B", "C", "D", "E")
> x
A B C D E
10.0 -2.0 0.3 -1.0 5.0
> x[c("B", "E", "A")]
B E A
-2 5 10
La funzione names()
può essere usata sia per ottenere i nomi degli elementi di un vettore, ma anche, come abbiamo fatto sopra, per assegnare i nomi agli elementi stessi.
Concludiamo questa parte sui vettori aggiungendo che quando si lavora con data set veri è molto frequente che alcuni dei dati siano mancanti. Il codice che in R indica i dati mancanti è NA
, acronimo di not available (corrisponde a una cella vuota in Excel). Supponiamo ad esempio che in una ricerca di mercato abbiamo raccolto i seguenti dati sul reddito annuo (in migliaia di euro) per un campione di 10 individui:
> income <- c(40, 55, 60, NA, 34, 89, NA, NA, 121, 73)
Tre degli individui intervistati non hanno comunicato il proprio reddito e pertanto sono riportati come NA
. Per sapere quali dati sono mancanti possiamo usare la funzione is.na()
, che restituisce un vettore logico di lunghezza pari a quella del vettore a cui è applicata:
> is.na(income)
[1] FALSE FALSE FALSE TRUE FALSE FALSE TRUE TRUE
[9] FALSE FALSE
Per contare quanti dati sono mancanti possiamo usare la funzione sum()
, la quale in generale consente di calcolare la somma degli elementi di un vettore. In questo caso sum()
converte prima i valori logici TRUE
e FALSE
restituiti da is.na()
rispettivamente in 1 e 0, e quindi ne calcola la somma:
> sum(is.na(income))
[1] 3
1.3.3 Matrici
Altre strutture di dati utili in R sono le matrici, ovvero insiemi di elementi dello stesso tipo (numeri, testo, valori logici) disposti lungo due dimensioni, righe e colonne, invece che lungo una sola dimensione come per i vettori.
Per creare una matrice usiamo la funzione matrix()
, la quale è più articolata rispetto alle funzioni che abbiamo usato finora, perché richiede diversi argomenti (per una discussione più dettagliata sulle caratteristiche di un oggetto di tipo funzione e una descrizione del tipo di argomenti che è possibile passare ad una funzione, si veda la Sezione 1.3.713). Il primo argomento richiesto, denominato data
, corrisponde al vettore di elementi della matrice riportati indifferentemente per riga o per colonna. Il secondo e terzo argomento, denominati rispettivamente nrow
e ncol
, indicano il numero di righe e di colonne in cui gli elementi in data
vogliamo vengano disposti. Il quarto argomento, denominato byrow
, serve per indicare ad R se la matrice deve essere riempita per righe (TRUE
) o per colonne (FALSE
). Nell’esempio che segue creiamo una matrice \((3 \times 2)\) procedendo per righe:
> A <- matrix(data = c(4, 2, 0, 1, -3, 0.9), nrow = 3, ncol = 2, byrow = TRUE)
> A
[,1] [,2]
[1,] 4 2.0
[2,] 0 1.0
[3,] -3 0.9
> str(A)
num [1:3, 1:2] 4 0 -3 2 1 0.9
La funzione dim()
restituisce un vettore con le dimensioni della matrice.
Per estrarre un sottoinsieme di elementi da una matrice abbiamo a disposizione le stesse modalità che abbiamo descritto per i vettori, salvo che ora all’interno delle parentesi quadre sarà necessario indicare sia gli indici di riga sia quelli di colonna degli elementi da estrarre. Nel caso si intenda estrarre tutte le righe o tutte le colonne di una matrice, è sufficiente non indicare il rispettivo vettore di indici. Il seguente codice mostra due esempi usando la matrice A
definita sopra14:
> A[2, 1] # elemento in riga 2 e colonna 1
[1] 0
> A[c(1, 3), 2] # elementi in riga 1 o 3 e in colonna 2
[1] 2.0 0.9
> A[, 1] # tutti gli elementi della prima colonna
[1] 4 0 -3
Provate ora un piccolo esercizio (verificate poi la risposta eseguendo il codice): quali sono gli elementi della matrice A
selezionati dal seguente codice?
> A[A[, 2] >= 1, A[3, ] < 0]
E’ possibile attribuire dei nomi anche agli elementi di una matrice. Visto che ora ci sono due dimensioni, esistono due funzioni, chiamate rownames
e colnames
, per assegnare dei nomi rispettivamente alle righe o alle colonne della matrice:
> rownames(A) <- c("a", "b", "c")
> colnames(A) <- c("d", "e")
> A
d e
a 4 2.0
b 0 1.0
c -3 0.9
Dopo aver assegnato i nomi a righe e colonne, possiamo ad esempio selezionare la terza riga della matrice A
come segue
> A["c", ]
d e
-3.0 0.9
E’ possibile unire matrici e vettori affiancandone i rispettivi elementi lungo una delle due dimensioni (righe o colonne) attraverso le funzioni rbind()
(per affiancare le righe) e cbind()
(per affiancare le colonne):
> D <- matrix(c(1, 0, 0, 1), nrow = 2, ncol = 2, byrow = TRUE)
> b <- c(-3, 4, 0)
>
> rbind(A, D)
d e
a 4 2.0
b 0 1.0
c -3 0.9
1 0.0
0 1.0
> cbind(A, b)
d e b
a 4 2.0 -3
b 0 1.0 4
c -3 0.9 0
1.3.4 Vettori factor
Un vettore factor è un vettore che contiene solo valori predefiniti ed è utilizzato per rappresentare delle variabili qualitative/categoriche. I vettori factor sono definiti internamente da R a partire da numeri interi a cui vengono poi “appiccicate” delle etichette di testo.
Per creare un vettore factor si usa la funzione factor()
. Supponiamo per esempio che nell’ipotetico sondaggio che abbiamo descritto nella Sezione 1.3.2, oltre al reddito annuo abbiamo raccolto anche il genere degli intervistati, riportati rispettivamente come "m"
e "f"
:
> gender <- factor(c("f", "f", "f", "m", "m", "f", "m", "f", "f", "m"))
> gender
[1] f f f m m f m f f m
Levels: f m
Per conoscere i livelli di un vettore factor si usa la funzione levels()
:
> levels(gender)
[1] "f" "m"
I vettori factor si riveleranno di fondamentale importanza per l’analisi di dati categorici.
1.3.5 Data frame
I data frame sono le strutture di dati tipicamente utilizzate in R per contenere un data set. Non sono da confondere con le matrici, poiché quest’ultime richiedono che i dati in esse contenute siano tutti dello stesso tipo, mentre i data frame possono contenere un misto di colonne numeriche, categoriche o testuali.
Possiamo creare un data frame ex-novo attraverso la funzione data.frame()
, anche se il sistema più comune per crearne uno consiste nell’importare dei dati già salvati precedentemente in un file (si veda la Sezione 1.8). A titolo di esempio, il codice seguente crea un data frame a partire dai vettori income
e gender
creati precedentemente, i quali diventeranno rispettivamente la prima e la seconda colonna del data frame:
> dat <- data.frame(income, gender)
> str(dat)
'data.frame': 10 obs. of 2 variables:
$ income: num 40 55 60 NA 34 89 NA NA 121 73
$ gender: Factor w/ 2 levels "f","m": 1 1 1 2 2 1 2 1 1 2
Notate che la funzione str()
restituisce una sintesi delle informazioni contenute nelle colonne del data frame, indicando per ognuna la corrispondente tipologia di dati, ovvero num
e Factor
con 2 livelli, "f"
e "m"
.
Come per le matrici, la funzione dim()
restituisce le dimensioni di un data frame (numero di osservazioni e numero di variabili), così come funziona allo stesso modo anche la selezione di un sottoinsieme di dati e l’assegnazione di nomi a righe e colonne:
> dat[5, 1]
[1] 34
> dat[, 2]
[1] f f f m m f m f f m
Levels: f m
> dat[c(1, 3, 4, 7), ]
income gender
1 40 f
3 60 f
4 NA m
7 NA m
> rownames(dat)
[1] "1" "2" "3" "4" "5" "6" "7" "8" "9" "10"
> colnames(dat)
[1] "income" "gender"
In aggiunta, possiamo selezionare una colonna di un data frame utilizzando l’operatore $
(simbolo di dollaro)15. Ad esempio, per calcolare la media dei redditi annui per gli individui inclusi nel data set possiamo eseguire il codice seguente:
> mean(dat$income, na.rm = TRUE)
[1] 67.43
Facciamo notare che nel codice precedente abbiamo specificato due argomenti, il primo corrisponde alla variabile di cui vogliamo calcolare la media, mentre il secondo argomento (na.rm
, opzionale) indica che, prima di calcolare la media, vogliamo rimuovere gli eventuali dati mancanti16.
Nel caso in cui il data set fosse troppo grande e non fosse quindi agevole stamparne l’intero contenuto nella Console
, possiamo visualizzarlo con la funzione View()
.
Per combinare tra loro diversi data frame è consigliabile usare la funzione data.frame()
invece che cbind()
. I data frame che vengono combinati devono avere lo stesso numero di righe.
1.3.6 Liste
Le liste sono oggetti molto generali i cui elementi possono essere altri oggetti di qualsiasi tipo, incluse altre liste. Possiamo creare una lista con la funzione list()
:
> X <- list(y, txt, A)
> X
[[1]]
[1] "Viva la Statistica!"
[[2]]
[1] "R" "è" "un" "software"
[5] "per" "il" "data" "science"
[[3]]
d e
a 4 2.0
b 0 1.0
c -3 0.9
> str(X)
List of 3
$ : chr "Viva la Statistica!"
$ : chr [1:8] "R" "è" "un" "software" ...
$ : num [1:3, 1:2] 4 0 -3 2 1 0.9
..- attr(*, "dimnames")=List of 2
.. ..$ : chr [1:3] "a" "b" "c"
.. ..$ : chr [1:2] "d" "e"
Vi suggeriamo di pensare alle liste come a una generalizzazione dei vettori i cui elementi non sono vincolati ad essere dello stesso tipo.
Per recuperare il numero di elementi inclusi in una lista dobbiamo usare la funzione length()
, mentre per accedere ad uno dei suoi elementi è necessario usare l’operatore [[]]
(doppia parentesi quadra)17:
> X[[1]] # primo elemento della lista X
[1] "Viva la Statistica!"
> X[[3]] # terzo elemento della lista X
d e
a 4 2.0
b 0 1.0
c -3 0.9
> X[[3]][3, 2] # elemento in riga 3 e colonna 2 per il terzo elemento di X
[1] 0.9
Le liste sono molto importanti in R, perché sono spesso usate per memorizzare e organizzare i risultati di un’analisi, come ad esempio quelli restituiti dalla funzione lm()
, che sarà discussa nel Capitolo 5.
1.3.7 Funzioni
L’ultima tipologia di oggetti che consideriamo sono le funzioni. In quanto oggetti, anche le funzioni possono essere manipolate come tutti gli altri oggetti disponibili in R. Tuttavia, la manipolazione di funzioni rappresenta un argomento di programmazione avanzata che quindi non discuteremo in questa sede. Qui ci limiteremo a descrivere alcune caratteristiche delle funzioni che ci torneranno utili nel resto del manuale.
Ogni funzione in R è caratterizzata da tre elementi:
- il corpo della funzione, ovvero il codice contenuto nella stessa
- l’ambiente in cui la funzione viene valutata
- la lista di argomenti, ovvero gli input della funzione
Tralasciamo i dettagli sui primi due elementi, poiché non ci serviranno nel resto del corso, mentre diamo qualche informazione sulla lista di argomenti.
Come abbiamo già detto, il compito di ogni funzione è quello di prendere in carico un elenco di argomenti (input), di utilizzare tali input per svolgere una serie di calcoli/operazioni e quindi di restituire un output. Alcune funzioni restituiscono un ouput numerico, altre un output grafico, altre ancora non restituiscono nulla ma producono degli effetti secondari. Ad esempio, le funzioni rownames()
e colnames()
non producono alcun risultato visibile, ma modificano i nomi delle righe e delle colonne di una matrice o di un data frame. Come abbiamo già avuto modo di verificare concretamente in alcuni esempi, l’output di una funzione può essere memorizzato in un nuovo oggetto, in modo da poterlo recuperare in un secondo momento senza dover eseguire nuovamente i calcoli contenuti nella funzione.
Dei tre elementi sopra elencati, quello sicuramente più importante ai nostri fini è l’elenco di argomenti. Ad eccezione di pochi casi, come ad esempio la funzione str()
, praticamente tutte le funzioni richiedono uno o più argomenti. Nel caso sia necessario indicare più argomenti, questi devono essere separati da una virgola (vedi l’esempio sulla funzione matrix()
nella Sezione 1.3.3).
Possiamo recuperare la lista completa di argomenti di una funzione con args()
. Ad esempio, per la funzione var()
, che calcola la varianza campionaria di un insieme di dati numerici, otteniamo
> args(var)
function (x, y = NULL, na.rm = FALSE, use)
NULL
Questo esempio mostra che la funzione var()
include 4 argomenti, x
, y
, na.rm
e use
, su cui per ora non forniamo ulteriori dettagli. Quindi, ogni argomento ha un nome (ad esempio, il terzo argomento di var()
si chiama na.rm
) e gli argomenti di una funzione sono presentati in un certo ordine (per var()
, prima viene x
, poi y
, e così via). Quando vogliamo usare una funzione che richiede più argomenti, dobbiamo riuscire a far capire ad R i valori che devono essere usati per ognuno di essi. Per fare ciò abbiamo a disposizione le seguenti alternative:
- fornire gli argomenti indicando per ognuno il rispettivo nome; in questo caso non dobbiamo preoccuparci di elencare gli argomenti nell’ordine in cui vengono mostrati da
args()
, ma possiamo scriverli in qualsiasi ordine - fornire gli argomenti senza indicarne il nome, ma in questo caso R si aspetta che l’ordine sia quello riportato da
args()
- entrambe le modalità precedenti, fornendo alcuni argomenti con il loro nome e altri inserendoli con la loro posizione corretta; quest’ultima modalità è di solito quella più usata nella pratica
Vediamo ora alcuni esempi attraverso il calcolo della varianza dei redditi annui dei 10 intervistati nell’ipotetico sondaggio descritto sopra:
> var(x = income, na.rm = TRUE) # modalità 1. --> corretto
[1] 907.6
> var(na.rm = TRUE, x = income) # modalità 1. --> corretto
[1] 907.6
> var(income, NULL, TRUE) # modalità 2. --> corretto
[1] 907.6
> var(income, TRUE) # modalità 2. --> errore!
Error in var(income, TRUE): incompatible dimensions
> var(income, na.rm = TRUE) # modalità 3. --> corretto
[1] 907.6
> var(TRUE, x = income) # modalità 3. --> errore!
Error in var(TRUE, x = income): incompatible dimensions
Concludiamo questa introduzione sulle funzioni notando che non è obbligatorio fornire tutti gli argomenti di una funzione. Ad esempio, negli esempi precedenti con var()
non abbiamo mai indicato l’argomento use
. Ciò deriva dal fatto che quasi tutte le funzioni hanno alcuni argomenti obbligatori e altri che sono invece facoltativi. L’argomento x
della funzione var()
è ad esempio obbligatorio: se provaste ad eseguire il codice var(na.rm = TRUE)
otterreste infatti un errore. Qualora non indicassimo esplicitamente i valori degli argomenti facoltativi, R ultilizzerebbe per questi un valore di default, ovvero quello indicato da args()
. Per esempio, la var()
ha solo il primo argomento obbligatorio, mentre gli altri sono facoltativi, con na.rm
che assume valore di default uguale a FALSE
18. Gli argomenti facoltativi, quindi, vanno esplicitamente indicati solo quando vogliamo assegnarli un valore diverso da quello di default.
L’output restituito da una funzione può essere un oggetto di qualsiasi tipo (vettore, matrice, data frame, lista, o un’altra funzione), anche se alcune funzioni, in particolare quelle che producono output grafico, non restituiscono direttamente nessun oggetto.
In realtà R distingue anche tra numeri interi (
int
) e numeri reali (num
), ma questa distinzione non ha nessuna implicazione dal nostro punto di vista.↩L’operatore
==
non è da confondere con il segno di uguale (=
).↩Per il momento anticipiamo solo che gli argomenti di una funzione si dividono in obbligatori e opzionali. Gli argomenti opzionali, come ad esempio
byrow()
nell’esempio che segue, assumono un valore di default nel caso non vengano esplicitamente indicati. I valori di default per gli argomenti opzionali di una funzione possono essere recuperati dall’help della funzione stessa (vedi la Sezione 1.4).↩Il carattere
#
consente di inserire un commento, ovvero un testo usato per descrivere una parte di codice, ma che non sarà eseguito da R.↩Un modo per potersi riferire direttamente alla colonna
income
senza dover specificare il data frame che la contiene consiste nell’uso della funzioneattach()
, che appunto rende disponibili le colonne di un data frame come oggetti indipendenti durante la sessione di lavoro corrente (la funzionedetach()
consente di ripristinare la situazione di partenza). Tuttavia,attach()
va usata con estrema cautela perché è possibile che data frame diversi contengano variabili con lo stesso nome. Per questo motivo, non la useremo ulteriormente in questo manuale.↩Se non aggiungessimo l’argomento
na.rm
in presenza di dati mancanti otterremmoNA
come risultato. Questo è il modo previsto in R per renderci consapevoli del fatto che una variabile contiene dei dati mancanti.↩Se usassimo le parentesi quadre singole, invece di estrarre direttamente il contenuto dell’elemento della lista selezionato otterremmo ancora una lista ma con un unico elemento corrispondente all’elemento selezionato. Se questa spiegazione non vi convince, provate ad eseguire il codice
str(X[1])
.↩L’argomento
y
divar()
assume valore di defaultNULL
, che in R indica un oggetto vuoto, che non contiene nulla.↩