1 Strutture di Controllo

Le strutture di controllo permettono di dirigere il flusso di esecuzione di un programma: eseguire istruzioni solo se una condizione è soddisfatta, ripetere blocchi di codice, o interrompere l’esecuzione anticipatamente. R offre le stesse strutture presenti nella maggior parte dei linguaggi di programmazione:

Struttura Uso principale
if / else esecuzione condizionale
ifelse() versione vettorializzata di if/else
while ripete finché una condizione è vera
repeat ripete indefinitamente fino a break
for itera su una sequenza di valori
break interrompe il ciclo corrente
next salta all’iterazione successiva

1.1 if / else

💡 if esegue un blocco di codice solo se la condizione è TRUE. La sintassi minimale è if (condizione) codice.

x <- 2
if (x < 3) print("x è minore di 3")
#> [1] "x è minore di 3"
x <- 4
if (x < 3) print("x è minore di 3")   # non viene eseguito

Con il blocco else si specifica cosa fare quando la condizione è FALSE:

x <- 2
if (x < 3) {
  print(x)
  print("x è minore di 3")
} else {
  print(x)
  print("x non è minore di 3")
}
#> [1] 2
#> [1] "x è minore di 3"
# Su una riga sola (per istruzioni brevi)
x <- 4
if (x < 3) print("x è minore di 3") else print("x non è minore di 3")
#> [1] "x non è minore di 3"

1.1.1 if/else come espressione

if/else restituisce in output l’ultima istruzione eseguita, che può essere salvata direttamente in un oggetto:

x <- 4
a <- if (x < 3) "x minore di 3" else "x non minore di 3"
a
#> [1] "x non minore di 3"
# Se manca il ramo else e la condizione è FALSE, restituisce NULL
b <- if (x < 3) "x minore di 3"
b   # → NULL
#> NULL
# Equivalenze
x <- 2
if (x < 3) a <- "x è minore di 3" else a <- "x non è minore di 3"
a <- if (x < 3) "x è minore di 3" else "x non è minore di 3"
a
#> [1] "x è minore di 3"

⚠️ if si aspetta una condizione scalare (un singolo TRUE o FALSE). Se si passa un vettore, R usa solo il primo elemento e lancia un warning. Per condizioni su vettori usare ifelse().

1.1.2 if/else Annidati

Quando le categorie sono più di due si possono concatenare più blocchi if/else:

x <- 6
if (x < 3) {
  print("x è minore di 3")
} else if (x == 3) {
  print("x è uguale a 3")
} else {
  print("x è maggiore di 3")
}
#> [1] "x è maggiore di 3"

1.2 ifelse() — Versione Vettorializzata

ifelse() è la versione vettorializzata di if/else: si applica elemento per elemento su un vettore e restituisce un vettore della stessa lunghezza.

x <- 2
b <- ifelse(x < 3, "x è minore di 3", "x non è minore di 3")
b
#> [1] "x è minore di 3"
# Su un vettore intero
x <- c(1, 4, 2, 7, 3)
ifelse(x < 3, "piccolo", "grande")
#> [1] "piccolo" "grande"  "piccolo" "grande"  "grande"

💡 ifelse() è particolarmente utile per creare nuove variabili in un data frame, come già visto con mutate():

df |> mutate(classe = ifelse(eta < 30, "giovane", "adulto"))

1.2.1 Esercizio

Esercizio 1
Dato x <- c(3, 7, 1, 9, 4, 2), usa ifelse() per creare un vettore che contenga "pari" o "dispari" per ogni elemento.
💡 L’operatore modulo %% restituisce il resto della divisione intera.

x <- c(3, 7, 1, 9, 4, 2)
ifelse(x %% 2 == 0, "pari", "dispari")
#> [1] "dispari" "dispari" "dispari" "dispari" "pari"    "pari"

1.3 while e repeat

1.3.1 while

while esegue un blocco di codice finché la condizione è vera. La condizione viene verificata prima di ogni iterazione.

while (condizione) codice
i <- 0
while (i < 6) {
  i <- i + 1
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6

⚠️ Se la condizione non diventa mai FALSE, il ciclo è infinito. Assicurarsi sempre che la variabile nella condizione venga modificata all’interno del blocco.

1.3.2 repeat

repeat esegue un blocco di codice indefinitamente, finché non incontra un’istruzione break. La condizione di uscita va quindi inserita esplicitamente all’interno del blocco.

repeat {
  codice
  if (condizione) break
}
i <- 0
repeat {
  i <- i + 1
  print(i)
  if (i > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6

💡 repeat è equivalente a while (TRUE) con un break interno. La scelta tra i due è stilistica: while è più leggibile quando la condizione di uscita è nota a priori; repeat è utile quando la condizione si valuta solo alla fine del blocco.


1.4 Cicli for

Il ciclo for itera su tutti gli elementi di un vettore, eseguendo il blocco di codice ad ogni iterazione. La variabile indice assume a turno il valore di ciascun elemento.

for (variabile in vettore) {
  codice
}
for (i in 1:6) {
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6

Il vettore su cui si itera può essere di qualsiasi tipo:

xval <- c("a", "b", "c", "d", "e")
for (i in xval) print(i)
#> [1] "a"
#> [1] "b"
#> [1] "c"
#> [1] "d"
#> [1] "e"

1.4.1 Cicli for Annidati

Si possono annidare più cicli for. L’indice del ciclo esterno rimane fisso mentre quello interno completa tutte le sue iterazioni:

xval <- c("a", "b", "c", "d", "e")
for (i in 1:5)
  for (j in xval) print(paste(i, j))
#> [1] "1 a"
#> [1] "1 b"
#> [1] "1 c"
#> [1] "1 d"
#> [1] "1 e"
#> [1] "2 a"
#> [1] "2 b"
#> [1] "2 c"
#> [1] "2 d"
#> [1] "2 e"
#> [1] "3 a"
#> [1] "3 b"
#> [1] "3 c"
#> [1] "3 d"
#> [1] "3 e"
#> [1] "4 a"
#> [1] "4 b"
#> [1] "4 c"
#> [1] "4 d"
#> [1] "4 e"
#> [1] "5 a"
#> [1] "5 b"
#> [1] "5 c"
#> [1] "5 d"
#> [1] "5 e"

1.5 break e next

1.5.1 break — Interrompere il Ciclo

break interrompe immediatamente il ciclo corrente (funziona con for, while e repeat):

# while con break
i <- 0
while (TRUE) {
  i <- i + 1
  print(i)
  if (i > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6
# for con break
for (i in 1:10) {
  print(i)
  if (i > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
#> [1] 6

1.5.2 next — Saltare un’Iterazione

next interrompe l’iterazione corrente e passa alla successiva, senza uscire dal ciclo:

# Stampa tutti i valori da 1 a 6, saltando il 3
for (i in 1:10) {
  if (i == 3) next
  print(i)
  if (i > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 4
#> [1] 5
#> [1] 6

💡 Lo stesso risultato si può ottenere con repeat, while o for — la scelta dipende da quale struttura rende il codice più leggibile nel contesto specifico:

i <- 0
repeat {
  i <- i + 1
  if (i == 3) next
  print(i)
  if (i > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 4
#> [1] 5
#> [1] 6

1.5.3 Esercizio

Esercizio 2
Scrivi un ciclo for che stampi i numeri da 1 a 20, ma salti i multipli di 3 e si fermi quando incontra il primo multiplo di 7 maggiore di 10.

for (i in 1:20) {
  if (i %% 3 == 0) next          # salta i multipli di 3
  if (i %% 7 == 0 & i > 10) break  # ferma al primo multiplo di 7 > 10
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 4
#> [1] 5
#> [1] 7
#> [1] 8
#> [1] 10
#> [1] 11
#> [1] 13

2 La Famiglia apply

I cicli for sono flessibili ma poco concisi. R offre la famiglia apply come alternativa più compatta e spesso più efficiente, particolarmente adatta per applicare una funzione a tutti gli elementi di una struttura dati.

Funzione Input Output
apply() matrice/array vettore, matrice o lista
lapply() lista o vettore lista
sapply() lista o vettore vettore/matrice (se possibile)
mapply() più vettori vettore/lista
tapply() vettore + fattore array (statistiche per gruppo)

2.1 apply() — Su Matrici e Array

apply(X, MARGIN, FUN) applica una funzione alle righe (MARGIN = 1) o alle colonne (MARGIN = 2) di una matrice.

z <- matrix(1:50, nrow = 10, ncol = 5)

apply(z, 1, mean)   # media per riga
#>  [1] 21 22 23 24 25 26 27 28 29 30
apply(z, 2, sd)     # deviazione standard per colonna
#> [1] 3.02765 3.02765 3.02765 3.02765 3.02765

Con valori mancanti è necessario na.rm = TRUE:

z[1, 1] <- NA

apply(z, 2, mean)                      # → NA nella prima colonna
#> [1]   NA 15.5 25.5 35.5 45.5
apply(z, 2, mean, na.rm = TRUE)        # → calcola ignorando NA
#> [1]  6.0 15.5 25.5 35.5 45.5

apply() accetta anche funzioni anonime definite al volo:

# Equivalente a mean(na.rm=TRUE) scritto come funzione anonima
apply(z, 2, function(x) sum(x) / length(x))
#> [1]   NA 15.5 25.5 35.5 45.5
# Alcune trasformazioni utili
apply(z, 2, FUN = function(x) x - min(x))           # scala da 0 al max
#>       [,1] [,2] [,3] [,4] [,5]
#>  [1,]   NA    0    0    0    0
#>  [2,]   NA    1    1    1    1
#>  [3,]   NA    2    2    2    2
#>  [4,]   NA    3    3    3    3
#>  [5,]   NA    4    4    4    4
#>  [6,]   NA    5    5    5    5
#>  [7,]   NA    6    6    6    6
#>  [8,]   NA    7    7    7    7
#>  [9,]   NA    8    8    8    8
#> [10,]   NA    9    9    9    9
apply(z, 2, FUN = function(x) (x - mean(x)) / sd(x)) # standardizzazione
#>       [,1]       [,2]       [,3]       [,4]       [,5]
#>  [1,]   NA -1.4863011 -1.4863011 -1.4863011 -1.4863011
#>  [2,]   NA -1.1560120 -1.1560120 -1.1560120 -1.1560120
#>  [3,]   NA -0.8257228 -0.8257228 -0.8257228 -0.8257228
#>  [4,]   NA -0.4954337 -0.4954337 -0.4954337 -0.4954337
#>  [5,]   NA -0.1651446 -0.1651446 -0.1651446 -0.1651446
#>  [6,]   NA  0.1651446  0.1651446  0.1651446  0.1651446
#>  [7,]   NA  0.4954337  0.4954337  0.4954337  0.4954337
#>  [8,]   NA  0.8257228  0.8257228  0.8257228  0.8257228
#>  [9,]   NA  1.1560120  1.1560120  1.1560120  1.1560120
#> [10,]   NA  1.4863011  1.4863011  1.4863011  1.4863011
apply(z, 2, scale)                                    # equivalente
#>             [,1]       [,2]       [,3]       [,4]       [,5]
#>  [1,]         NA -1.4863011 -1.4863011 -1.4863011 -1.4863011
#>  [2,] -1.4605935 -1.1560120 -1.1560120 -1.1560120 -1.1560120
#>  [3,] -1.0954451 -0.8257228 -0.8257228 -0.8257228 -0.8257228
#>  [4,] -0.7302967 -0.4954337 -0.4954337 -0.4954337 -0.4954337
#>  [5,] -0.3651484 -0.1651446 -0.1651446 -0.1651446 -0.1651446
#>  [6,]  0.0000000  0.1651446  0.1651446  0.1651446  0.1651446
#>  [7,]  0.3651484  0.4954337  0.4954337  0.4954337  0.4954337
#>  [8,]  0.7302967  0.8257228  0.8257228  0.8257228  0.8257228
#>  [9,]  1.0954451  1.1560120  1.1560120  1.1560120  1.1560120
#> [10,]  1.4605935  1.4863011  1.4863011  1.4863011  1.4863011

2.1.1 Confronto con il ciclo for

# Ciclo for
v2 <- c()
for (i in 1:ncol(z)) v2[i] <- mean(z[, i], na.rm = TRUE)

# apply
v1 <- apply(z, 2, mean, na.rm = TRUE)

v1
#> [1]  6.0 15.5 25.5 35.5 45.5
v2
#> [1]  6.0 15.5 25.5 35.5 45.5

💡 Per matrici di grandi dimensioni apply() è generalmente più veloce del ciclo for. Si può misurare con system.time():

system.time({ for (i in 1:ncol(z)) v2[i] <- mean(z[, i]) })
system.time({ v1 <- apply(z, 2, mean) })

2.2 lapply() — Su Liste

lapply(X, FUN) applica FUN a ogni elemento della lista X e restituisce sempre una lista con lo stesso numero di elementi.

x  <- as.list(1:5)

v1 <- lapply(x, log)   # lista
v2 <- list()
for (i in seq_along(x)) v2[[i]] <- log(x[[i]])

v1
#> [[1]]
#> [1] 0
#> 
#> [[2]]
#> [1] 0.6931472
#> 
#> [[3]]
#> [1] 1.098612
#> 
#> [[4]]
#> [1] 1.386294
#> 
#> [[5]]
#> [1] 1.609438
v2
#> [[1]]
#> [1] 0
#> 
#> [[2]]
#> [1] 0.6931472
#> 
#> [[3]]
#> [1] 1.098612
#> 
#> [[4]]
#> [1] 1.386294
#> 
#> [[5]]
#> [1] 1.609438

2.3 sapply() — Versione Semplificata di lapply()

sapply() è analoga a lapply() ma semplifica il risultato: restituisce un vettore o una matrice quando possibile, altrimenti una lista.

sapply(x, log)     # restituisce un vettore numerico
#> [1] 0.0000000 0.6931472 1.0986123 1.3862944 1.6094379
sapply(3:9, seq)   # restituisce una lista (lunghezze diverse)
#> [[1]]
#> [1] 1 2 3
#> 
#> [[2]]
#> [1] 1 2 3 4
#> 
#> [[3]]
#> [1] 1 2 3 4 5
#> 
#> [[4]]
#> [1] 1 2 3 4 5 6
#> 
#> [[5]]
#> [1] 1 2 3 4 5 6 7
#> 
#> [[6]]
#> [1] 1 2 3 4 5 6 7 8
#> 
#> [[7]]
#> [1] 1 2 3 4 5 6 7 8 9

2.4 mapply() — Su Più Argomenti

mapply() è la versione multivariata di sapply(): applica una funzione passando in parallelo più vettori come argomenti.

# rep(1, 4), rep(2, 3), rep(3, 2), rep(4, 1)
mapply(rep, 1:4, 4:1)
#> [[1]]
#> [1] 1 1 1 1
#> 
#> [[2]]
#> [1] 2 2 2
#> 
#> [[3]]
#> [1] 3 3
#> 
#> [[4]]
#> [1] 4

2.4.1 Esercizio

Esercizio 3
Usando apply(), calcola per ogni colonna della matrice z (definita sopra): minimo, massimo e range (max - min). Confronta il risultato con un ciclo for equivalente.

z <- matrix(1:50, nrow = 10, ncol = 5)

# apply
apply(z, 2, min)
#> [1]  1 11 21 31 41
apply(z, 2, max)
#> [1] 10 20 30 40 50
apply(z, 2, function(x) max(x) - min(x))
#> [1] 9 9 9 9 9
# for equivalente
risultati <- matrix(NA, nrow = 3, ncol = ncol(z),
                    dimnames = list(c("min", "max", "range"), NULL))
for (i in 1:ncol(z)) {
  risultati["min",   i] <- min(z[, i])
  risultati["max",   i] <- max(z[, i])
  risultati["range", i] <- max(z[, i]) - min(z[, i])
}
risultati
#>       [,1] [,2] [,3] [,4] [,5]
#> min      1   11   21   31   41
#> max     10   20   30   40   50
#> range    9    9    9    9    9

3 Funzioni

In R è possibile creare funzioni personalizzate e organizzarle in pacchetti. Le funzioni vengono salvate come oggetti nell’environment e possono essere usate esattamente come le funzioni predefinite di R.

La sintassi è:

nome_funzione <- function(arg1, arg2, ...) {
  corpo della funzione
  return(risultato)   # opzionale
}

3.1 Funzioni di Base

cube <- function(x) {
  y <- x^3
  return(y)
}
cube(3)
#> [1] 27
# Equivalentemente — senza salvare in un oggetto intermedio
cube <- function(x) {
  return(x^3)
}
cube(2)
#> [1] 8

💡 Se return() è omesso, la funzione restituisce automaticamente l’ultima espressione valutata:

power <- function(x, exp) {
  x^exp         # ultima istruzione → viene restituita automaticamente
}
power(2, 2)
#> [1] 4
a <- power(2, 2)
a
#> [1] 4

3.2 Argomenti con Valore di Default

Si possono specificare valori di default per gli argomenti: se l’utente non li fornisce, viene usato il default.

power <- function(x, exp = 1) {   # exp = 1 è il default
  y <- x^exp
  return(y)
}

power(2)      # usa exp = 1 → restituisce 2
#> [1] 2
power(2, 3)   # usa exp = 3 → restituisce 8
#> [1] 8

3.3 Restituire Più Oggetti

Una funzione può restituire un solo oggetto. Per restituire più valori si raccolgono in un vettore o in una lista:

power <- function(x, exp = 1) {
  y <- x^exp
  return(c(result = y, input = x, exp = exp))
}

power_list <- function(x, exp = 1) {
  y <- x^exp
  return(list(result = y, input = x, exp = exp))
}

power(2)
#> result  input    exp 
#>      2      2      1
power(2, 2)
#> result  input    exp 
#>      4      2      2
a <- power(2, 2)
a
#> result  input    exp 
#>      4      2      2
a[1]
#> result 
#>      4
a_list <- power(2, 2)
a_list[[1]]
#> [1] 4
# Equivalente
#a_list$result

3.4 Esempio: Funzione per la Media

x <- 1:10

mymean <- function(a) sum(a) / length(a)

mymean(x)
#> [1] 5.5
mean(x)   # confronto con la funzione built-in
#> [1] 5.5

3.4.1 Esercizio

Esercizio 4
Scrivi una funzione myvar() che calcoli la varianza di un vettore numerico e confronta il risultato con var().
Ricorda: \(\text{Var}(x) = \frac{1}{n-1} \sum_{i=1}^{n}(x_i - \bar{x})^2\)

myvar <- function(x) {
  n    <- length(x)
  xbar <- sum(x) / n
  sum((x - xbar)^2) / (n - 1)
}

x <- c(2, 4, 4, 4, 5, 5, 7, 9)
myvar(x)
#> [1] 4.571429
var(x)    # devono coincidere
#> [1] 4.571429

Esercizio 5
Scrivi una funzione mysummary() che prenda in input un data frame e restituisca un data frame con, per ogni variabile numerica: minimo, massimo, media, i tre quartili, numero di NA e numero di valori unici.
Verifica il funzionamento sul dataset Insurance del pacchetto MASS.

mysummary <- function(df) {
  df2 <- df[, sapply(df, is.numeric)]

  varMean   <- apply(df2, 2, mean,     na.rm = TRUE)
  varQuant  <- apply(df2, 2, quantile, na.rm = TRUE)
  varNA     <- apply(df2, 2, function(x) sum(is.na(x)))
  varUnique <- apply(df2, 2, function(x) length(unique(x)))
  varMin    <- apply(df2, 2, min, na.rm = TRUE)
  varMax    <- apply(df2, 2, max, na.rm = TRUE)

  as.data.frame(rbind(
    min     = varMin,
    Q1      = varQuant[2, ],
    mean    = varMean,
    median  = varQuant[3, ],
    Q3      = varQuant[4, ],
    max     = varMax,
    n_NA    = varNA,
    n_unici = varUnique
  ))
}

library(MASS)
mysummary(Insurance)

3.5 get() — Iterare su Oggetti per Nome

La funzione get() prende in input una stringa e restituisce l’oggetto con quel nome nell’environment. Permette di costruire cicli for che iterano su diversi oggetti (ad esempio funzioni):

square       <- function(x) x^2
cube         <- function(x) x^3
doublesquare <- function(x) x^4

for (name in c("square", "cube", "doublesquare", "mean")) {
  f <- get(name)
  print(f(2))
}
#> [1] 4
#> [1] 8
#> [1] 16
#> [1] 2