Ricerca…


Osservazioni

I cicli For sono un metodo di controllo del flusso per la ripetizione di un'attività o un insieme di attività su un dominio. La struttura di base di un ciclo for è

for ( [index] in [domain]){
  [body]
}

Dove

  1. [index] è un nome che prende esattamente un valore di [domain] su ogni iterazione del ciclo.
  2. [domain] è un vettore di valori su cui eseguire iterazioni.
  3. [body] è l'insieme di istruzioni da applicare a ogni iterazione.

Come esempio banale, si consideri l'uso di un ciclo for per ottenere la somma cumulativa di un vettore di valori.

x <- 1:4
cumulative_sum <- 0
for (i in x){
  cumulative_sum <- cumulative_sum + x[i]
}
cumulative_sum

Ottimizzazione della struttura dei cicli For

I loop possono essere utili per concettualizzare ed eseguire attività da ripetere. Se non sono costruiti con cura, tuttavia, possono essere molto lenti da eseguire rispetto all'utilizzo preferito della famiglia di funzioni apply . Tuttavia, ci sono una manciata di elementi che puoi includere nella costruzione del ciclo for per ottimizzare il ciclo. In molti casi, una buona costruzione del ciclo for produrrà un'efficienza computazionale molto vicina a quella di una funzione apply.

Un 'costruito in modo appropriato' per le build di loop sulla struttura principale e include una dichiarazione che dichiara l'oggetto che catturerà ogni iterazione del ciclo. Questo oggetto dovrebbe avere sia una classe sia una lunghezza dichiarate.

[output] <- [vector_of_length]
for ([index] in [length_safe_domain]){
  [output][index] <- [body]
}

Per illustrare, scriviamo un ciclo per quadrare ogni valore in un vettore numerico (questo è un esempio banale per solo illustrazione. Il modo "corretto" per completare questa attività sarebbe x_squared <- x^2 ).

x <- 1:100
x_squared <- vector("numeric", length = length(x))
for (i in seq_along(x)){
  x_squared[i] <- x[i]^2
}

Di nuovo, notiamo che prima abbiamo dichiarato un ricettacolo per l'output x_squared e gli abbiamo dato la classe "numeric" con la stessa lunghezza di x . Inoltre, abbiamo dichiarato un "dominio sicuro per lunghezza" usando la funzione seq_along . seq_along genera un vettore di indici per un oggetto che è adatto per l'uso nei cicli. Anche se sembra intuitivo usare for (i in 1:length(x)) , se x ha lunghezza 0, il ciclo tenterà di scorrere il dominio di 1:0 , causando un errore (l'indice 0th non è definito in R ).

Gli oggetti ricettacolo e i domini sicuri di lunghezza sono gestiti internamente dalla famiglia di funzioni apply e gli utenti sono incoraggiati ad adottare l'approccio apply al posto dei loop il più possibile. Tuttavia, se costruito correttamente, un ciclo for può occasionalmente fornire una maggiore chiarezza del codice con una minima perdita di efficienza.

Vettorizzazione per cicli

Spesso i loop possono essere uno strumento utile per concettualizzare le attività che devono essere completate all'interno di ogni iterazione. Quando il ciclo è completamente sviluppato e concettualizzato, ci possono essere dei vantaggi nel trasformare il ciclo in una funzione.

In questo esempio, svilupperemo un ciclo for per calcolare la media di ogni colonna nel set di dati mtcars (di nuovo, un esempio banale in quanto potrebbe essere ottenuto tramite la funzione colMeans ).

column_mean_loop <- vector("numeric", length(mtcars))
for (k in seq_along(mtcars)){
  column_mean_loop[k] <- mean(mtcars[[k]])
}

Il ciclo for può essere convertito in una funzione apply riscrivendo il corpo del loop come una funzione.

col_mean_fn <- function(x) mean(x)
column_mean_apply <- vapply(mtcars, col_mean_fn, numeric(1))

E per confrontare i risultati:

identical(column_mean_loop, 
          unname(column_mean_apply)) #* vapply added names to the elements
                                     #* remove them for comparison

I vantaggi della forma vettoriale sono che siamo stati in grado di eliminare alcune righe di codice. I meccanismi per determinare la lunghezza e il tipo dell'oggetto di output e iterare su un dominio sicuro di lunghezza vengono gestiti per noi dalla funzione apply. Inoltre, la funzione apply è un po 'più veloce del ciclo. La differenza di velocità è spesso trascurabile in termini umani a seconda del numero di iterazioni e della complessità del corpo.

Basic For Loop Construction

In questo esempio calcoleremo la deviazione al quadrato per ogni colonna in un frame di dati, in questo caso il mtcars .

Opzione A: indice intero

squared_deviance <- vector("list", length(mtcars))
for (i in seq_along(mtcars)){
  squared_deviance[[i]] <- (mtcars[[i]] - mean(mtcars[[i]]))^2
}

squared_deviance è un elenco di 11 elementi, come previsto.

class(squared_deviance)
length(squared_deviance)

Opzione B: indice dei caratteri

squared_deviance <- vector("list", length(mtcars))
Squared_deviance <- setNames(squared_deviance, names(mtcars))
for (k in names(mtcars)){
  squared_deviance[[k]] <- (mtcars[[k]] - mean(mtcars[[k]]))^2
}

Cosa succede se vogliamo un data.frame come risultato? Bene, ci sono molte opzioni per trasformare una lista in altri oggetti. Tuttavia, e forse la più semplice in questo caso, sarà quello di memorizzare il for risultati in un data.frame .

squared_deviance <- mtcars #copy the original
squared_deviance[TRUE]<-NA  #replace with NA or do squared_deviance[,]<-NA
for (i in seq_along(mtcars)){
  squared_deviance[[i]] <- (mtcars[[i]] - mean(mtcars[[i]]))^2
}
dim(squared_deviance)
[1] 32 11

Il risultato sarà lo stesso evento anche se usiamo l'opzione carattere (B).

Costruzione ottimale di un ciclo For

Per illustrare l'effetto del buono per la costruzione del ciclo, calcoleremo la media di ciascuna colonna in quattro modi diversi:

  1. Utilizzo di un ciclo non ottimizzato per il ciclo
  2. Usando un pozzo ottimizzato per ciclo
  3. Usando un *apply famiglia di funzioni
  4. Utilizzando la funzione colMeans

Ciascuna di queste opzioni sarà mostrata nel codice; verrà mostrato un confronto del tempo di calcolo per eseguire ciascuna opzione; e infine verrà data una discussione sulle differenze.

Non ottimizzato per il ciclo

column_mean_poor <- NULL
for (i in 1:length(mtcars)){
  column_mean_poor[i] <- mean(mtcars[[i]])
}

Ben ottimizzato per il ciclo

column_mean_optimal <- vector("numeric", length(mtcars))
for (i in seq_along(mtcars)){
  column_mean_optimal <- mean(mtcars[[i]])
}

Funzione vapply

column_mean_vapply <- vapply(mtcars, mean, numeric(1))

Funzione colMeans

column_mean_colMeans <- colMeans(mtcars)

Confronto di efficienza

Di seguito sono riportati i risultati del benchmarking di questi quattro approcci (codice non visualizzato)

Unit: microseconds
     expr     min       lq     mean   median       uq     max neval  cld
     poor 240.986 262.0820 287.1125 275.8160 307.2485 442.609   100    d
  optimal 220.313 237.4455 258.8426 247.0735 280.9130 362.469   100   c 
   vapply 107.042 109.7320 124.4715 113.4130 132.6695 202.473   100 a   
 colMeans 155.183 161.6955 180.2067 175.0045 194.2605 259.958   100  b

Si noti che il ciclo ottimizzato for loop ha eliminato il loop mal progettato. Il ciclo mal costruito per aumentare costantemente la lunghezza dell'oggetto di output e ad ogni cambio della lunghezza, R sta rivalutando la classe dell'oggetto.

Alcune di queste spese generali vengono rimosse dal ciclo ottimizzato for dichiarando il tipo di oggetto di output e la sua lunghezza prima di iniziare il ciclo.

In questo esempio, tuttavia, l'uso di una funzione vapply raddoppia l'efficienza computazionale, in gran parte perché abbiamo detto a R che il risultato doveva essere numerico (se qualsiasi risultato non fosse numerico, sarebbe stato restituito un errore).

L'uso della funzione colMeans è un tocco più lento della funzione vapply . Questa differenza è attribuibile ad alcuni controlli di errore eseguiti in colMeans e principalmente alla conversione as.matrix (perché mtcars è un data.frame ) che non sono stati eseguiti nella funzione vapply .

The Other Looping Constructs: while and repeat

R fornisce due costrutti di loop aggiuntivi, while e repeat , che sono tipicamente usati in situazioni in cui il numero di iterazioni richieste è indeterminato.


Il ciclo while

La forma generale di un ciclo while è la seguente,

while (condition) {
    ## do something
    ## in loop body
}

dove la condition viene valutata prima di entrare nel corpo del ciclo. Se la condition valutata su TRUE , il codice all'interno del corpo del loop viene eseguito e questo processo si ripete finché la condition valutata su FALSE (o viene raggiunta un'istruzione di break , vedere di seguito). A differenza del ciclo for , se un ciclo while utilizza una variabile per eseguire iterazioni incrementali, la variabile deve essere dichiarata e inizializzata in anticipo e deve essere aggiornata all'interno del corpo del ciclo. Ad esempio, i seguenti cicli eseguono lo stesso compito:

for (i in 0:4) {
    cat(i, "\n")
}
# 0 
# 1 
# 2 
# 3 
# 4 

i <- 0
while (i < 5) {
    cat(i, "\n")
    i <- i + 1
}
# 0 
# 1 
# 2 
# 3 
# 4 

Nel ciclo while precedente, la riga i <- i + 1 è necessaria per impedire un loop infinito.


Inoltre, è possibile terminare un ciclo while con una chiamata per break interno del corpo del loop:

iter <- 0
while (TRUE) {
    if (runif(1) < 0.25) {
        break
    } else {
        iter <- iter + 1
    }
}
iter
#[1] 4

In questo esempio, la condition è sempre TRUE , quindi l'unico modo per terminare il ciclo è con una chiamata da break all'interno del corpo. Si noti che il valore finale iter dipenderà dallo stato del PRNG quando viene eseguito questo esempio e dovrebbe produrre risultati diversi (essenzialmente) ogni volta che viene eseguito il codice.


Il ciclo repeat

Il costrutto di repeat è essenzialmente lo stesso di while (TRUE) { ## something } , e ha il seguente formato:

repeat ({
    ## do something
    ## in loop body
})

I {} extra non sono richiesti, ma i () sono. Riscrivere l'esempio precedente usando repeat ,

iter <- 0
repeat ({
    if (runif(1) < 0.25) {
        break
    } else {
        iter <- iter + 1
    }
})
iter
#[1] 2 

Altro in break

È importante notare che l' break interromperà solo il ciclo immediatamente racchiuso . Cioè, il seguente è un ciclo infinito:

while (TRUE) {
    while (TRUE) {
        cat("inner loop\n")
        break
    }
    cat("outer loop\n")
}

Con un po 'di creatività, tuttavia, è possibile rompere interamente da un ciclo annidato. Ad esempio, prendi in considerazione la seguente espressione, che, nel suo stato corrente, si muoverà all'infinito:

while (TRUE) {
    cat("outer loop body\n")
    while (TRUE) {
        cat("inner loop body\n")
        x <- runif(1)
        if (x < .3) {
            break
        } else {
            cat(sprintf("x is %.5f\n", x))
        }
    }
}

Una possibilità consiste nel riconoscere che, a differenza di break , il return espressione non ha la capacità di restituire il controllo su più livelli di cicli racchiusi. Tuttavia, poiché return è valido solo se utilizzato all'interno di una funzione, non possiamo semplicemente sostituire break with return() sopra, ma anche avvolgere l'intera espressione come una funzione anonima:

(function() {
    while (TRUE) {
        cat("outer loop body\n")
        while (TRUE) {
            cat("inner loop body\n")
            x <- runif(1)
            if (x < .3) {
                return()
            } else {
                cat(sprintf("x is %.5f\n", x))
            }
        }
    }
})()

In alternativa, possiamo creare una variabile dummy ( exit ) prima dell'espressione e attivarla tramite <<- dal loop interno quando siamo pronti per terminare:

exit <- FALSE
while (TRUE) {
    cat("outer loop body\n")
    while (TRUE) {
        cat("inner loop body\n")
        x <- runif(1)
        if (x < .3) {
            exit <<- TRUE
            break
        } else {
            cat(sprintf("x is %.5f\n", x))
        }
    }
    if (exit) break
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow