Corso
Questo tutorial sui loop in R analizzerà le strutture disponibili in R per iterare, quando andrebbero usate e come sfruttare le alternative, come la funzionalità di vettorizzazione di R, per svolgere i compiti di iterazione in modo più efficiente.
Il post presenterà alcuni esempi di loop per poi criticarli e deprecarli a favore delle alternative vettorizzate più diffuse tra le molte disponibili nel ricco set di librerie che R offre.
In generale, il consiglio di questo tutorial sui loop in R è: impara i loop. Ti offrono una visione dettagliata di ciò che dovrebbe accadere a livello elementare e ti aiutano a comprendere i dati che stai manipolando.
E, una volta compresi bene i loop, liberatene.
Concentra gli sforzi nell'imparare le alternative vettorizzate. Ripagano in termini di efficienza.
Per esercitarti in modo interattivo, prova il capitolo sui loop nel nostro corso R intermedio.
Cosa sono i loop?
“Iterare”, “ciclare”, “ripetere” o semplicemente replicare istruzioni è una pratica antica, nata ben prima dell'invenzione dei computer. Non è altro che l'automazione di un processo multi-step organizzando sequenze di azioni o processi “batch” e raggruppando le parti che devono essere ripetute.
Tutti i linguaggi di programmazione moderni forniscono costrutti speciali che permettono la ripetizione di istruzioni o blocchi di istruzioni.
In generale, esistono due tipi di questi costrutti speciali, o loop, nei linguaggi moderni. Alcuni loop si eseguono per un numero prestabilito di volte, controllato da un contatore o da un indice, incrementato a ogni iterazione. Questi fanno parte della famiglia dei for.
Altri loop, invece, si basano sull'insorgere e sulla verifica di una condizione logica. La condizione viene testata all'inizio o alla fine del costrutto. Queste varianti appartengono rispettivamente alla famiglia dei while o dei repeat.
Un'introduzione ai loop in R
Secondo il manuale base di R, tra i comandi di controllo di flusso, i costrutti di loop sono for, while e repeat, con le clausole aggiuntive break e next.
Ricorda che i comandi di controllo di flusso sono i comandi che permettono a un programma di diramarsi tra alternative, o di “prendere decisioni”, per così dire.
Puoi sempre vedere questi comandi richiamando ?Control nella riga di comando di RStudio.
?Control
Questo flow chart mostra le strutture di loop in R:

For loop in R
Le prossime sezioni analizzeranno più da vicino ognuna delle strutture mostrate nella figura sopra. Inizieremo dalla struttura a sinistra e proseguiremo spostandoci gradualmente verso destra.
Per un'introduzione video ai for loop e un esercizio di follow-up, prova il capitolo For Loop nel nostro corso R intermedio.
For loop spiegati
Questa struttura di loop, composta dal riquadro rettangolare “init” (o inizializzazione), dal rombo decisionale e dal riquadro rettangolare i1, viene eseguita un numero noto di volte.
In termini di flowchart, i riquadri rettangolari significano qualcosa come “fai qualcosa che non implica decisioni”. I rombi, invece, sono chiamati “simboli di decisione” e quindi si traducono in domande con due sole possibili risposte logiche, ovvero True (T) o False (F).
Nota che, per semplicità, altri possibili simboli sono stati omessi dalla figura.
Una o più istruzioni all'interno del rettangolo di inizializzazione sono seguite dalla valutazione della condizione su una variabile che può assumere valori all'interno di una sequenza specificata. In figura, questo è rappresentato dal rombo: i simboli significano “il valore corrente della variabile v appartiene alla sequenza seq?”.
In altre parole, stai verificando se il valore corrente di v è all'interno di un intervallo specificato. Normalmente definisci questo intervallo nell'inizializzazione, con qualcosa come 1:100 per far sì che il loop inizi.
Se la condizione non è soddisfatta e l'esito è False, il loop non viene mai eseguito. Questo è indicato dalla freccia laterale a destra della struttura del for. Il programma eseguirà quindi la prima istruzione trovata dopo il blocco del loop.
Se la condizione è verificata, viene eseguita un'istruzione -o un blocco di istruzioni- i1. E magari questo blocco è un altro loop. In tali casi, si parla di loop annidato.
Una volta fatto ciò, la condizione viene valutata di nuovo. Questo è indicato dalle linee che vanno da i1 verso l'alto, subito dopo il riquadro di inizializzazione. In R -e in Python- è possibile esprimerlo in linguaggio naturale, chiedendo se la nostra variabile appartiene o meno a un intervallo di valori.
Nota che in altri linguaggi, per esempio in C, la condizione è resa più esplicita con l'uso di un operatore logico, come maggiore o minore di, uguale a, …
Ecco un esempio di semplice for loop:
# Create a vector filled with random normal values
u1 <- rnorm(30)
print("This loop calculates the square of the first 10 elements of vector u1")
# Initialize `usq`
usq <- 0.
for (i in 1:10) {
# i-th element of `u1` squared into `i`-th position of `usq`
usq[i] <- u1[i] * u1[i]
print(usq[i])
}
print(i)
Il blocco for è racchiuso tra parentesi graffe. Possono essere poste subito dopo la condizione di test o sotto di essa, preferibilmente con una rientranza. Nulla di ciò è obbligatorio, ma le parentesi graffe migliorano decisamente la leggibilità del codice e permettono di individuare facilmente il blocco del loop e i potenziali errori al suo interno.
Nota che il vettore dei quadrati, usq, è inizializzato. Questo non sarebbe necessario nel semplice codice RStudio, ma nella versione in markup, knitr non compilerebbe perché non trova un riferimento al vettore prima del suo utilizzo nel loop, generando quindi un errore in RStudio. Per maggiori informazioni su knitr, vai alla pagina di R Markdown.
Annidare i for loop
Ora che sai che i for loop possono anche essere annidati, probabilmente ti stai chiedendo perché e quando usarli nel tuo codice.
Supponi, per esempio, di voler manipolare un array bidimensionale assegnando ai suoi elementi valori specifici.
Potresti allora fare qualcosa del genere:
# Insert your own integer here
my_int <- 42
nr <- as.integer(my_int)
# Create a `n` x `n` matrix with zeros
mymat <- matrix(0, nr, nr)
# For each row and for each column, assign values based on position
# These values are the product of two indexes
for (i in 1:dim(mymat)[1]) {
for (j in 1:dim(mymat)[2]) {
mymat[i, j] = i * j
}
}
Suggerimento: per maggiori informazioni sulla funzione matrix(), visita R Documentation.
i scorre sulle righe e j scorre sulle colonne.Hai creato la familiarissima tavola pitagorica che dovresti conoscere a memoria.
Puoi anche scegliere un intero e poi produrre una tabella in base alla tua scelta: puoi assegnare un intero a una variabile se la tabella è quadrata o a due variabili se la tabella è rettangolare. Questa variabile fungerà poi da limite superiore per gli indici i e j.
# Show the first 10x10 chunk or the first `nr` x `nr` chunk
if (nr > 10) {
mymat[1:10, 1:10]
} else
mymat[1:nr, 1:nr]
Nota che, per evitare di inondare lo schermo con tabelle enormi, inserisci una condizione alla fine per stampare solo il primo blocco 10 x 10 se l'utente ha richiesto un intero maggiore di 10. Altrimenti verrà stampato un blocco n x n.
Il codice completo è il seguente:
# Insert your own integer here
my_int <- 42
nr <- as.integer(my_int)
# Create a `n` x `n` matrix with zeroes
mymat <- matrix(0, nr, nr)
# For each row and for each column, assign values based on position
# These values are the product of two indexes
for (i in 1:dim(mymat)[1]) {
for (j in 1:dim(mymat)[2]) {
mymat[i, j] = i * j
}
}
# Show the first 10x10 chunk or the first `nr` x `nr` chunk
if (nr > 10) {
mymat[1:10, 1:10]
} else
mymat[1:nr, 1:nr]
For loop: popolari ma non sempre perfetti per il tuo uso
Il loop for è di gran lunga il più popolare e il suo costrutto implica che il numero di iterazioni sia fisso e noto in anticipo, come nei casi “genera i primi 200 numeri primi” o “elenca i 10 clienti più importanti”.
Ma cosa succede se non conosci o non controlli il numero di iterazioni, e possono verificarsi una o più condizioni non prevedibili a priori?
Per esempio, potresti voler contare il numero di clienti che vivono in un'area identificata da un certo CAP, o il numero di clic su un banner web negli ultimi due giorni, o eventi analoghi e imprevisti.
In casi come questi, il loop while e il suo cugino repeat possono venire in soccorso…
While loop
Il loop while, al centro della figura sopra, è composto come prima da un blocco di inizializzazione, seguito da una condizione logica. Questa condizione è tipicamente espressa dal confronto tra una variabile di controllo e un valore, usando maggiore di, minore di o uguale a, ma è legittima qualsiasi espressione che valuti a un valore logico, True o False.
Se il risultato è False (F), il loop non viene mai eseguito, come indicato dalla freccia laterale a destra della figura. Il programma eseguirà quindi la prima istruzione che trova dopo il blocco del loop.
Se è True (T), viene eseguita l'istruzione o il blocco di istruzioni i1.
Nota che è stata aggiunta un'ulteriore istruzione o blocco di istruzioni i2: serve come aggiornamento della variabile di controllo, che può alterare il risultato della condizione all'inizio del loop, ma non è necessario. Oppure potresti voler aggiungere un incremento a un contatore per tenere traccia del numero di iterazioni eseguite. Le iterazioni cessano quando la condizione risulta falsa.
Il formato è while(cond) expr, dove cond è la condizione da testare ed expr è un'espressione.
Per esempio, il seguente loop chiede all'utente con una funzione definita dall'utente (UDF) di inserire la risposta corretta alla domanda sull'universo e tutto quanto. Continuerà a farlo fino a quando l'utente non darà la risposta giusta:
# Your User Defined Function
readinteger <- function(){
n <- readline(prompt="Please, enter your ANSWER: ")
}
response <- as.integer(readinteger())
while (response!=42) {
print("Sorry, the answer to whatever the question MUST be 42");
response <- as.integer(readinteger());
}
Per cominciare, usa una funzione definita dall'utente per acquisire l'input prima di entrare nel loop. Questo loop proseguirà finché la risposta non è la 42 attesa.
In altre parole, lo fai perché altrimenti R si lamenterebbe dell'espressione mancante che dovrebbe fornire il richiesto True o False — e infatti non conosce ‘response’ prima di usarlo nel loop. Lo fai anche perché, se rispondi correttamente al primo tentativo, il loop non verrà eseguito affatto.
Repeat loop
Il loop repeat si trova all'estrema destra del diagramma di flusso sopra. È simile al while, ma è strutturato in modo che i blocchi di istruzioni i1 e i2 vengano eseguiti almeno una volta, a prescindere dal risultato della condizione.
Seguendo altri linguaggi, si potrebbe chiamare questo loop “repeat until” per sottolineare il fatto che le istruzioni i1 e i2 vengono eseguite finché la condizione rimane False (F) o, equivalentemente, diventa True (T), uscendo; ma in ogni caso, almeno una volta.
Come variante del precedente esempio, potresti scrivere:
readinteger <- function(){
n <- readline(prompt="Please, enter your ANSWER: ")
}
repeat {
response <- as.integer(readinteger());
if (response == 42) {
print("Well done!");
break
} else print("Sorry, the answer to whatever the question MUST be 42");
}
Dopo la ormai familiare funzione di input, hai il loop repeat il cui blocco viene eseguito almeno una volta e terminerà quando sarà verificata la condizione if.
Nota che hai dovuto impostare una condizione all'interno del loop sulla quale uscire con la clausola break. Questa clausola ci introduce alla nozione di uscita o interruzione dei cicli all'interno dei loop.
Interruzione e uscita dai loop in R
Quindi, come si esce da un loop?
In altri termini, a parte la fine “naturale” del loop, che avviene perché hai raggiunto il numero prescritto di iterazioni (for) o perché hai soddisfatto una condizione (while, repeat), puoi fermare o interrompere il loop?
E se sì, come?
L'istruzione break risponde alla prima domanda: l'hai vista nell'ultimo esempio.
Interrompi i tuoi loop con break
Quando l'interprete R incontra un break, passa il controllo all'istruzione immediatamente successiva alla fine del loop (se esiste). Nel caso di loop annidati, break permette di uscire solo dal loop più interno.
Ecco un esempio.
Questo pezzo di codice definisce una matrice m x n di zeri e poi entra in un for annidato per riempire le posizioni della matrice, ma solo se i due indici sono diversi. Lo scopo è creare una matrice triangolare inferiore, cioè una matrice i cui elementi sotto la diagonale principale sono non nulli. Gli altri restano al loro valore inizializzato a zero.
Quando gli indici sono uguali e quindi è soddisfatta la condizione nel loop interno, che scorre sull'indice di colonna j, viene eseguito un break e il loop più interno viene interrotto con un salto diretto all'istruzione successiva al loop interno. Questa istruzione è una print(). Poi, il controllo passa alla condizione del for esterno (sulle righe, indice i), che viene valutata di nuovo.
Se gli indici differiscono, viene eseguita l'assegnazione e il contatore viene incrementato di 1. Alla fine, il programma stampa il contatore ctr, che contiene il numero di elementi assegnati.
# Make a lower triangular matrix (zeroes in upper right corner)
m = 10
n = 10
# A counter to count the assignment
ctr = 0
# Create a 10 x 10 matrix with zeroes
mymat = matrix(0, m, n)
for (i in 1:m) {
for (j in 1:n) {
if (i == j) {
break
} else {
# you assign the values only when i<>j
mymat[i, j] = i * j
ctr = ctr + 1
}
}
print(i * j)
}
# Print how many matrix cells were assigned
print(ctr)
Nota che può essere un po' eccessivamente prudente mettere le parentesi graffe anche quando non sono strettamente necessarie. Di solito lo fai per assicurarti che qualsiasi cosa aperta con una { venga anche chiusa con una }. Quindi, se noti numeri non corrispondenti di { o }, sai che c'è un errore, anche se il contrario non è necessariamente vero!
L'uso di next nei loop
next interrompe una particolare iterazione e salta al ciclo successivo. Di fatto, salta alla valutazione della condizione del loop corrente.
In altri linguaggi potresti trovare l'equivalente (un po' fuorviante) chiamato “continue”, che significa lo stesso: ovunque tu sia, al verificarsi della condizione, salta alla valutazione del loop.
Un esempio più semplice di proseguire il loop scartando un ciclo particolare al verificarsi di una condizione è:
m = 20
for (k in 1:m) {
if (!k %% 2)
next
print(k)
}
Questo codice stampa tutti i numeri dispari nell'intervallo 1:m (qui m=20). In altre parole, verranno stampati tutti gli interi tranne quelli con resto non zero quando divisi per 2 (da qui l'uso dell'operatore modulo %%), come specificato dal test if.
I numeri il cui resto è zero non verranno stampati, perché il programma salta alla valutazione della condizione i in 1:m e ignora qualsiasi istruzione successiva. In questo caso, print(k) viene ignorata.
Ricapitolando l'uso dei loop in R
- Cerca di inserire nel loop il minor codice possibile portando fuori quante più istruzioni puoi. Ricorda, tutto ciò che è dentro il loop verrà ripetuto più volte e magari non è necessario.
- Fai attenzione quando usi
repeat: assicurati che una terminazione sia impostata esplicitamente testando una condizione o rischi di finire in un loop infinito. - È meglio usare una o più chiamate di funzione all'interno del loop se un loop sta diventando (troppo) grande. Le chiamate di funzione renderanno più facile ad altri utenti seguire il codice. Ma l'uso di un for loop annidato per eseguire operazioni su matrici o array è probabilmente il segnale che non hai implementato le cose nel modo migliore per un linguaggio basato su matrici come R.
- Non è consigliabile “far crescere” variabili o dataset usando un'assegnazione a ogni iterazione. In alcuni linguaggi come Matlab viene emesso un avviso: puoi continuare, ma sei invitato a considerare alternative. Un esempio tipico sarà mostrato nella prossima sezione.
Quando usare i loop in R
Tutto bene, ma quando dovresti usare i loop in R e quando no?
Ogni volta che qualche operazione deve essere ripetuta, un loop può tornare utile.
Devi solo specificare quante volte o a quali condizioni quelle operazioni devono essere eseguite: assegni valori iniziali a una variabile di controllo del loop, esegui il loop e poi, una volta finito, in genere fai qualcosa con i risultati.
Ma quando dovresti usare i loop?
Non potresti replicare l'istruzione desiderata per il numero sufficiente di volte?
Be', una regola empirica potrebbe essere che se devi eseguire un'azione (diciamo) tre volte o più, allora un loop ti servirà meglio. Rende il codice più compatto, leggibile e manutenibile e ti fa risparmiare battitura: metti che scopri che una certa istruzione va ripetuta una volta in più del previsto; invece di riscrivere l'intera istruzione, puoi semplicemente modificare il valore di una variabile nella condizione di test.
Tuttavia, la natura peculiare di R suggerisce di non usare affatto i loop (!) quando esistono alternative.
Per fortuna, ci sono delle alternative!
R gode di una caratteristica che pochi linguaggi hanno, chiamata vettorizzazione.
Le alternative ai loop in R
Che cos'è la vettorizzazione?
Come suggerisce la parola, la vettorizzazione è l'operazione di convertire operazioni ripetute su numeri semplici (“scalari”) in singole operazioni su vettori o matrici. Ne hai visti diversi esempi nelle sottosezioni sopra.
Ora, un vettore è la struttura dati elementare in R ed è “un'entità singola costituita da una raccolta di elementi”, secondo il manuale base di R.
Dunque, una raccolta di numeri è un vettore numerico.
Se combini vettori (della stessa lunghezza), ottieni una matrice. Puoi farlo in verticale o in orizzontale, usando diverse istruzioni di R. Così, in R, una matrice è vista come una collezione di vettori orizzontali o verticali. Per estensione, puoi vettorizzare operazioni ripetute sui vettori.
Molti dei costrutti di loop sopra possono essere resi impliciti usando la vettorizzazione.
Dico “impliciti” perché non scompaiono davvero. A un livello più basso, la forma vettorizzata alternativa si traduce in codice che conterrà uno o più loop nel linguaggio di basso livello in cui la forma è stata implementata e compilata (Fortran, C o C++).
Questi sono nascosti all'utente e di solito sono più veloci dell'equivalente codice R esplicito, ma a meno che tu non stia pianificando di implementare le tue funzioni R usando uno di quei linguaggi, questo è del tutto trasparente.
Il caso più elementare che puoi immaginare è la somma di due vettori v1 e v2 in un vettore v3, che può essere fatta elemento per elemento con un for loop:
v1 <- c(3,4,5)
v2 <- c(3,4,6)
v3 <- c(0,0,0)
n = length(v1)
for (i in 1:n) {
v3[i] <- v1[i] + v2[i]
}
v3
Oppure puoi usare la forma vettorizzata “nativa”:
v3 = v1 + v2
v3
Nota che dici “nativa” perché R riconosce tutti gli operatori aritmetici come operanti su vettori e matrici.
Allo stesso modo, per due matrici A e B, invece di sommare gli elementi di A[i,j] e B[i,j] nelle posizioni corrispondenti, per cui dovresti gestire due indici i e j, puoi dire a R di fare quanto segue:
A <- matrix(1:6, nrow = 3, ncol = 2)
B <- matrix(c(2,4,3,1,5,7), nrow=3, ncol=2)
C= A + B
C
Ed è davvero molto semplice!
Vettorizzazione in R spiegata
Perché la vettorizzazione dovrebbe essere più veloce, dato che il numero di operazioni elementari sembra lo stesso?
La spiegazione migliore richiederebbe di guardare ai meccanismi interni di R, cosa che meriterebbe un post a parte; in breve: innanzitutto, R è un linguaggio interpretato e, in quanto tale, tutti i dettagli sulla definizione delle variabili sono gestiti dall'interprete. Non hai bisogno di specificare che un numero è in virgola mobile o di allocare memoria con un puntatore, per esempio.
L'interprete di R “capisce” queste questioni dal contesto man mano che inserisci i comandi, ma lo fa comando per comando. Dovrà quindi gestire tali definizioni ogni volta che impartisci un dato comando, anche se lo stai solo ripetendo.
Un compilatore, invece, risolve letteralmente tutte le definizioni e dichiarazioni in fase di compilazione su tutto il codice; quest'ultimo viene tradotto in un codice binario compatto e ottimizzato, prima di eseguire qualsiasi cosa. Ora, poiché le funzioni di R sono scritte in uno di questi linguaggi di basso livello, sono più efficienti.
In pratica, guardando al codice di basso livello, si scoprirebbero chiamate a C o C++, di solito implementate all'interno di quello che viene chiamato wrapper code.
In secondo luogo, nei linguaggi che supportano la vettorizzazione (come R o Matlab), ogni istruzione che utilizza un dato numerico agisce su un oggetto nativamente definito come vettore, anche se composto da un solo elemento. Questo è il default quando definisci, per esempio, una singola variabile numerica: la sua rappresentazione interna in R sarà sempre un vettore, nonostante sia composta da un solo numero.
I loop continuano a esistere sotto il cofano, ma al livello compilato C/C++ molto più veloce. Il vantaggio di avere un vettore è che le definizioni sono risolte dall'interprete una sola volta, sull'intero vettore, a prescindere dalla sua dimensione, in contrasto con un loop eseguito su uno scalare, dove le definizioni e le allocazioni vanno fatte elemento per elemento, ed è più lento.
Infine, lavorare con il formato vettoriale nativo permette di sfruttare routine di Algebra Lineare molto efficienti (come BLAS, Basic Linear Algebra Subprograms), così che, quando esegue istruzioni vettorizzate, R si appoggia a queste routine numeriche. Quindi il messaggio è: quando possibile, processa intere strutture dati con una singola chiamata di funzione per evitare tutte le operazioni di copia che vengono eseguite.
Ma basta digressioni: facciamo un esempio generale di vettorizzazione e poi, nella prossima sottosezione, entrerai in funzioni R più specifiche e popolari per sostituire i loop.
Un esempio di vettorizzazione in R
Torniamo alla nozione di “far crescere i dati”, un modo tipicamente inefficiente di “aggiornare” un data frame.
Per prima cosa, crei una matrice m x n con replicate(m, rnorm(n)) con m=10 vettori colonna di n=10 elementi ciascuno, costruiti con rnorm(n), che crea numeri normali casuali.
Poi la trasformi in un data frame (quindi 10 osservazioni di 10 variabili) ed esegui un'operazione algebrica su ogni elemento usando un for annidato: a ogni iterazione, una funzione sinusoidale incrementa ogni elemento referenziato dai due indici.
Il seguente esempio è un po' artificiale, ma potrebbe rappresentare l'aggiunta di un segnale a un rumore casuale:
# This is a bad loop with 'growing' data
set.seed(42)
m = 10
n = 10
# Create matrix of normal random numbers
mymat <- replicate(m, rnorm(n))
# Transform into data frame
mydframe <- data.frame(mymat)
for (i in 1:m) {
for (j in 1:n) {
mydframe[i, j] <- mydframe[i, j] + 10 * sin(0.75 * pi)
print(mydframe)
}
}
Qui, gran parte del tempo di esecuzione consiste nel copiare e gestire il loop.
Vediamo come appare una soluzione vettorizzata:
set.seed(42)
m = 10
n = 10
mymat <- replicate(m, rnorm(n))
mydframe <- data.frame(mymat)
mydframe <- mydframe + 10 * sin(0.75 * pi)
mydframe
Questo è più semplice: l'ultima riga prende il posto del for annidato. Nota l'uso di set.seed() per garantire che le due implementazioni diano esattamente lo stesso risultato.
Quantifichiamo ora il tempo di esecuzione per le due soluzioni.
Puoi farlo usando comandi di sistema di R, come system.time() a cui si può passare un blocco di codice in questo modo:
Suggerimento: inserisci semplicemente il codice che vuoi valutare tra le parentesi della funzione system.time().
# Insert `system.time()` to measure loop execution
for (i in 1:m) {
for (j in 1:n) {
mydframe[i, j] <- mydframe[i, j] + 10 * sin(0.75 * pi)
}
}
# Add `system.time()` to measure vectorized execution
mydframe <- mydframe + 10 * sin(0.75 * pi)
Nel chunk di codice sopra, fai la scelta di m e n, la creazione della matrice e la sua trasformazione in data frame solo una volta all'inizio, e poi valuti il blocco for rispetto alla “one-liner” della versione vettorizzata con due chiamate separate a system.time().
Vedi che già con un'impostazione minima di m=n=10 la versione vettorizzata è 7 volte più veloce, anche se per valori così bassi è appena rilevante per l'utente.
Le differenze diventano percepibili (su scala umana) se imposti m=n=100, mentre aumentare a 1000 fa sembrare il for loop bloccato per diverse decine di secondi, mentre la forma vettorizzata esegue ancora in un battito di ciglia.
Per m=n=10000 il for loop resta bloccato per più di un minuto, mentre la versione vettorizzata richiede 2,54 secondi. Naturalmente, queste misure vanno prese con cautela e dipendono da configurazione hardware e software, evitando magari di sovraccaricare il laptop con decine di tab del browser aperte e varie applicazioni in background; ma sono illustrative delle differenze.
A onor del vero, l'aumento di m e n influisce pesantemente anche sulla generazione della matrice, come puoi facilmente vedere inserendo un'altra chiamata a system.time() attorno alla funzione di replicazione.
Sei invitato a giocare con m e n per vedere come cambia il tempo di esecuzione, tracciandolo in funzione del prodotto m x n. Questo è l'indicatore rilevante, perché esprime la dimensione delle matrici create. Quantifica quindi anche il numero di iterazioni necessarie per completare il compito tramite il for an-nidato.
Questo è dunque un esempio di vettorizzazione. Ma ce ne sono molti altri. In R News, una newsletter per il progetto R a pagina 46, ci sono funzioni molto efficienti per calcolare somme e medie su determinate dimensioni in array o matrici, come: rowSums(), colSums(), rowMeans() e colMeans().
Inoltre, la newsletter menziona anche che “...le funzioni della famiglia ‘apply’, denominate [s,l,m,t]apply, sono fornite per applicare un'altra funzione agli elementi/dimensioni degli oggetti. Queste funzioni ‘apply’ forniscono una sintassi compatta per compiti talvolta piuttosto complessi, più leggibile e più veloce di loop scritti male”.
La famiglia apply: solo loop nascosti?
La famiglia molto ricca e potente delle funzioni apply è composta da funzioni intrinsecamente vettorizzate. Se a prima vista non sembrano contenere alcun loop, questa caratteristica diventa evidente quando guardi sotto il cofano.
Il comando apply, o meglio la famiglia di comandi, appartiene al pacchetto base di R. È popolata da un numero di funzioni ([s,l,m,r, t,v]apply) per manipolare “fette” di dati sotto forma di matrici o array in modo ripetuto, permettendo di attraversare i dati ed evitando l'uso esplicito di costrutti di loop. Le funzioni agiscono su una matrice o un array in ingresso e applicano una funzione nominata scelta con uno o più argomenti opzionali.
Nota che è per questo che rientrano nelle cosiddette “funzionali” (come nella pagina di advanced R di Hadley Wickham).
La funzione chiamata può essere una funzione di aggregazione, come una semplice media, o un'altra funzione di trasformazione o di sottoinsieme.
Va notato che l'applicazione di queste funzioni non porta necessariamente a un'esecuzione più rapida: le differenze non sono enormi; piuttosto evita la scrittura di loop macchinosi, riducendo le possibilità di errore.
Le funzioni della famiglia sono: apply(), sapply(), lapply(), mapply(), rapply(), tapply(), vapply().
Ma quando e come dovresti usarle?
Vale la pena notare che un pacchetto come plyr copre la funzionalità della famiglia; benché ricordarle tutte senza ricorrere alla documentazione ufficiale possa essere difficile, queste funzioni costituiscono comunque la base di combinazioni più complesse e utili.
Le prime tre sono le più usate:
apply()
Vuoi applicare una certa funzione alle righe (indice “1”) o alle colonne (indice “2”) di una matrice. Vedi questa pagina per maggiori informazioni.
lapply()
Vuoi applicare una certa funzione a ogni elemento di una lista e ottenere come risultato una lista (da qui la “l” nel nome). Approfondisci sulla documentazione R.
sapply()
Vuoi applicare una certa funzione a ogni elemento di una lista ma desideri ottenere un vettore invece di una lista. Vuoi saperne di più? Visita la pagina di documentazione R.
Funzioni correlate sono sweep(), by() e aggregate() e sono talvolta usate insieme agli elementi della famiglia apply().
Limito la discussione di questo post alla funzione apply() (un'analisi più estesa potrebbe essere oggetto di un futuro post).
Data una matrice M, la chiamata apply(M,1,fun) o apply(M, 2,fun) applicherà la funzione specificata fun alle righe di M, se è specificato 1; o alle colonne di M, quando è specificato 2. Questo argomento numerico è chiamato “margin” ed è limitato ai valori 1 e 2 perché la funzione opera su una matrice. Tuttavia, potresti avere un array con fino a 8 dimensioni.
La funzione può essere qualsiasi valida funzione R, ma può essere anche una funzione definita dall'utente (UDF), persino scritta all'interno di apply(), il che è comodo.
apply(): un esempio
# define matrix `mymat` by replicating the sequence `1:5` for `4` times and transforming into a matrix
mymat <- matrix(rep(seq(5), 4), ncol = 5)
# `mymat` sum on rows
apply(mymat, 1, sum)
# `mymat` sum on columns
apply(mymat, 2, sum)
# With user defined function within the apply that adds any number `y` to the sum of the row
# `y` is set at `4.5`
apply(mymat, 1, function(x, y)
sum(x) + y, y = 4.5)
# Or produce a summary column wise for each column
apply(mymat, 2, function(x, y)
summary(mymat))
Usi spesso i data frame: in questo caso particolare, devi assicurarti che i dati abbiano lo stesso tipo, altrimenti potrebbero verificarsi conversioni forzate di tipo, probabilmente non desiderate. Per esempio, in un data frame misto testo e numeri, i dati numerici verranno convertiti in stringhe o caratteri.
Considerazioni finali su uso e alternative ai loop in R
Questo percorso ci ha portati dai costrutti fondamentali dei loop usati in programmazione alla (base) nozione di vettorizzazione e a un esempio d'uso di una delle funzioni della famiglia apply(), molto frequenti in R.
In termini di controllo del flusso del codice, ti sei occupato solo di loop: for, while, repeat e dei modi per interromperli e uscirne.
Dal momento che le ultime sottosezioni suggeriscono che i loop in R andrebbero evitati, potresti chiederti perché mai dovresti impararli.
Secondo me, dovresti imparare queste strutture perché:
- È probabile che R non sarà l'unico linguaggio che userai in data science o altrove, e comprendere costrutti generali come i loop è una cosa utile da mettere nel tuo bagaglio di competenze. La sintassi dei loop può variare a seconda del linguaggio, ma una volta padroneggiati in uno, li applicherai facilmente a qualsiasi altro linguaggio che incontrerai.
- L'universo di R è enorme ed è molto difficile, se non impossibile, conoscere tutte le funzioni esistenti. Ci sono molti modi per fare le cose, alcuni più efficienti o eleganti di altri, e la tua curva di apprendimento sarà incrementale; usandole, inizierai a chiederti modi più efficienti per fare le cose e alla fine arriverai a funzioni di cui non avevi mai sentito parlare. Nel frattempo che leggi, saranno stati sviluppati nuovi pacchetti, altri rilasciati: la probabilità di conoscerli tutti è bassa, a meno di passare tutto il tempo come specialista R.
- Infine, almeno quando non tratti dataset particolarmente ad alta dimensionalità, una soluzione con loop è facile da scrivere e leggere. E magari, come data scientist, ti viene chiesto di prototipare un lavoro estemporaneo che semplicemente funzioni. Forse non ti interessano modi sofisticati o eleganti per ottenere un risultato. Forse, nel flusso di analisi, devi solo mostrare competenza di dominio e concentrarti sul contenuto. Qualcun altro, probabilmente uno specialista back-end, si occuperà della fase di produzione (e forse codificherà in un linguaggio diverso!)
Se dopo aver letto questo tutorial i for loop in R non sono più una sfida per te, potresti considerare di seguire il corso R intermedio di DataCamp. Questo corso rafforzerà le tue conoscenze degli argomenti di R intermedio con una serie di nuovi e divertenti esercizi. Se però i loop non hanno più segreti per te, il corso di DataCamp Writing Functions in R, tenuto da Hadley e Charlotte Wickham, potrebbe interessarti. Dai un'occhiata anche al tutorial For Loops in R.
Esegui e modifica il codice da questo tutorial online
Esegui codiceDomande frequenti sui loop in R
Cosa sono i loop in R e perché sono utili?
I loop sono un modo per ripetere più volte un blocco di codice. Sono utili quando vuoi eseguire la stessa azione su più punti dati o oggetti, oppure quando vuoi ripetere un'azione finché non si verifica una certa condizione.
Quali sono i diversi tipi di loop in R?
In R ci sono due tipi principali di loop: i for loop e i while loop.
- I for loop ripetono un blocco di codice per ogni elemento in una lista o sequenza. Per esempio, puoi usare un for loop per stampare tutti gli elementi di un vettore o per applicare una funzione a ogni elemento di una lista.
- I while loop ripetono un blocco di codice finché una certa condizione è vera. Per esempio, puoi usare un while loop per continuare a chiedere un input all'utente finché non inserisce una risposta valida.
Come interrompo un loop in R?
Per interrompere un loop in R, puoi usare l'istruzione break. Questo uscirà immediatamente dal loop e proseguirà con il resto del codice.
Come salto alla prossima iterazione di un loop in R?
Per passare alla prossima iterazione di un loop in R, puoi usare l'istruzione next. Questa salterà il resto dell'iterazione corrente e passerà alla successiva.
Posso inserire loop dentro altri loop in R?
Sì, in R puoi inserire loop dentro altri loop. Questo si chiama "annidare" i loop. Per esempio, puoi usare un for loop dentro un altro for loop per iterare sugli elementi di una matrice.

