R Language
Kontroluj struktury przepływu
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
-
[index]
to nazwa, która ma dokładnie jedną wartość[domain]
na każdą iterację pętli. -
[domain]
jest wektorem wartości, nad którymi należy iterować. -
[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:
- Używanie źle zoptymalizowanej pętli
- Używanie dobrze zoptymalizowanej dla pętli
- Używając
*apply
rodzinę funkcji - 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
}