Szukaj…


Uwagi

Pętle to metoda kontroli przepływu służąca do powtarzania zadania lub zestawu zadań w domenie. Podstawową strukturą pętli for jest

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

Gdzie

  1. [index] to nazwa, która ma dokładnie jedną wartość [domain] na każdą iterację pętli.
  2. [domain] jest wektorem wartości, nad którymi należy iterować.
  3. [body] to zestaw instrukcji do zastosowania przy każdej iteracji.

Jako trywialny przykład rozważ użycie pętli for do uzyskania skumulowanej sumy wektora wartości.

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

Optymalizacja struktury pętli

Pętle mogą być przydatne do konceptualizacji i wykonywania zadań do powtórzenia. Jednak jeśli nie zostaną skonstruowane starannie, mogą być bardzo powolne w porównaniu do preferowanej rodziny funkcji apply . Niemniej jednak istnieje kilka elementów, które można uwzględnić w konstrukcji pętli for w celu optymalizacji pętli. W wielu przypadkach dobra konstrukcja pętli for da wydajność obliczeniową bardzo zbliżoną do wydajności funkcji wprowadzania.

„Prawidłowo skonstruowana” pętla opiera się na strukturze rdzenia i zawiera instrukcję deklarującą obiekt, który będzie przechwytywał każdą iterację pętli. Ten obiekt powinien mieć zarówno deklarowaną klasę, jak i długość.

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

Aby to zilustrować, napiszmy pętlę do kwadratu każdej wartości w wektorze numerycznym (jest to trywialny przykład tylko dla ilustracji. „Prawidłowym” sposobem wykonania tego zadania byłoby 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
}

Ponownie zauważmy, że najpierw zadeklarowaliśmy gniazdo dla wyjścia x_squared i x_squared mu klasę „numeryczną” o tej samej długości co x . Dodatkowo zadeklarowaliśmy „bezpieczną domenę długości” za pomocą funkcji seq_along . seq_along generuje wektor indeksów dla obiektu, który nadaje się do użycia w pętlach. Podczas gdy wydaje się intuicyjne w użyciu for (i in 1:length(x)) , jeśli x ma 0 długości, pętla będzie próbować iterować w domenie 1:0 , co spowoduje błąd (indeks 0 nie jest zdefiniowany w R ).

Obiekty pojemników i domeny bezpieczne długości są obsługiwane wewnętrznie przez rodzinę funkcji apply a użytkownicy są zachęcani do apply podejścia apply zamiast pętli w jak największym stopniu. Jednak, jeśli jest poprawnie zbudowana, pętla for może czasami zapewniać większą przejrzystość kodu przy minimalnej utracie wydajności.

Wektoryzacja dla pętli

Pętle często mogą być przydatnym narzędziem w konceptualizacji zadań, które należy wykonać w ramach każdej iteracji. Gdy pętla jest całkowicie rozwinięta i konceptualizowana, może być korzystne przekształcenie pętli w funkcję.

W tym przykładzie opracujemy pętlę for, aby obliczyć średnią każdej kolumny w mtcars danych mtcars (ponownie, trywialny przykład, ponieważ można to osiągnąć za pomocą funkcji colMeans ).

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

Pętlę for można przekonwertować na funkcję zastosowania, przepisując treść pętli jako funkcję.

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

I aby porównać wyniki:

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

Zalety wektoryzowanej formy polegają na tym, że udało nam się wyeliminować kilka wierszy kodu. Mechanizmy określania długości i typu obiektu wyjściowego oraz iteracji po domenie bezpiecznej dla długości są obsługiwane przez funkcję Apply. Dodatkowo funkcja zastosuj jest trochę szybsza niż pętla. Różnica prędkości jest często nieistotna z ludzkiego punktu widzenia, w zależności od liczby iteracji i złożoności ciała.

Podstawowy do budowy pętli

W tym przykładzie mtcars kwadratowe odchylenie dla każdej kolumny w ramce danych, w tym przypadku mtcars .

Opcja A: indeks liczb całkowitych

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

squared_deviance to lista 11 elementów, zgodnie z oczekiwaniami.

class(squared_deviance)
length(squared_deviance)

Opcja B: indeks znaków

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
}

Co jeśli chcemy w data.frame ? Istnieje wiele opcji przekształcania listy w inne obiekty. Jednakże, a może najprostszy w tym przypadku, będzie przechowywać for wyniki w 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

Rezultatem będzie to samo zdarzenie, chociaż używamy opcji postaci (B).

Optymalna konstrukcja pętli For

Aby zilustrować wpływ dobrej konstrukcji pętli, obliczymy średnią każdej kolumny na cztery różne sposoby:

  1. Używanie źle zoptymalizowanej pętli
  2. Używanie dobrze zoptymalizowanej dla pętli
  3. Używając *apply rodzinę funkcji
  4. Korzystanie z funkcji colMeans

Każda z tych opcji zostanie pokazana w kodzie; zostanie pokazane porównanie obliczeniowego czasu do wykonania każdej opcji; i na koniec zostanie omówiona różnica.

Źle zoptymalizowany pod kątem pętli

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

Dobrze zoptymalizowany pod kątem pętli

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

Funkcja vapply

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

Funkcja colMeans

column_mean_colMeans <- colMeans(mtcars)

Porównanie wydajności

Wyniki analizy porównawczej tych czterech podejść pokazano poniżej (kod nie jest wyświetlany)

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

Zauważ, że zoptymalizowana for pętli usunęła słabo skonstruowaną pętlę dla. Źle skonstruowana pętla for stale zwiększa długość obiektu wyjściowego, a przy każdej zmianie długości R ponownie ocenia klasę obiektu.

Niektóre z tych obciążeń ogólnych są usuwane przez zoptymalizowaną dla pętli deklarację typu obiektu wyjściowego i jego długości przed uruchomieniem pętli.

Jednak w tym przykładzie użycie funkcji vapply podwaja wydajność obliczeniową, głównie dlatego, że powiedzieliśmy R, że wynik musi być liczbowy (jeśli jakikolwiek wynik nie byłby liczbowy, zwracany byłby błąd).

Korzystanie z funkcji colMeans jest o wiele wolniejsze niż funkcja vapply . Różnica ta wynika z niektórych kontroli błędów przeprowadzanych w colMeans a głównie z konwersji as.matrix (ponieważ mtcars jest data.frame ), która nie została wykonana w funkcji vapply .

Inne konstrukcje zapętlające: póki i powtarzaj

R zapewnia dwie dodatkowe konstrukcje zapętlania, while i repeat , które są zwykle stosowane w sytuacjach, w których liczba wymaganych iteracji jest nieokreślona.


while pętla

Ogólną postać while pętla jest następująca:

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

gdzie condition jest oceniany przed wejściem do ciała pętli. Jeśli condition wartość TRUE , wykonywany jest kod wewnątrz ciała pętli, a proces ten powtarza się, dopóki condition zostanie przetworzony na FALSE (lub osiągnięta zostanie instrukcja break ; patrz poniżej). W przeciwieństwie do for pętli, jeśli while pętla wykorzystuje zmienną do wykonywania przyrostowych iteracji zmienna musi być zadeklarowany i zainicjowana z wyprzedzeniem oraz muszą być aktualizowane w ciele pętli. Na przykład następujące pętle realizują to samo zadanie:

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 

W while pętli powyżej linia i <- i + 1 jest konieczne dla zapobiegania nieskończonej pętli.


Dodatkowo, możliwe jest, aby zakończyć się while pętlę o wywołaniu break od wewnątrz ciała pętli:

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

W tym przykładzie condition jest zawsze TRUE , więc jedynym sposobem na zakończenie pętli jest wywołanie break wewnątrz ciała. Zauważ, że końcowa wartość iter będzie zależeć od stanu twojego PRNG, gdy ten przykład zostanie uruchomiony, i powinna dawać różne wyniki (zasadniczo) przy każdym uruchomieniu kodu.


Pętla repeat

Konstrukcja repeat jest zasadniczo taka sama jak while (TRUE) { ## something } i ma następującą postać:

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

Dodatkowe {} nie są wymagane, ale () są. Przepisanie poprzedniego przykładu za pomocą repeat ,

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

Więcej o break

Należy zauważyć, że break spowoduje jedynie zamknięcie bezpośrednio otaczającej pętli . Oznacza to, że następująca pętla jest nieskończona:

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

Przy odrobinie kreatywności można jednak całkowicie zerwać z zagnieżdżonej pętli. Jako przykład rozważmy następujące wyrażenie, które w obecnym stanie zapętla się w nieskończoność:

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))
        }
    }
}

Jedną z możliwości jest, aby uznać, że, w przeciwieństwie do break The return wyrażenie ma możliwości powrotu kontrolę na wielu poziomach obejmujących pętli. Ponieważ return jest poprawne tylko wtedy, gdy jest używane w funkcji, nie możemy po prostu zastąpić break return() powyżej, ale musimy również zawinąć całe wyrażenie jako funkcję anonimową:

(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))
            }
        }
    }
})()

Alternatywnie możemy utworzyć zmienną fikcyjną ( exit ) przed wyrażeniem i aktywować ją za pomocą <<- z wewnętrznej pętli, gdy jesteśmy gotowi do zakończenia:

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
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow