R Language
Controle stroomstructuren
Zoeken…
Opmerkingen
For-lussen zijn een stroombesturingsmethode voor het herhalen van een taak of een reeks taken over een domein. De kernstructuur van een for-lus is
for ( [index] in [domain]){
[body]
}
Waar
-
[index]
is een naam die exact één waarde van[domain]
over elke iteratie van de lus. -
[domain]
is een vector van waarden waarover moet worden herhaald. -
[body]
is de set instructies die op elke iteratie moet worden toegepast.
Overweeg als een triviaal voorbeeld het gebruik van een for-lus om de cumulatieve som van een vector van waarden te verkrijgen.
x <- 1:4
cumulative_sum <- 0
for (i in x){
cumulative_sum <- cumulative_sum + x[i]
}
cumulative_sum
Structuur van For Loops optimaliseren
For-lussen kunnen handig zijn voor het herdenken van taken en het uitvoeren van taken. Indien niet zorgvuldig geconstrueerd, kunnen ze echter zeer langzaam worden uitgevoerd in vergelijking met de voorkeur die wordt gebruikt in de van apply
zijnde familie van functies. Desalniettemin zijn er een handvol elementen die u kunt opnemen in uw for-lusconstructie om de lus te optimaliseren. In veel gevallen zal een goede constructie van de for-lus rekenefficiëntie opleveren die zeer dicht bij die van een toepassingsfunctie ligt.
Een 'correct geconstrueerde' lus is gebaseerd op de kernstructuur en bevat een verklaring waarin het object wordt aangegeven dat elke iteratie van de lus vastlegt. Dit object moet zowel een klasse als een lengte hebben.
[output] <- [vector_of_length]
for ([index] in [length_safe_domain]){
[output][index] <- [body]
}
Laten we ter illustratie een lus schrijven om elke waarde in een numerieke vector te kwadrateren (dit is slechts een triviaal voorbeeld ter illustratie. De 'juiste' manier om deze taak te x_squared <- x^2
is 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
}
Merk nogmaals op dat we eerst een bakje voor de uitvoer x_squared
en de klasse "numeriek" hebben gegeven met dezelfde lengte als x
. Bovendien hebben we een "lengte veilig domein" verklaard met behulp van de functie seq_along
. seq_along
genereert een vector van indices voor een object dat geschikt is voor gebruik in lussen. Hoewel het intuïtief lijkt te zijn om te gebruiken for (i in 1:length(x))
, als x
0 lengte heeft, probeert de lus het domein van 1:0
te herhalen, wat resulteert in een fout (de 0e index is niet gedefinieerd in R ).
Receptacle-objecten en veilige lengte-domeinen worden intern afgehandeld door de apply
familie van functies en gebruikers worden aangemoedigd om de apply
benadering zoveel mogelijk te gebruiken in plaats van for-loops. Echter, indien correct geconstrueerd, kan een for-lus af en toe een grotere codehelderheid bieden met minimaal verlies aan efficiëntie.
Vectoriseren voor lussen
For-lussen kunnen vaak een handig hulpmiddel zijn bij het conceptualiseren van de taken die binnen elke iteratie moeten worden voltooid. Wanneer de lus volledig is ontwikkeld en geconceptualiseerd, kan het voordelen bieden om de lus in een functie te veranderen.
In dit voorbeeld zullen we een for-lus ontwikkelen om het gemiddelde van elke kolom in de mtcars
gegevensset te berekenen (nogmaals, een triviaal voorbeeld omdat dit zou kunnen worden bereikt via de functie colMeans
).
column_mean_loop <- vector("numeric", length(mtcars))
for (k in seq_along(mtcars)){
column_mean_loop[k] <- mean(mtcars[[k]])
}
De for-lus kan worden omgezet in een toepassingsfunctie door de body van de lus als een functie te herschrijven.
col_mean_fn <- function(x) mean(x)
column_mean_apply <- vapply(mtcars, col_mean_fn, numeric(1))
En om de resultaten te vergelijken:
identical(column_mean_loop,
unname(column_mean_apply)) #* vapply added names to the elements
#* remove them for comparison
Het voordeel van de gevectoriseerde vorm is dat we enkele regels code konden verwijderen. De mechanismen voor het bepalen van de lengte en het type van het uitvoerobject en het itereren over een veilig lengte-domein worden voor ons afgehandeld door de Apply-functie. Bovendien is de toepassingsfunctie een beetje sneller dan de lus. Het snelheidsverschil is in menselijke termen vaak te verwaarlozen, afhankelijk van het aantal iteraties en de complexiteit van het lichaam.
Basis voor lusconstructie
In dit voorbeeld berekenen we de gekwadrateerde afwijking voor elke kolom in een gegevensframe, in dit geval de mtcars
.
Optie A: gehele getalindex
squared_deviance <- vector("list", length(mtcars))
for (i in seq_along(mtcars)){
squared_deviance[[i]] <- (mtcars[[i]] - mean(mtcars[[i]]))^2
}
squared_deviance
is een lijst met 11 elementen, zoals verwacht.
class(squared_deviance)
length(squared_deviance)
Optie B: tekenindex
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
}
Wat als we een data.frame
als resultaat willen? Welnu, er zijn veel opties om een lijst om te zetten in andere objecten. In dit geval is het opslaan van de for
resultaten in een data.frame
misschien wel de eenvoudigste.
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
Het resultaat zal dezelfde gebeurtenis zijn, hoewel we de tekenoptie (B) gebruiken.
Optimale constructie van een For Loop
Om het effect van goed voor lusconstructie te illustreren, zullen we het gemiddelde van elke kolom op vier verschillende manieren berekenen:
- Een slecht geoptimaliseerde lus gebruiken
- Met behulp van een goed geoptimaliseerd voor lus
- Gebruik een
*apply
familie van functies*apply
- De functie
colMeans
Elk van deze opties wordt in code weergegeven; een vergelijking van de berekeningstijd om elke optie uit te voeren zal getoond worden; en ten slotte zal een bespreking van de verschillen worden gegeven.
Slecht geoptimaliseerd voor lus
column_mean_poor <- NULL
for (i in 1:length(mtcars)){
column_mean_poor[i] <- mean(mtcars[[i]])
}
Goed geoptimaliseerd voor lus
column_mean_optimal <- vector("numeric", length(mtcars))
for (i in seq_along(mtcars)){
column_mean_optimal <- mean(mtcars[[i]])
}
vapply
functie
column_mean_vapply <- vapply(mtcars, mean, numeric(1))
colMeans
functie
column_mean_colMeans <- colMeans(mtcars)
Efficiëntie vergelijking
De resultaten van het benchmarken van deze vier benaderingen worden hieronder weergegeven (code niet weergegeven)
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
Merk op dat de geoptimaliseerde for
lus de slecht geconstrueerde voor lus uitschakelde. De slecht geconstrueerde lus maakt de lengte van het uitvoerobject constant groter en bij elke verandering van de lengte evalueert R opnieuw de klasse van het object.
Een deel van deze overhead wordt verwijderd door de geoptimaliseerde voor lus door het type uitvoerobject en de lengte ervan aan te geven voordat de lus wordt gestart.
In dit voorbeeld verdubbelt het gebruik van een vapply
functie echter de vapply
, grotendeels omdat we R hebben verteld dat het resultaat numeriek moest zijn (als een resultaat niet numeriek was, zou een fout worden geretourneerd).
Het gebruik van de functie colMeans
is iets langzamer dan de vapply
functie. Dit verschil is te wijten aan enkele foutcontroles in colMeans
en voornamelijk aan de as.matrix
conversie (omdat mtcars
een data.frame
) die niet in de vapply
functie zijn uitgevoerd.
The Other Looping Constructs: while and repeat
R biedt twee extra lusconstructies, while
en repeat
, die typisch worden gebruikt in situaties waarin het aantal vereiste iteraties onbepaald is.
De while
lus
De algemene vorm van een while
lus is als volgt,
while (condition) {
## do something
## in loop body
}
waarbij de condition
wordt geëvalueerd voorafgaand aan het binnengaan van het luslichaam. Als de condition
TRUE
, wordt de code in de body van de lus uitgevoerd en wordt dit proces herhaald totdat de condition
geëvalueerd als FALSE
(of een instructie break
wordt bereikt; zie hieronder). In tegenstelling tot de for
lus, moet, als een while
lus een variabele gebruikt om incrementele iteraties uit te voeren, de variabele vooraf worden gedeclareerd en geïnitialiseerd en moet deze binnen de lus worden bijgewerkt. De volgende lussen vervullen bijvoorbeeld dezelfde taak:
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
In de while
lus hierboven is de lijn i <- i + 1
nodig om een oneindige lus te voorkomen.
Bovendien is het mogelijk om een while
lus te beëindigen met een oproep om vanuit de body van de lus te break
:
iter <- 0
while (TRUE) {
if (runif(1) < 0.25) {
break
} else {
iter <- iter + 1
}
}
iter
#[1] 4
In dit voorbeeld is de condition
altijd TRUE
, dus de enige manier om de lus te beëindigen, is met een oproep om in het lichaam te break
. Merk op dat de uiteindelijke waarde van iter
afhangt van de status van uw PRNG wanneer dit voorbeeld wordt uitgevoerd, en bij elke uitvoering van de code (in wezen) verschillende resultaten zou moeten opleveren.
De repeat
De repeat
is in wezen hetzelfde als while (TRUE) { ## something }
en heeft de volgende vorm:
repeat ({
## do something
## in loop body
})
De extra {}
zijn niet vereist, maar de ()
wel. Herschrijven van het vorige voorbeeld met repeat
,
iter <- 0
repeat ({
if (runif(1) < 0.25) {
break
} else {
iter <- iter + 1
}
})
iter
#[1] 2
Meer break
Het is belangrijk op te merken dat break
alleen de onmiddellijk insluitende lus zal beëindigen . Dat wil zeggen, het volgende is een oneindige lus:
while (TRUE) {
while (TRUE) {
cat("inner loop\n")
break
}
cat("outer loop\n")
}
Met een beetje creativiteit is het echter mogelijk om volledig vanuit een geneste lus te breken. Beschouw als voorbeeld de volgende uitdrukking, die in zijn huidige toestand oneindig zal doorlopen:
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))
}
}
}
Een mogelijkheid is om te erkennen dat, in tegenstelling tot de break
, de return
uitdrukking heeft wel de mogelijkheid om de controle over meerdere niveaus van het omsluiten van loops terug te keren. Aangezien return
echter alleen geldig is wanneer deze binnen een functie wordt gebruikt, kunnen we break
niet eenvoudigweg vervangen door return()
hierboven, maar moeten we ook de gehele expressie als anonieme functie omwikkelen:
(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))
}
}
}
})()
Als alternatief kunnen we een dummy-variabele ( exit
) maken voorafgaand aan de uitdrukking en deze activeren via <<-
vanuit de binnenste lus wanneer we klaar zijn om te beëindigen:
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
}