Oggetti

Nella scorsa lezione abbiamo visto un po’ della logica della cosiddetta programmazione a oggetti (object-oriented programming). Ricordiamo che gli oggetti possono essere considerati dei contenitori di informazioni, delle scatole all’interno delle quali possiamo mettere diversi tipi di informazioni. Possono contenere valori di diverso tipo (numeri, caratteri alfanumerici) ma anche gruppi di valori strutturati in modi diversi (come un dataset), e addirittura sequenze di istruzioni per fare operazioni con altri oggetti (come funzioni e algoritmi).

Lavorare con R in larga parte consiste nel fare interagire diversi oggetti tra loro. È quindi importante capirne la logica e vedere alcuni esempi di cosa si può fare con gli oggetti.

L’ultima volta abbiamo visto oggetti che contengono singoli valori numerici

x <- 3
y <- 2

e che possono essere fatti interagire in modi diversi

x + y
## [1] 5

Ovviamente gli oggetti possono contenere anche il risultato di operazioni tra altri oggetti

z <- x * y
z
## [1] 6

Da qui in poi la storia si fa più complessa, ma la logica rimane la stessa.

Vettori e matrici

Gli oggetti possono contenere vettori. In informatica, i vettori sono serie di valori organizzati su una dimensione:

v <- c(1, 2, 10, 30, 10000)
v
## [1]     1     2    10    30 10000

Gli oggetti possono contenere anche matrici. Queste sono serie di valori organizzate su due dimensioni:

m <- matrix(c(1, 2, 10, 30), ncol = 2, nrow = 2)
m
##      [,1] [,2]
## [1,]    1   10
## [2,]    2   30

Una cosa importante: i vettori e le matrici non possono contenere valori di diverso tipo (come numeri e caratteri) allo stesso tempo. Se cercate di assegnare un valore di tipo diverso a un oggetto, i valori verranno convertiti per essere tutti dello stesso tipo. Dato che i caratteri alfanumerici sono più generici dei numeri (i caratteri sono semplici etichette, non hanno un valore quantitativo), quando aggiungete un elemento alfanumerico a un vettore o una matrice, R convertirà anche gli altri valori in caratteri:

v <- c(v, "f")
v  
## [1] "1"     "2"     "10"    "30"    "10000" "f"

Si può vedere che ora tutti i valori sono caratteri dalle virgolette.

Selezionare elementi di vettori e matrici

Per selezionare un elemento da dentro un oggetto, occorre usare le parentesi quadre e specificare la posizione (o valore di indice) dell’elemento desiderato. Per esempio, per vedere il secondo elemento nel vettore v:

v[2]
## [1] "2"

Seguendo la stessa logica si possono selezionare più elementi diversi:

v[c(1, 3, 5)]
## [1] "1"     "10"    "10000"

Per oggetti multidimensionali, come le matrici, occorre specificare la posizione dell’elemento in tutte le dimensioni. Nel caso delle matrici, che hanno 2 dimensioni, il primo valore di indice si riferisce alla posizione di riga, e il secondo alla posizione di colonna:

m[1, 2]
## [1] 10

Si possono anche selezionare intere righe e colonne

m[1, ]  # Prima riga
## [1]  1 10
m[, 1]  # Prima colonna
## [1] 1 2

Liste

Le liste sono “vettori di oggetti” che possono contenere di tutto (altri vettori, matrici, singoli valori, ecc.). Il grande vantaggio delle liste sta nel fatto che possono contenere valori di diverso tipo (non solamente numerici o caratteri). Sono un formato molto conveniente per archiviare dati complessi (organizzati in più di 2 dimensioni). Tuttavia sono oggetti dalla struttura più “astratta” e difficile da capire intuitivamente di quelli che abbiamo visto fino a questo momento

lst <- list(
  a = c(1, 3, 5),                                # Un vettore numerico
  b = matrix(c(1, 2, 3, 4), nrow = 2, ncol = 2), # Una matrice numerica
  c = "c"                                        # Un valore alfanumerico
)
lst
## $a
## [1] 1 3 5
## 
## $b
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
## 
## $c
## [1] "c"

Come con vettori e matrici, gli elementi di una lista possono essere richiamati utilizzando il numero di indice

lst[[1]] # Note the double brackets
## [1] 1 3 5

o chiamandoli con il loro nome (quando ne hanno uno)

lst$b
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4

In questo caso si utilizza il simbolo del dollaro $.

Data frame

Un tipo di oggetti molto importanti è il “data frame”. I data frame sono qualcosa a metà tra le matrici e le liste: sono bi-dimensionali, come le matrici, ma i valori in colonne diverse possono contenere valori di diverso tipo (numerici, caratteri), come nelle liste. Quando lavoriamo con un data frame assumiamo che le righe sono osservazioni e le colonne variabili. Come nelle liste, le colonne di un data frame hanno un nome: questi sono i nomi delle variabili nel dataset.

dat <- data.frame(
  var1 = c(1, 2, 3, 10), 
  var2 = c("a", "b", "c", "d")
)
dat
##   var1 var2
## 1    1    a
## 2    2    b
## 3    3    c
## 4   10    d

Si possono vedere i nomi delle variabili contenute in un data frame usando la funzione names().

names(dat)
## [1] "var1" "var2"

Le variabili in un data frame possono essere selezionate utilizzando le parentesi quadre e il numero di indice, come con una matrice:

dat[, 1]  # Seleziona la prima colonna, AKA la prima variabile
## [1]  1  2  3 10

Oppure si possono utilizzare i nomi delle variabili (più utile), come nelle liste, utilizzando il simbolo del dollaro $.

dat$var1
## [1]  1  2  3 10

Le parentesi quadre sono più utili quando occorre selezionare osservazioni

dat[1, ]             # Mostra il primo caso
##   var1 var2
## 1    1    a
dat[dat$var1 > 2, ]  # Mostra tutti i casi per i quali "var1" è maggiore di 2
##   var1 var2
## 3    3    c
## 4   10    d

Tipi di valori e conversione

R può gestire dati e valori di diverso tipo, come numeri e caratteri. Possiamo chiedere direttamente a R quale tipo di valori contiene una variabile utilizzando la funzione typeof(), o possiamo chiedere in modo più specifico utilizzando le funzioni di classe is.

# Un vettore di NUMERI INTERI
int <- 1:10
# Un vettore numerico con decimali
num <- seq(0, 1, by = 0.2)
# Un vettore di carattere
let <- letters[1:10]

# Chiedere il tipo di valore
typeof(int)
## [1] "integer"
typeof(num)
## [1] "double"
typeof(let)
## [1] "character"
# Chiedere per tipo specifico
is.numeric(int)   # È numerico?
## [1] TRUE
is.character(let) # È in carattere?
## [1] TRUE

Convertire i valori da un tipo all’altro è molto semplice, utilizzando le funzioni di classe as.. Alcuni esempi:

# Convertire numeri in caratteri
as.character(num)
## [1] "0"   "0.2" "0.4" "0.6" "0.8" "1"
typeof(as.character(num))
## [1] "character"
# Ri-convertire caratteri in numeri
as.numeric(as.character(num))
## [1] 0.0 0.2 0.4 0.6 0.8 1.0
# La conversione da carattere a numero funziona solo quando i caratteri rappresentano numeri (ed è già tanto)
as.numeric(let)
## Warning: NAs introduced by coercion
##  [1] NA NA NA NA NA NA NA NA NA NA

Un altro tipo di valore che sta più o meno a metà tra numeri e caratteri sono le cosiddette variabili “factor”, ovvero valori categorici. R utilizza molto questo formato, al punto che a volte pensiamo che alcuni valori sono numerici ma in realtà i numeri sono semplici etichette.

as.factor(num)        # Sembrano numeri...
## [1] 0   0.2 0.4 0.6 0.8 1  
## Levels: 0 0.2 0.4 0.6 0.8 1
mean(as.factor(num))  # ...ma non lo sono
## Warning in mean.default(as.factor(num)): argument is not numeric or logical:
## returning NA
## [1] NA

Una variabile factor contiene informazioni riguardo a tutti i possibili valori unici che la variabile può avere. Questi sono chiamati “livelli”:

levels(as.factor(num))
## [1] "0"   "0.2" "0.4" "0.6" "0.8" "1"

I livelli sono come le “etichette” in SPSS o Stata: i valori di una variabile factor sono numeri (anche se non hanno valore quantitativo ma, appunto, sono solo etichette), e i livelli rappresentano il contenuto sostantivo di tali valori. Questa proprietà delle variabili factor ci permette di convertire un carattere in numero utilizzando il factor come passo intermedio:

data.frame(
  letters = let,
  factors = as.factor(let),
  numbers = as.numeric(as.factor(let))
)
##    letters factors numbers
## 1        a       a       1
## 2        b       b       2
## 3        c       c       3
## 4        d       d       4
## 5        e       e       5
## 6        f       f       6
## 7        g       g       7
## 8        h       h       8
## 9        i       i       9
## 10       j       j      10

Funzioni

Le funzioni sono una parte molto importante di R. Qualsiasi cosa che R fa, è fatta tramite una funzione. In generale, le funzioni sono sequenze di istruzioni che, dato uno o più argomenti di input, restituiscono un output (che può essere messo in un oggetto). La struttura tipica di una funzione in R è function(argument 1, argument 2, ...). Naturalmente, anche le funzioni stesse possono essere immagazzinate in oggetti.

# Esempio: questa funzione somma +1 al numero in input
plusone <- function(x) {x + 1}
plusone(5)
## [1] 6

Una caratteristica importante delle funzioni in R è che possono essere vettorizzate: nonostante la funzione richieda un solo argomento, può essere applicata a una qualsiasi collezione di numeric, come un vettore una matrice. In tal caso R ripeterà la stessa operazione per ogni elemento del vettore (o della matrice) e restituirà un vettore (o matrice) dove ogni elemento è passato attraverso la funzione.

plusone(c(1, 3, 5, 99))
## [1]   2   4   6 100

La maggior parte delle funzioni in R è più complessa della nostra funzione plusone(), e gli output possono essere di tipo diverso. La logica comunque è sempre la stessa: (1) la funzione richiede un input, (2) elabora l’input e, (3) restituisce un output.

Alcune funzioni utili

  • c(): la funzione che combina diversi valori in un vettore. L’abbiamo usata prima, e in generale è una delle funzioni che si useranno più spesso
c(1, 5, 6, 8, 100)
## [1]   1   5   6   8 100
  • seq(): crea un vettore con una sequenza di numeri
seq(from = 1, to = 5, by = 0.5)         # Numeri da 1 a 5, con incremento di 0.5
## [1] 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0
seq(from = 1, to = 2, length.out = 10)  # Sequenza di 10 da 1 a 2
##  [1] 1.000000 1.111111 1.222222 1.333333 1.444444 1.555556 1.666667 1.777778
##  [9] 1.888889 2.000000

Un modo rapido per ottenere una sequenza di numeri interi con incremento 1 è usare i due punti :

1:10  # Numeri da 1 a 10 con incremento di 1
##  [1]  1  2  3  4  5  6  7  8  9 10
  • rep(): crea un vettore dove un valore o un insieme di valori vengono ripetuti n volte
rep(1:3, 5)               # Ripete la sequenza da 1 a 3 per 5 volte
##  [1] 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
rep(1:3, each = 5)        # Ripete ogni numero da 1 a 3 per 5 volte
##  [1] 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3
rep(c("a", "b", "c"), 5)  # Funziona anche con caratteri
##  [1] "a" "b" "c" "a" "b" "c" "a" "b" "c" "a" "b" "c" "a" "b" "c"
  • sample(): estrae uno o più valori casuali da un vettore
sample(c(1:199), size = 2)  # Estrae 2 numeri casuali dalla sequenza 1:199 
## [1] 50 26

Notare che i numeri estratti possono essere rimessi nell’insieme (e quindi possibilmente venire estratti di nuovo) aggiungendo l’opzione replace = TRUE:

sample(c(1:5), size = 6, replace = TRUE)  
## [1] 3 3 4 2 1 2
  • round(): arrotonda numeri con decimali selezionando il numero di cifre dopo la virgola
# Pi greco (una costante che è già presente nella memoria di R)
pi
## [1] 3.141593
# Arrotonda a due cifre decimali
round(pi, 2)
## [1] 3.14
  • rbind() e cbind(): creano una matrice combinando diversi vettori, per riga e per colonna rispetivamente
rbind(
  c(0, 2, 4, 6, 8),
  c(1, 3, 5, 7, 9)
)
##      [,1] [,2] [,3] [,4] [,5]
## [1,]    0    2    4    6    8
## [2,]    1    3    5    7    9
cbind(
  c(0, 2, 4, 6, 8),
  c(1, 3, 5, 7, 9)
)
##      [,1] [,2]
## [1,]    0    1
## [2,]    2    3
## [3,]    4    5
## [4,]    6    7
## [5,]    8    9
  • sort(): ordina i valori all’interno di un vettore
unord <- c(9, 3, 100, 57, 28, 74, 0, 2359)
# Ordine crescente
sort(unord)
## [1]    0    3    9   28   57   74  100 2359
# Ordine decrescente
sort(unord, decreasing = T)
## [1] 2359  100   74   57   28    9    3    0
  • order(): funzione più generica di sort(), restituisce l’ordine con il quale i numeri di indice (o posizioni) degli elemenbti nel vettore devono essere selezionati per essere messi in ordine
order(unord)
## [1] 7 2 1 5 4 6 3 8
# Per ottenere lo stesso risultato di "sort()", occorre chiamare "order()" all'interno delle parentesi quadre
unord[order(unord)]
## [1]    0    3    9   28   57   74  100 2359
unord[order(unord, decreasing = T)]
## [1] 2359  100   74   57   28    9    3    0
  • unique(): restituisce i valori unici in un vettore (ovvero, elimina i valori duplicati)
dupl <- c(1, 2, 2, 4, 3, 1, 6, 7, 6)
unique(dupl)
## [1] 1 2 4 3 6 7
  • length(): restituisce la lunghezza di un vettore, ovvero quanti elementi contiene
vec <- c(1, 5, 13, 9)
length(vec)
## [1] 4
  • nrow(), ncol() e dim(): queste funzioni riportano il numero di righe e colonne di una matrice
mat <- matrix(1:12, nrow = 4, ncol = 3)
nrow(mat) # Numero di righe
## [1] 4
ncol(mat) # Numero di colonne
## [1] 3
dim(mat)  # Numero di righe e colonne
## [1] 4 3
# "dim()" è più generica, ad esempio si può vedere anche solo il numero di righe
dim(mat)[1]
## [1] 4
# o di colonne
dim(mat)[2]
## [1] 3
  • rm(): elimina un oggetto dalla memoria
rm(mat)
  • unlist(): converte una lista in un vettore (portando tutti i valori allo stesso tipo)
lst
## $a
## [1] 1 3 5
## 
## $b
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
## 
## $c
## [1] "c"
unlist(lst)
##  a1  a2  a3  b1  b2  b3  b4   c 
## "1" "3" "5" "1" "2" "3" "4" "c"

Esercizio

  • Create 3 vettori a, b and c, ognuno contenente un vettore di 3 numeri estratti casualmente dalla sequenza 1:500
  • Combinate i 3 vettori in una matrice 3x3 nella quale ogni colonna sarà uno dei 3 vettori.
  • Riportate il contenuto della matrice.

Funzioni statistiche di base

Naturalmente R ha parecchie funzioni che permettono di fare operazioni statistiche nei valori all’interno di vettori e matrici

# Generiamo una sequenza di numeri
numbers <- seq(0, 10, by = 0.2)
numbers
##  [1]  0.0  0.2  0.4  0.6  0.8  1.0  1.2  1.4  1.6  1.8  2.0  2.2  2.4  2.6  2.8
## [16]  3.0  3.2  3.4  3.6  3.8  4.0  4.2  4.4  4.6  4.8  5.0  5.2  5.4  5.6  5.8
## [31]  6.0  6.2  6.4  6.6  6.8  7.0  7.2  7.4  7.6  7.8  8.0  8.2  8.4  8.6  8.8
## [46]  9.0  9.2  9.4  9.6  9.8 10.0
# Somma di tutti gli elementi nel vettore "numbers"
sum(numbers)
## [1] 255
# Media aritmetica
mean(numbers)
## [1] 5
# Mediana
median(numbers)
## [1] 5
# Varianza
var(numbers)
## [1] 8.84
# Deviazione standard
sd(numbers)
## [1] 2.973214
# Valore minimo
min(numbers)
## [1] 0
# Valore massimo
max(numbers)
## [1] 10
# Campo di variazione (valori minimo e massimo)
range(numbers)
## [1]  0 10
# Quantili
quantile(numbers, probs = c(0.05, 0.5, 0.95))
##  5% 50% 95% 
## 0.5 5.0 9.5

Nota: tutte queste funzioni non amano i valori mancanti (i cosiddetti missing values). In R i valori mancanti sono rappresentati come NA. Basta un NA in un vettore o in una matrice e R si rifiuterà di calcolare il risultato:

numbers[1] <- NA
numbers
##  [1]   NA  0.2  0.4  0.6  0.8  1.0  1.2  1.4  1.6  1.8  2.0  2.2  2.4  2.6  2.8
## [16]  3.0  3.2  3.4  3.6  3.8  4.0  4.2  4.4  4.6  4.8  5.0  5.2  5.4  5.6  5.8
## [31]  6.0  6.2  6.4  6.6  6.8  7.0  7.2  7.4  7.6  7.8  8.0  8.2  8.4  8.6  8.8
## [46]  9.0  9.2  9.4  9.6  9.8 10.0
# Media
mean(numbers)
## [1] NA

Come soluzione, tutte le funzioni che abbiamo visto includono l’opzione na.rm = TRUE, che dice a R di calcolare il parametro richiesto solo sulle osservazioni effettivamente presenti nel vettore o nella matrice (quindi escludendo i valori mancanti).

mean(numbers, na.rm = T)
## [1] 5.1
var(numbers, na.rm = T)
## [1] 8.5
  • summary() è una funzione generica utilizzata per ispezionare il contenuto di un oggetto. Può essere utilizzata con diversi tipi di oggetti, con risultati differenti.
# Vettori
numbers <- seq(0, 10, by = 0.1)
summary(numbers)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##     0.0     2.5     5.0     5.0     7.5    10.0
# Data frame
dat <- data.frame(
  var1 = c(1, 2, 3, 10), 
  var2 = c("a", "b", "c", "d")
)
summary(dat)
##       var1           var2          
##  Min.   : 1.00   Length:4          
##  1st Qu.: 1.75   Class :character  
##  Median : 2.50   Mode  :character  
##  Mean   : 4.00                     
##  3rd Qu.: 4.75                     
##  Max.   :10.00
# Liste
lst <- list(
  a = c(1, 3, 5),
  b = "c"
)
summary(lst)
##   Length Class  Mode     
## a 3      -none- numeric  
## b 1      -none- character
  • table(): produce una tabella di frequenze (“tabella di contingenza”, “cross-table”)
dat <- data.frame(
  v1 = rep(0:3, 5),
  v2 = rep(0:1, each = 10)
)
# Variabile singola
table(dat$v1)
## 
## 0 1 2 3 
## 5 5 5 5
# Due variabili
table(dat$v1, dat$v2)
##    
##     0 1
##   0 3 2
##   1 3 2
##   2 2 3
##   3 2 3
  • prop.table(): prende una tabella di frequenze come input (ovvero l’output della funzione table(), che può essere messo in un oggetto) e restituisce una tabella con le frequenze relative invece di quelle assolute. Può calcolare le frequenze relative per riga, per colonna, o per riga e colonna.
tabs <- table(dat$v1, dat$v2)
# Quota per riga
prop.table(tabs, margin = 1)
##    
##       0   1
##   0 0.6 0.4
##   1 0.6 0.4
##   2 0.4 0.6
##   3 0.4 0.6
# Quota per colonna
prop.table(tabs, margin = 2)
##    
##       0   1
##   0 0.3 0.2
##   1 0.3 0.2
##   2 0.2 0.3
##   3 0.2 0.3
# Quota globale
prop.table(tabs)
##    
##        0    1
##   0 0.15 0.10
##   1 0.15 0.10
##   2 0.10 0.15
##   3 0.10 0.15

Cercare aiuto con le funzioni

Mettiamo il caso che abbiate bisogno di utilizzare una funzione, come per esempio mean(), ma non sapete come usarla. In questo caso potete utilizzare la funzione help(), semplificata con il singolo punto di domanda ?, che aprirà una pagina con una descrizione più o meno dettagliata di quello che la funzione fa e quali argomenti necessita.

help(mean)  # Versione lunga
# oppure
?mean  # Versione breve

Strutture di controllo

La maggior parte delle operazioni in R vengono eseguite utilizzando funzioni. Tuttavia, programmare (con R, così come con qualsiasi altro software) implica seguire un flusso logico che risponda ai dati (o a qualsiasi input dato al software). Le strutture di controllo sono un insieme di istruzioni che possono essere usate per istruire R su come comportarsi in alcuni punti del vostro codice. Potete vederle come procedure fisse che permettono al programma di fare operazioni più complesse di quelle di base che abbiamo visto finora (e, in un certo senso, di comportarsi indipendentemente dal vostro costante input di istruzioni).

I seguenti comandi sono utili strumenti di programmazione che userete per lo più in alcune operazioni di data management. Vi renderanno la vita più facile e vi faranno risparmiare tempo (e digitazione) quando i compiti sono complessi o ripetitivi.

Strutture condizionali

Un aspetto molto importante della programmazione riguarda il dire al programma di eseguire alcune operazioni date certe condizioni. Le funzioni statistiche che abbiamo appena visto sono fatte per essere applicate a tutti i dati contenuti nell’input (sia esso un vettore, una matrice, una variabile in un data frame, e così via). Tuttavia, spesso abbiamo bisogno di fare cose diverse per diversi gruppi di osservazioni. Per esempio, con un campione di persone intervistate in un sondaggio, potremmo voler creare una variabile che da valore 1 a tutte le femmine e 0 a tutti i maschi. In tal caso dobbiamo istruire R a comportarsi diversamente con le femmine e con i maschi.

Inoltre, un giorno potremmo ritrovarci a scrivere programmi più complessi, dove ci serve che R si comporti in modo diverso a seconda del risultato di una operazione precedente. Anche in questo caso ci sono delle condizioni da cui dipende quello che chiediamo a R di fare.

  • Gli enunciati if e else permettono di valutare una data condizione e di agire diversamente a seconda della risposta. Possono essere usati insieme, ma a volte serve solo il primo e non il secondo. La forma più elementare di dichiarazione condizionale sarebbe:
if(condition){
  # fa qualcosa #
}
# continua con il programma #

In questo caso, se la condizione è vera R eseguirà una data operazione, altrimenti passerà alle operazioni successive. Immaginate di andare a fare la spesa e di incontrare un amico lungo la strada. Nella maggior parte dei casi, avrete una linea di condotta che implica salutare o anche fermarsi a chiacchierare nel caso in cui incontriate qualcuno che conoscete per strada. Tuttavia, avete già un compito predefinito (andare a fare la spesa), quindi se non incontrate nessuno per strada sapete già cosa fare. In questo caso, una semplice dichiarazione if sarà sufficiente.

Un ulteriore passo è specificare un’operazione da eseguire nel caso in cui la condizione sia falsa. In questo caso la struttura della sintassi assomiglia di più a:

if(condition){
  # fa qualcosa #
}
else {
  # fa qualcos'altro #
}

Un esempio banale di struttura if/else è il seguente programma che valuta se un numero b è più grande di un altro numero a, e produce come output un oggetto y contenente un valore diverso a seconda del risultato di tale valutazione

a <- 0
b <- sample(-5:5, 1) # Estrae un numero casuale nella sequenza da -5 a +5
if(b > a) {
  y <- 1
} else {
  y <- 0
}
y
## [1] 1

Notare che questo può anche essere scritto nel seguente modo:

y <- if(b > a) {
  1
} else {
  0
}
y
## [1] 1

Una limitazione del comando if in R è che non può essere vettorizzato. Questo significa che se gli elementi da valutare sono più di uno, R valuterà solo il primo (e farà apparire anche un messaggio di warning).

a <- 0
b <- c(-5:5)
if(b > a) {
  y <- 1
} else {
  y <- 0
}
## Warning in if (b > a) {: the condition has length > 1 and only the first element
## will be used
y
## [1] 0

Questo complica le cose. Tuttavia, tenete a mente che gli enunciati if e else non sono pensati per ricodificare dati, ma per stare all’interno di algoritmi più complessi.

Operatori logici: & e |

Spesso gli enunciati if e else si applicano a condizioni complesse, o in generale a più di una sola condizione. Due operatori fondamentali per scrivere condizioni complesse sono gli operatori AND e OR (che potreste avere usato per ricerche su Google, dato che funzionano anche in quel caso).

a <- 0
b <- sample(c(-1, 1), 1)
c <- sample(c(0:10), 1)

# AND statement: &
if(b > a & c > 3) {
  y <- 0
} else {
  y <- 1
}
y
## [1] 1
# OR statement: |
if(b > a | c > 3) {
  y <- 0
} else {
  y <- 1
}
y
## [1] 0

La funzione ifelse()

  • La funzione ifelse() è una funzione “contenitore” (wrapper) per gli enunciati if e else. Essendo una funzione, ha l’indubbio vantaggio di essere vettorizzata. Questo implica che, a differenza delle strutture vistre poco fa, ifelse() può essere applicata a vettori e più in generale a sequenze di dati. La struttura è sempre la stessa: SE la condizione è vera ALLORA esegui “espressione 1”, ALTRIMENTI esegui “espressione 2”. Questo significa che la funzione vuole sapere sempre cosa fare in ogni caso. È una funzione molto utilizzata per ricodificare le variabili in un dataset.

Per esempio, data una variabile var1, possiamo utilizzare ifelse() per creare una seconda variabile chiamata var2 che ha valore \(1\) per tutti i casi in cui var è maggiore di \(100\), e \(0\) negli altri casi:

dat <- data.frame(
  var1 = c(1, 100, 3, 200, 5)
)
dat$var2 <- ifelse(dat$var1 > 100, 1, 0)
dat
##   var1 var2
## 1    1    0
## 2  100    0
## 3    3    0
## 4  200    1
## 5    5    0

Esercizio

Il data frame seguente contiene informazioni riguardanti il genere e l’età di 10 studenti:

students <- data.frame(
  sex = c("F", "M", "M", "F", "F", "F", "M", "F", "M", "F"),
  age = c(20, 23, 19, 18, 21, 21, 20, 22, 19, 18)
)

Create una variabile chiamata male_teen che ha valore 1 per tutti i maschi che hanno meno di 20 anni e 0 per tutti gli altri studenti.

Loop

I loop sono strutture che si usano per dire a R di ripetere una certa operazione per un determinato numero di volte, o finché non succede qualcosa (ad esempio, finché non si raggiunge la fine di un vettore, finché non si raggiunge un certo valore, ecc.) In generale, i loop si possono usare ogni volta che occorre fare qualcosa di ripetitivo e non si vuole digitare il codice ogni volta (per esempio, se avete bisogno di eseguire una regressione su molte variabili dipendenti). Al giorno d’oggi la programmazione efficiente è andata oltre l’uso dei loop, ma è utile vedere come funzionano i loop per “entrare” nella logica della programmazione. Inoltre, i loop sono meno “da scatola nera” di modi alternativi e più efficienti per ripetere le stesse operazioni, e questo è un utile esercizio per iniziare. Qui parliamo di 3 tipi di loop.

repeat

  • repeat, come dice il nome, si limita a ripetere la stessa espressione. Questo è il tipo di loop più semplice e generico che si possa trovare. Richiede tuttavia di utilizzare il comando break per essere interrotto:
i <- 0          # Questo oggetto fa da "indice", è necessario in ogni loop
repeat {
  if (i <= 25) {
    print(i)
    i <- i + 5
  } else break
}
## [1] 0
## [1] 5
## [1] 10
## [1] 15
## [1] 20
## [1] 25

Notare che senza il comando break il loop sarebbe andato avanti all’infinito.

while

  • I loop di tipo while ripetono la stessa operazione fino a quando una determinata condizione è vera. Sono un passo verso la minore generalità e maggiore semplicità rispetto a repeat. Per esempio il loop qui sotto concatena una serie di numeri a un vettore chiamato a. Prima di entrare nel loop creiamo un oggetto che utilizziamo come indice e chiamiamo i, a cui diamo valore \(1\). Dopodichè diciamo al loop di continuare ad aggiungere valori al vettore a fino a che i \(\leq 10\), e poi fermarsi. Ricordiamoci di aggiornare il valore di i all’interno del loop, altrimenti la condizione non si avvererà mai e il loop andrà avanti all’infinito.
a <- 0
i <- 1
while(i <= 10) {
    a <- c(a, i)
    i <- i + 1
}
print(a)
##  [1]  0  1  2  3  4  5  6  7  8  9 10

Altro esempio di loop di tipo while: la “sequenza di Fibonacci”. In questa sequenza ogni numero è la somma dei due numeri precedenti, date le prime due cifre \(0\) e \(1\). Potete cercarla su Wikipedia. NEll’esercizio sotto calcoliamo i primi 20 numeri della sequenza:

fib.seq <- c(0, 1)
i <- 1
while(i <= 18) {    # Abbiamo già i primi 2 numeri, quindi il loop deve girare 18 volte
  fib.seq <- c(fib.seq, fib.seq[i] + fib.seq[i + 1])
    i <- i + 1
}
fib.seq
##  [1]    0    1    1    2    3    5    8   13   21   34   55   89  144  233  377
## [16]  610  987 1597 2584 4181

for

  • I loop di tipo for sono i più comuni. Rappresentano un ulteriore passo verso una minore generalità, ma questo li rende anche più comodi da utilizzare. I loop di tipo for essenzialmente passano da ogni elemento all’interno di un vettore. In questo caso non dobbiamo definire i come variabile, perchè viene create implicitamente dal loop e viene aggiornata in automatico al suo interno
random.vector <- c(2, 5, 13, 0.5, 8, 100)
for(i in random.vector) {
    print(i^2)    # Notare che per riportare un valore dall'interno di un loop occorre utilizzare "print()"
}
## [1] 4
## [1] 25
## [1] 169
## [1] 0.25
## [1] 64
## [1] 10000

Esercizio

  • Scrivere un loop di tipo for per creare la sequenza di Fibonacci come abbiamo fatto sopra con while.

Vettorizzazione

I loop sono divertenti, tuttavia come ho detto prima, vengono usati sempre meno spesso Perché? Perché la maggior parte delle operazioni che si fanno in R usando i loop possono essere fatte più velocemente e usando meno codice vettorizzando un’operazione. In sostanza “vettorializzare” significa applicare la stessa funzione a tutti i valori, gruppi di valori, oggetti e quant’altro, che sono in un vettore/data frame/lista.

R vettorizza automaticamente molte funzioni. Per esempio, possiamo applicare automaticamente la nostra funzione plusone() a tutti gli elementi di un vettore o di una matrice:

vc <- c(1, 3, 5, 99)
mt <- matrix(c(4, 3, 10, 5, 2, 0), nrow = 3, ncol = 2)
plusone(vc)
## [1]   2   4   6 100
plusone(mt)
##      [,1] [,2]
## [1,]    5    6
## [2,]    4    3
## [3,]   11    1

Tuttavia, ci sono alcune funzioni che ci aiutano a vettorizzare operazioni complesse in modo esplicito.

# Per riga
apply(mt, 1, min)
## [1] 4 2 0
# Per colonna
apply(mt, 2, min)
## [1] 3 0

Naturalmente apply() può essere utilizzato sui data frame:

dat <- data.frame(
  var1 = c(1, 100, 3, 200, 5),
  var2 = c(70, 4, 1, 370, -7)
)
dat
##   var1 var2
## 1    1   70
## 2  100    4
## 3    3    1
## 4  200  370
## 5    5   -7
# Media per riga
apply(dat, 1, mean)
## [1]  35.5  52.0   2.0 285.0  -1.0
# Media per colonna
apply(dat, 2, mean)
## var1 var2 
## 61.8 87.6

Nel caso di operazioni più complesse, la funzione può essere scritta direttamente all’interno di apply():

# Differenza tra il valore massimo e quello minimo, per riga
apply(dat, 1, function(x) max(x) - min(x))
## [1]  69  96   2 170  12
any_list <- list(
  a = c(0.5, 10.6),
  b = c(1, 6, 30, 964, 0, NA),
  c = 9
)
lapply(any_list, length)
## $a
## [1] 2
## 
## $b
## [1] 6
## 
## $c
## [1] 1
lapply(any_list, plusone)
## $a
## [1]  1.5 11.6
## 
## $b
## [1]   2   7  31 965   1  NA
## 
## $c
## [1] 10
sapply(any_list, length)
## a b c 
## 2 6 1

Tuttavia, a seconda della struttura dei dati o dell’operazione richiesta, sapply() potrebbe non produrre un vettore ma una lista (diventando di fatto equivalente a lapply()). Per esempio, se vogliamo applicare la funzione plusone() a tutti gli elementi di any_list, il risultato manterrà la struttura multidimensionale di any_list per evitare di farci perdere informazione riguardante a quali elementi sono assieme nello stesso gruppo:

sapply(any_list, plusone)
## $a
## [1]  1.5 11.6
## 
## $b
## [1]   2   7  31 965   1  NA
## 
## $c
## [1] 10

In questo caso, se proprio vogliamo un vettore, possiamo usare la funzione unlist().

unlist(lapply(any_list, plusone))
##    a1    a2    b1    b2    b3    b4    b5    b6     c 
##   1.5  11.6   2.0   7.0  31.0 965.0   1.0    NA  10.0