3 Oggetti
In questo capitolo e nel prossimo, useremo R per assemblare un mazzo di 52 carte da gioco.
Inizieremo costruendo un semplice oggetto di
Successivamente, cercheremo di lavorare con oggetti sempre più complessi fino ad arrivare a una “tabella” di dati. In breve, costruiremo l’equivalente di un foglio di Excel. Quando avremo finito, il nostro mazzo di carte somiglierà a questo:
figura seme valore13
re picche 12
regina picche 11
jack picche 10
dieci picche 9
nove picche 8
otto picche ...
E’ sempre necessario inserire i dati a mano in R?
Ma certo che no!
Nella maggiorparte dei casi, i dati vengono importati tramite delle semplici procedure, vedi Importazione. Questo esercizio ci servirà per capire come R salva i dati in memoria e come possiamo assemblare e disassemblare il nostro dataset.
Ora che sappiamo come usare la console di RStudio, proviamo a creare un oggetto che contenga i valori delle carte non vestite (da 2 a 10).
Chiameremo questo vettore valore
.
Se ben ricordate, nella sezione precedente abbiamo introdotto l’operatore :
. Questo operatore ritorna come risultato un vettore. Un vettore è un insieme unidimensionale di elementi (valori numerici, testo, ecc…).
Per creare il vettore valore
digitiamo:
2:10
## [1] 2 3 4 5 6 7 8 9 10
Ma ciò non è sufficiente poichè il vettore creato è stato salvato solo temporaneamente nella RAM e non è più possibile recuperarlo.
Per rendere valore
un oggetto di R, successivamente richiamabile, dobbiamo salvarlo nell’ambiente globale (Global Environment in RStudio) assegnando il risultato dell’operazione 2:10
a una variabile, tramite l’operatore di assegnazione <-
.
<- 2:10
valore valore
## [1] 2 3 4 5 6 7 8 9 10
Se l’oggetto valore
è stato creato con successo, lo potremo trovare nel pannello Global Environment
di RStudio come indicato nella figura seguente:
- pannello Global Environment: contiene tutti gli oggetti creati nell’attuale sessione di R. Quando la sessione viene terminata, l’ambiente viene ‘svuotato’. Un global environment può essere salvato cliccando sull’icona con il floppy nella barra superiore del pannello stesso. Il file viene salvato con estensione .Rdata e può essere caricato in un qualsiasi progetto di R cliccando su
File > Open File > Nome_del_file.Rdata
.
3.1 Vettori atomici
L’oggetto più semplice in R è chiamato vettore atomico ( atomic vector ). I vettori atomici non si incontrano spesso nella pratica, ma molti oggetti di R sono costruiti a partire da essi.
Un vettore atomico è semplicemente un vettore di dati. Ad esempio, l’oggetto valore
che abbiamo creato precedentemente è proprio un vettore atomico.
Si può creare un vettore atomico raggruppando degli scalari attraverso la funzione c()
:
is.vector(valore)
## [1] TRUE
is.vector()
La funzione is.vector()
testa se un oggetto è un vettore atomico. Ritorna TRUE
se l/’oggetto è un vettore atomico e FALSE
altrimenti.
La funzione c()
può essere usata anche per concatenare vettori:
c(valore, valore)
## [1] 2 3 4 5 6 7 8 9 10 2 3 4 5 6 7 8 9
## [18] 10
Si possono inoltre creare vettore atomici con solo un elemento:
<- c(5)
vettore vettore
## [1] 5
is.vector(vettore)
## [1] TRUE
length(vettore)
## [1] 1
length()
La funzione length()
ritorna la lunghezza di un vettore atomico.
3.1.1 Operazioni element-wise
Ora che la variabile valore
è stata creata possiamo eseguire delle operazioni su di essa.
Ecco alcuni esempi:
- 1 valore
## [1] 1 2 3 4 5 6 7 8 9
/ 2 valore
## [1] 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0
* valore valore
## [1] 4 9 16 25 36 49 64 81 100
Come si può notare dai risultati, quando si manipola un set di dati come può essere un vettore, R non segue le regole dell’algebra lineare, bensì, esegue le operazioni elemento per elemento (element-wise).
Esercizio 3
Cosa succede se si effettuano operazioni tra due vettori di lunghezza diversa?
Provate a eseguire i seguenti comandi:
1:7 * 1:6
1:12 * 1:6
1:6 * 1:12
1:7 * 1:6
## Warning in 1:7 * 1:6: longer object length is not a
## multiple of shorter object length
## [1] 1 4 9 16 25 36 7
1:12 * 1:6
## [1] 1 4 9 16 25 36 7 16 27 40 55 72
1:6 * 1:12
## [1] 1 4 9 16 25 36 7 16 27 40 55 72
Un vettore atomico può solo contenere elementi dello stesso tipo.
In R esistono 6 tipi di vettori atomici: double, integer, character, logical, complex, and raw (tralasceremo questi ultimi due).
Per creare il nostro mazzo di carte, avremo bisogno di usare diversi tipi di vettori atomici per salvare informazioni diverse (figura e seme: testo, valore: numeri).
Esistono delle convenzioni per comunicare ad R il tipo di vettore atomico che stiamo creando.
Ad esempio, se vogliamo creare un vettore di interi basterà mettere una L
maiuscola dopo ogni numero, per creare invece un vettore di stringhe il testo andrà racchiuso nei doppi apici ""
:
<- c(1L, 5L)
interi <- c("asso", "cuori") stringhe
Perchè R deve assegnare un tipo a un vettore atomico?
Ogni funzione implementata in R si può comportare diversamente (metodo) a seconda del tipo di dato in input. Ad esempio non si possono compiere certe operazioni matematiche sui vettori di stringhe.
sum(interi) #sommo tutti gli elementi del vettore "interi"
## [1] 6
sum(stringhe) #sommo tutti gli elementi del vettore "stringhe"
## Error in sum(stringhe): invalid 'type' (character) of argument
- double
Un vettore double contiene valori numerici. Tali valori possono essere positivi o negativi, grandi o piccoli, avere o meno dei decimali. Se non specificato diversamente, R tratta tutti i valori numerici come double.
Per conoscere il tipo di un oggetto salvato nell’ambiente globale si può usare la funzione typeof()
.
Esercizio 4
Determinare il tipo dei seguenti oggetti:
valore
stringhe
interi
typeof(valore)
## [1] "integer"
typeof(stringhe)
## [1] "character"
typeof(interi)
## [1] "integer"
il termine “double”
Il termine “double” si riferisce al numero di byte necessari per salvare il numero nella memoria.
Alcune funzioni di R fanno riferimento ai double come numerics. Anche in questo corso useremo i termini “double” e “numerici” in modo intercambiabile.
- integer
I vettori integer contengono valori numerici interi, vale a dire numeri che si possono scrivere senza valori decimali. Solitamente, questo tipo di vettore viene usato per immagazzinare indici od oggetti specifici che potrebbero essere considerati come double ma che, per questioni di performance e gestione della memoria, è preferibile specificare come integer.
Per creare un vettore di interi basta postporre al numero intero la lettera L
maiuscola.
<- c(-1L, 2L, 4L)
interi interi
## [1] -1 2 4
is.integer(interi)
## [1] TRUE
La differenza tra tipo double e tipo integer sta nel numero di byte utilizzati per salvare il numero in memoria. R salva gli oggetti double in uno spazio di 16 bytes, questo significa che i numeri che hanno decimali significativi anche dopo il sedicesimo decimale vengono arrotondati. La costante \(\pi\) è un esempio.
A volte, questo genere di arrotondamento crea delle incongruenze nei calcoli matematici. Gli errori di questo tipo vengono chiamati floating point error o numerical error. Eccone un esempio:
sqrt(2)^2 - 2
## [1] 4.441e-16
Il risultato corretto della precedente operazione sarebbe 0 ma R non è in grado di salvare l’oggetto sqrt(2)
con sufficienti cifre decimali. Perciò il calcolo non è eseguito precisamente.
- character
Un vettore di character contiene testi brevi. Si può creare un vettore di character, come già anticipato, racchiudendo il testo tra doppi apici.
<- c("Hello", "World")
testo testo
## [1] "Hello" "World"
is.character(testo)
## [1] TRUE
Gli elementi singoli di un vettore character sono chiamati stringhe.
Esercizio 5
Si noti che una stringa può contenere caratteri alfanumerici o simboli.
Determinare se i seguenti oggetti sono stringhe o numeri:
1
"1"
"uno"
is.double(1)
## [1] TRUE
is.character("1")
## [1] TRUE
is.character("uno")
## [1] TRUE
E/’ necessario scrivere le stringhe all’interno dei doppi apici, altrimenti, R andrebbe a cercare una variabile con nome uguale alla stringa digitata e molto probabilmente ritornerebbe un errore in quanto la variabile non esisterebbe.
- logical
I vettori logical contengono elementi TRUE
o FALSE
. I vettori logici sono molto utili per fare confronti o per filtrare un vettore:
3 > 4
## [1] FALSE
R assume che T
e F
siano le abbreviazioni di TRUE
e FALSE
, a meno che non vengano ridefinite da qualche altra parte (e.g. T <- 500
).
<- c(TRUE, FALSE, TRUE) # logici <- c(T,F,F)
logici logici
## [1] TRUE FALSE TRUE
is.logical(F)
## [1] TRUE
Esercizio 6
Creare un vettore atomico che rappresenti le figure delle carte che posso avere in una mano di poker (a 5 carte).
Che tipo di vettore bisogna usare per salvare la mano di carte?
Un vettore di caratteri è il vettore più appropriato per salvare le figure delle carte. Possiamo crearlo attraverso la funzione c
:
<- c("asso", "re", "regina", "jack", "dieci")
mano mano
## [1] "asso" "re" "regina" "jack" "dieci"
Ben fatto!
Più avanti proveremo a creare una struttura dati più sofisticata: una tabella a due dimensioni con le figure e i semi delle carte.
Per cominciare, si può costruire un oggetto più sofisticato a partire da un vettore atomico, assegnandogli una classe ( class ) e degli attributi ( attribute ).
3.2 Attributi
Un attributo ( attribute ) è un segmento di informazione che si può annettere a un vettore atomico (o a qualsiasi altro oggetto di R). L’attributo non modificherà i valori nell’oggetto e non apparirà quando l’oggetto verrà mostrato nell’output della console. Un attributo è un metadata, o più comunemente un’etichetta, ovvero un luogo adatto a contenere informazioni associate all’oggetto. Alcune funzioni possono usare gli attributi di un oggetto per eseguire specifiche azioni.
Si possono vedere gli attributi di un oggetto usando la funzione attributes()
. Tale funzione ritornerà NULL
se l’oggetto non possiede attributi. Un vettore atomico come valore
non ha nessun attributo a meno che non glielo si assegni:
attributes(valore)
## NULL
NULL
R usa NULL
per rappresentare l’insieme vuoto, un oggetto vuoto. Si può creare un oggetto NULL
assegnandogli il valore NULL
, scritto in maiuscolo.
<- NULL
insieme_vuoto insieme_vuoto
## NULL
Per assegnare un attributo personalizzato a un oggetto si può usare la funzione attr(oggetto, 'nome_attributo')
.
Ad esempio, assegnamo all’oggetto valore
il tipo di gioco a cui si applica:
attr(valore, 'gioco') <- "poker"
attr(valore, 'gioco')
## [1] "poker"
3.2.1 Nomi (names)
Gli attributi più comuni che si possono assegnare a un vettore atomico sono i nomi ( names ) e le dimensioni ( dim ).
Ad esempio, possiamo assegnare dei nomi al nostro valore
names(valore)<-c("due","tre","quattro","cinque","sei","sette","otto","nove","dieci")
names(valore)
## [1] "due" "tre" "quattro" "cinque" "sei"
## [6] "sette" "otto" "nove" "dieci"
attributes(valore)
## $gioco
## [1] "poker"
##
## $names
## [1] "due" "tre" "quattro" "cinque" "sei"
## [6] "sette" "otto" "nove" "dieci"
R mostrerà i nomi del vettore atomico quando lo si richiamerà nella console:
valore
## due tre quattro cinque sei sette
## 2 3 4 5 6 7
## otto nove dieci
## 8 9 10
## attr(,"gioco")
## [1] "poker"
3.2.2 Dimensioni (dim)
L’attributo dim trasforma il vettore atomico in un array n-dimensionale.
Per usare questa funzionalità dobbiamo assegnare all’attributo dim dell’oggetto che vogliamo trasformare (input) un vettore atomico di lunghezza n contenente i valori numerici che identificano le dimensioni dell’oggetto trasformato (output).
R riorganizzerà gli elementi per colonna.
Vediamo in pratica come funziona.
Restrutturiamo il nostro valore
in un array di n=2 dimensioni attraverso un vettore di dimensioni c(3,3)
(i.e. in una matrice 3 per 3):
<- valore #non perdiamo l'oggetto originale
valore.arr2d dim(valore.arr2d) <- c(3, 3) #equivalentemente avremmo potuto usare array(valore, dim=c(3,3))
valore.arr2d
## [,1] [,2] [,3]
## [1,] 2 5 8
## [2,] 3 6 9
## [3,] 4 7 10
## attr(,"gioco")
## [1] "poker"
Abbiamo ottenuto così una matrice!
Un altro modo per creare una matrice a partire da un vettore è quello di usare la funzione matrix(oggetto, nrow = r, ncol = c)
dove oggetto
è un vettore atomico e r
e c
sono valori interi. L’equivalente della precedente operazione è:
<- matrix(valore,
valore.mat nrow = 3
) valore.mat
## [,1] [,2] [,3]
## [1,] 2 5 8
## [2,] 3 6 9
## [3,] 4 7 10
== valore.mat valore.arr2d
## [,1] [,2] [,3]
## [1,] TRUE TRUE TRUE
## [2,] TRUE TRUE TRUE
## [3,] TRUE TRUE TRUE
La funzione matrix()
riempie la matrice colonna per colonna. Per eseguire l’operazione inversa (riga per riga) bisogna impostare l’argomento byrow = TRUE
:
<- matrix(valore,
valore.mat2 nrow = 3,
byrow=TRUE
) valore.mat2
## [,1] [,2] [,3]
## [1,] 2 3 4
## [2,] 5 6 7
## [3,] 8 9 10
Che cos’è byrow=TRUE?
La funzione matrix()
fa parte del pacchetto base di R. Come tutte le funzioni approvate dal CRAN, anche matrix()
ha una sua documentazione in cui si possono trovare tutte le informazioni sui parametri modificabili (argomenti). La documentazione di una funzione è accessibile attraverso il comando ?
seguito dal nome della funzione di cui si stanno cercando informazioni. Nelle lezioni successive faremo un po’ di pratica nella navigazione della documentazione.
Esercizio 7
Finalmente possiamo realizzare la nostra mano di poker inserendo anche il seme delle carte.
Creare la seguente matrice usando uno dei metodi precedentemente elencati
## [,1] [,2]
## [1,] "asso" "picche"
## [2,] "re" "picche"
## [3,] "regina" "picche"
## [4,] "jack" "picche"
## [5,] "dieci" "picche"
<- c("asso", "re", "regina", "jack", "dieci")
figura <- c("picche", "picche", "picche", "picche", "picche" )
seme <- array(c(figura, seme), dim = c(5, 2))
figseme.mat figseme.mat
## [,1] [,2]
## [1,] "asso" "picche"
## [2,] "re" "picche"
## [3,] "regina" "picche"
## [4,] "jack" "picche"
## [5,] "dieci" "picche"
3.2.3 Fattori
Attraverso i fattori ( factor ) R salva in memoria i dati di tipo categorico, come l’etnia o il colore degli occhi. I fattori, a differenza delle variabili character, hanno la particolarità di essere salvati con un certo ordine e servono a risparmiare memoria.
Ad esempio, per il genere, F < M.
L’ordinamento rende più agevole la gestione di variabili qualitative ordinabili, come il livello di trattamento nelle analisi biostatistiche (0 = non trattato, 1 = trattato).
Per convertire un vettore character in un vectore factor si utilizza la funzione, appunto, factor()
. R ricodificherà la variabile in un vettore di interi, uno per ogni modalità del fattore. Tali modalità vengono chiamate livelli ( levels ) e non sono altro che attributi del vettore appena creato. I livelli contengono le etichette testuali di ogni modalità.
<- factor(c("M", "F", "F", "M"))
genere typeof(genere)
## [1] "integer"
attributes(genere)
## $levels
## [1] "F" "M"
##
## $class
## [1] "factor"
class
Le classi ( class ) sono degli attributi che non affronteremo in questo corso. In breve, servono a dare una categoria all’oggetto a cui vengono assegnate. Ad esempio, il vettore genere
appartiene alla classe factor
.
Si può riconvertire un vettore factor in un vettore character attraverso la funzione as.character()
.
as.character(genere)
## [1] "M" "F" "F" "M"
Ora che abbiamo capito quali sono le funzionalità e i casi d’uso dei vettori atomici, possiamo creare una carta da gioco più complicata.
Esercizio 8
Ogni gioco di carte assegna un valore diverso a ogni carta. Per esempio, nel blackjack, le figure valgono 10 punti, le carte non vestite valgono dai 2 ai 10 punti, e gli assi valgono 1 o 11 punti, a seconda del punteggio finale.
Crea una carta virtuale che combini “asso”, “cuori” e il valore 1 in un vettore. Che tipo di vettore atomico risulterà?
Non è possibile combinare tali informazioni in un vettore atomico poichè, come abbiamo già detto, esso può contenere elementi di un solo tipo. “asso” è una stringa mentre 1 è un valore numerico.
<- c("asso", "cuori", 1)
carta carta
## [1] "asso" "cuori" "1"
R adotterà la tecnica di coercion e renderà 1 un “1”.
3.3 Coercion e Recycling
Se si prova a inserire elementi di tipo diverso in un vettore, R effettua quella che si chiama coercion degli elementi non conformi, seguendo una lista di priorità. In cima alla lista c’è il tipo ‘carattere’, quindi, se c’è almeno un elemento di tipo carattere in un vettore, gli altri elementi vengono convertiti in carattere a loro volta.
R usa le stesse regole di coercion anche se il tipo di variabile usata in input in una funzione non è compatibile con la funzione stessa. Ad esempio, se si prova a sommare un vettore di elementi logici, il vettore verrà prima convertito in interi 0 e 1 e successivamente viene eseguita la somma degli elementi convertiti.
sum(c(TRUE, TRUE, FALSE, FALSE))
## [1] 2
diventerà:
sum(c(1, 1, 0, 0))
## [1] 2
Una specie di coercion che si applica quando due oggetti hanno dimensioni diverse è il recycling. In particolare, prima di eseguire un’operazione tra set di dati, R confronta le loro dimensioni ed esegue quella che si chiama recycling, o broadcasting, della funzione tra gli oggetti.
Se le dimensioni dei set sono compatibili (ad esempio, una è un multiplo dell’altra), R adatta la dimensione di un oggetto a quella dell’altro ed esegue l’operazione che ritiene più logica (ad esempio ripete il vettore più corto fino a raggiungere la lunghezza del vettore più lungo ed esegue la funzione sui vettori ridimensionati).
La tecnica di recycling torna molto utile quando eseguiamo operazioni tra oggetti di dimensioni diverse come un vettore e uno scalare. Ovvero, molte funzioni vengono vettorizzate. Ecco perchè il seguente codice non produrrà errori:
sample(10) + 100
## [1] 101 108 105 109 102 103 110 107 106 104
runif(10) > 0.5
## [1] TRUE FALSE FALSE TRUE FALSE TRUE TRUE FALSE
## [9] FALSE TRUE
3.4 Conversione
Per evitare di incorrere in “coercizioni” errate, si può preventivamente cambiare il tipo di un oggetto attraverso la funzione as.type()
dove ‘type’ corrisponde al tipo desiderato.
Esempi:
as.character(1)
## [1] "1"
as.logical(1)
## [1] TRUE
as.numeric(FALSE)
## [1] 0
Molti dataset contengono diversi tipi di informazione. L’impossibilità di inserire informazioni di tipo diverso in un vettore, in una matrice o in un array n-dimensionale sembra una limitazione importante. Quindi perché dobbiamo usarli?
In molti casi usare un solo tipo di dati è un grande vantaggio. Vettori, matrici e array rendono molto semplici e veloci le operazioni matematiche tra insiemi di grandi dimensioni.
In altri casi, invece, può essere uno svantaggio. In queste situazioni ricorreremo alle liste.
3.5 Liste
Le liste ( list ) sono come dei vettori atomici perché raggruppano i dati in un insieme. Tuttavia, le liste non raggruppano insieme oggetti singoli ma oggetti di R come vettori atomici o altre liste. Per sempio, si può creare una lista che contenga un vettore numerico di lunghezza 31 come primo elemento, un vettore di caratteri di lunghezza 2 come secondo elemento e così via.
Per creare una lista dobbiamo usare la funzione list()
.
list()
crea una lista nello stesso modo in cui c()
crea un vettore atomico:
<- list(100:130, "R", list(TRUE, FALSE))
list1 list1
## [[1]]
## [1] 100 101 102 103 104 105 106 107 108 109 110 111
## [13] 112 113 114 115 116 117 118 119 120 121 122 123
## [25] 124 125 126 127 128 129 130
##
## [[2]]
## [1] "R"
##
## [[3]]
## [[3]][[1]]
## [1] TRUE
##
## [[3]][[2]]
## [1] FALSE
Questa volta R usa le doppie parentesi quadre per indicare la posizione dell’elemento nella lista. Ad esempio, il primo elemento [[1]]
della lista è il vettore 100:130
, il secondo [[2]]
è la stringa "R"
.
Possiamo salvare la nostra carta da gioco con una semplice lista:
Esercizio 9
Usare una lista per salvare una singola carta da gioco. La lista dovrà contenere la figura, il seme e il valore della carta.
<- list("asso", "cuori", 1)
carta carta
## [[1]]
## [1] "asso"
##
## [[2]]
## [1] "cuori"
##
## [[3]]
## [1] 1
Quindi, ora potremmo usare una lista per creare un intero mazzo di carte. Questa lista sarebbe composta da 52 sottoliste come quella appena creata.
Ovviamente in R c’è un modo ancora più semplice per creare un oggetto come il nostro mazzo di carte: usare un data frame.