Cursus
Deze tutorial over R-lussen bekijkt de constructies die in R beschikbaar zijn voor herhaling, wanneer je die zou moeten gebruiken, en hoe je alternatieven kunt inzetten, zoals R’s vectorisatiefunctie, om je herhalingstaken efficiënter uit te voeren.
Dit artikel laat een paar voorbeelden van lussen zien om die vervolgens te bekritiseren en af te raden ten gunste van de populairste gevectoriseerde alternatieven uit de rijke set bibliotheken die R biedt.
In het algemeen is het advies van deze R-tutorial over lussen: leer over lussen. Ze geven je een gedetailleerd beeld van wat er op elementair niveau zou moeten gebeuren en helpen je bovendien de data te begrijpen die je manipuleert.
En zodra je lussen goed begrijpt, doe ze dan weg.
Steek je tijd in het leren van gevectoriseerde alternatieven. Dat loont qua efficiëntie.
Om interactief te oefenen, probeer het hoofdstuk over lussen in onze intermediate R-cursus.
Wat zijn lussen?
“Lussen”, “cyclen”, “itereren” of simpelweg instructies herhalen is een oude praktijk die al bestond lang voor de uitvinding van computers. Het is niets meer dan het automatiseren van een proces met meerdere stappen door reeksen acties of ‘batch’-processen te organiseren en de delen te groeperen die herhaald moeten worden.
Alle moderne programmeertalen bieden speciale constructies waarmee je instructies of blokken instructies kunt herhalen.
Grofweg zijn er twee typen van deze speciale constructies of lussen in moderne programmeertalen. Sommige lussen worden een voorgeschreven aantal keren uitgevoerd, gestuurd door een teller of index die bij elke iteratie wordt verhoogd. Dit zijn de for-lussen.
Andere lussen zijn gebaseerd op het ontstaan en de verificatie van een logische voorwaarde. De voorwaarde wordt aan het begin of het einde van de lusconstructie getest. Deze varianten behoren respectievelijk tot de while- of repeat-familie van lussen.
Een introductie tot lussen in R
Volgens de R base-handleiding behoren binnen de besturingsstroomcommando’s de lusconstructies tot for, while en repeat, met de aanvullende clausules break en next.
Onthoud dat besturingsstroomcommando’s de commando’s zijn waarmee een programma tussen alternatieven kan vertakken, of “beslissingen kan nemen”, om zo te zeggen.
Je kunt deze besturingsstroomcommando’s altijd bekijken door ?Control in de RStudio-opdrachtregel aan te roepen.
?Control
Dit stroomdiagram toont de R-lusstructuren:

For-lussen in R
In de volgende secties kijken we van dichtbij naar elk van deze structuren die in de bovenstaande figuur worden getoond. We beginnen links en gaan in de volgende secties geleidelijk naar rechts.
Voor een video-introductie tot for-lussen en een vervolgopdracht, probeer het hoofdstuk For Loop in onze intermediate R-cursus.
Uitleg van for-lussen
Deze lusstructuur, bestaande uit het rechthoekige vak ‘init’ (of initialisatie), de ruitvormige beslissing en het rechthoekige vak i1, wordt een bekend aantal keren uitgevoerd.
In termen van stroomdiagrammen betekenen rechthoekige vakken zoiets als “doe iets dat geen beslissingen impliceert”. Ruiten of diamanten heten “beslissingssymbolen” en vertalen zich dus in vragen met slechts twee mogelijke logische antwoorden, namelijk True (T) of False (F).
Let op dat, om het simpel te houden, andere mogelijke symbolen uit de figuur zijn weggelaten.
Een of meer instructies binnen het initialisatievak worden gevolgd door de evaluatie van de voorwaarde op een variabele die waarden kan aannemen binnen een opgegeven reeks. In de figuur wordt dit voorgesteld door de ruit: de symbolen betekenen “behoort de huidige waarde van variabele v tot de reeks seq?”
Met andere woorden, je test of de huidige waarde van v binnen een opgegeven bereik ligt. Je definieert dit bereik normaal in de initialisatie, met iets als 1:100 om te zorgen dat de lus start.
Als niet aan de voorwaarde wordt voldaan en de uitkomst False is, wordt de lus nooit uitgevoerd. Dit wordt aangegeven door de losse pijl rechts van de for-lusstructuur. Het programma voert dan de eerste instructie uit na het lusblok.
Als aan de voorwaarde is voldaan, wordt een instructie—of een blok instructies—i1 uitgevoerd. En misschien is dit blok instructies weer een lus. In zulke gevallen spreek je van een geneste lus.
Daarna wordt de voorwaarde opnieuw geëvalueerd. Dit wordt aangegeven door de lijnen die van i1 terug naar boven lopen, direct na het initialisatievak. In R—en in Python—kun je dit in gewoon Engels uitdrukken door te vragen of onze variabele tot een bereik van waarden behoort of niet.
Let op dat in andere talen, bijvoorbeeld in C, de voorwaarde explicieter wordt gemaakt met behulp van een logische operator, zoals groter dan, kleiner dan, gelijk aan, …
Hier is een voorbeeld van een simpele for-lus:
# 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)
Het for-blok staat tussen accolades. Die kun je direct na de testvoorwaarde plaatsen of eronder, bij voorkeur gevolgd door een inspringing. Niets hiervan is verplicht, maar de accolades verhogen zeker de leesbaarheid van je code en maken het makkelijk om het lusblok en eventuele fouten erin te spotten.
Let op dat de vector met de kwadraten, usq, is geïnitialiseerd. Dit zou niet nodig zijn in gewone RStudio-code, maar in de opgemaakte versie zou knitr niet compileren omdat er vóór gebruik in de lus geen verwijzing naar de vector wordt gevonden, wat in RStudio een fout oplevert. Ga voor meer informatie over knitr naar de R Markdown-pagina.
For-lussen nesten
Nu je weet dat for-lussen ook genest kunnen worden, vraag je je waarschijnlijk af waarom en wanneer je dit in je code zou gebruiken.
Stel dat je een tweedimensionale array wilt manipuleren door de elementen op specifieke waarden te zetten.
Dan kun je zoiets doen als dit:
# 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
}
}
Tip: voor meer informatie over de functie matrix(), bezoek R Documentation.
i loopt over de rijen en j loopt over de kolommen.Je hebt de welbekende tafels gemaakt die je uit je hoofd zou moeten kennen.
Je kunt ook een geheel getal kiezen en vervolgens een tafel maken naar jouw keuze: je kunt een geheel getal aan één variabele toekennen als de tafel vierkant is of aan twee variabelen als de tafel rechthoekig is. Deze variabele fungeert dan als bovengrens voor de indexen i en 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]
Let op dat je, om te voorkomen dat de gebruiker het scherm overspoelt met enorme tabellen, aan het eind een voorwaarde zet om alleen de eerste 10 x 10 sectie af te drukken als de gebruiker om een geheel getal groter dan 10 heeft gevraagd. Anders wordt een n x n-gedeelte afgedrukt.
De volledige code ziet er zo uit:
# 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-lussen: populair maar niet altijd perfect voor jouw gebruik
De for-lus is veruit het populairst, en de constructie impliceert dat het aantal iteraties vaststaat en vooraf bekend is, zoals in gevallen als “genereer de eerste 200 priemgetallen” of “zet de 10 belangrijkste klanten op een rij”.
Maar wat als je het aantal iteraties niet kent of niet stuurt, en er een of meer voorwaarden kunnen optreden die je niet vooraf kunt voorspellen?
Je wilt bijvoorbeeld het aantal klanten tellen die wonen in een gebied met een bepaalde postcode, of het aantal kliks op een banner op een webpagina in de laatste twee dagen, of soortgelijke onvoorziene gebeurtenissen.
In zulke gevallen kunnen de while-lus en diens familielid repeat uitkomst bieden…
While-lussen
De while-lus, in het midden van de figuur hierboven, bestaat net als tevoren uit een initialisatieblok, gevolgd door een logische voorwaarde. Deze voorwaarde wordt doorgaans uitgedrukt door de vergelijking tussen een stuurvariabele en een waarde, met groter dan, kleiner dan of gelijk aan, maar elke expressie die evalueert naar een logische waarde, True of False, is legitiem.
Als het resultaat False (F) is, wordt de lus nooit uitgevoerd, zoals aangegeven door de losse pijl rechts in de figuur. Het programma voert dan de eerste instructie uit die het na het lusblok vindt.
Als het True (T) is, wordt de instructie of het blok instructies i1 vervolgens uitgevoerd.
Merk op dat een extra instructie of blok instructies i2 is toegevoegd: dit dient als update voor de stuurvariabele, wat de uitkomst van de voorwaarde aan het begin van de lus kan veranderen, maar dit is niet noodzakelijk. Of misschien wil je een teller verhogen om het aantal uitgevoerde iteraties bij te houden. De iteraties stoppen zodra de voorwaarde false wordt.
De notatie is while(cond) expr, waarbij cond de te testen voorwaarde is en expr een expressie.
In het volgende voorbeeld vraagt de lus de gebruiker met een User Defined Function (UDF) om het juiste antwoord op de vraag over het universum en alles in te voeren. Dit blijft zo doorgaan totdat de gebruiker het antwoord goed heeft:
# 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());
}
Je gebruikt om te beginnen een door de gebruiker gedefinieerde functie om de invoer op te halen vóór je de lus ingaat. Deze lus gaat door zolang het antwoord niet de verwachte 42 is.
Met andere woorden, je doet dit omdat R anders zou klagen over de ontbrekende expressie die de vereiste True of False moest leveren—en in feite kent het ‘response’ niet vóór gebruik in de lus. Je doet dit ook omdat, als je het meteen goed hebt, de lus helemaal niet wordt uitgevoerd.
Repeat-lussen
De repeat-lus staat helemaal rechts in het stroomdiagram hierboven. Deze lus lijkt op de while-lus, maar is zo gemaakt dat de blokken instructies i1 en i2 minstens één keer worden uitgevoerd, ongeacht de uitkomst van de voorwaarde.
In lijn met andere talen zou je deze lus “repeat until” kunnen noemen om te benadrukken dat de instructies i1 en i2 worden uitgevoerd totdat de voorwaarde False (F) blijft of, equivalent, True (T) wordt en zo afsluit; maar in elk geval minimaal één keer.
Als variant op het vorige voorbeeld kun je schrijven:
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");
}
Na de inmiddels bekende invoerfunctie volgt de repeat-lus waarvan het blok ten minste één keer wordt uitgevoerd en die eindigt zodra aan de if-voorwaarde is voldaan.
Let erop dat je binnen de lus een voorwaarde moest instellen waarop je met de clausule break kunt uitstappen. Deze clausule introduceert het idee van stoppen of onderbreken van cycli binnen lussen.
Onderbreken en verlaten van lussen in R
Hoe verlaat je een lus?
Met andere woorden, afgezien van het “natuurlijke” einde van de lus, dat optreedt omdat je ofwel het voorgeschreven aantal iteraties hebt bereikt (for) of omdat je aan een voorwaarde hebt voldaan (while, repeat), kun je de lus stoppen of onderbreken?
En zo ja, hoe?
De instructie break beantwoordt de eerste vraag: je zag dit in het laatste voorbeeld.
Onderbreek je lussen met break
Wanneer de R-interpreter een break tegenkomt, geeft hij de controle door aan de instructie direct na het einde van de lus (als die er is). In het geval van geneste lussen zorgt break er alleen voor dat je de binnenste lus verlaat.
Hier is een voorbeeld.
Deze code definieert een m x n-matrix van nullen en gaat dan een geneste for-lus in om de posities van de matrix te vullen, maar alleen als de twee indexen verschillen. Het doel is een onderdriehoeksmatrix te maken, dat is een matrix waarvan de elementen onder de hoofddiagonaal ongelijk aan nul zijn. De andere blijven onaangeroerd op hun geïnitialiseerde nulwaarde.
Wanneer de indexen gelijk zijn en dus aan de voorwaarde in de binnenste lus, die loopt over de kolomindex j, wordt voldaan, wordt een break uitgevoerd en wordt de binnenste lus onderbroken met een directe sprong naar de instructie na de binnenste lus. Deze instructie is een print()-instructie. Vervolgens gaat de controle naar de buitenste for-voorwaarde (over de rijen, index i), die opnieuw wordt geëvalueerd.
Als de indexen verschillen, wordt de toekenning uitgevoerd en wordt de teller met 1 verhoogd. Aan het eind drukt het programma de teller ctr af, die het aantal toegewezen elementen bevat.
# 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)
Let op dat het misschien wat overdreven voorzichtig is om ook accolades te zetten wanneer ze niet strikt noodzakelijk zijn. Meestal doe je dit om zeker te zijn dat alles wat met een { wordt geopend, ook met een } wordt gesloten. Dus als je een ongelijk aantal { of } ziet, weet je dat er een fout is, al geldt het omgekeerde niet per se!
Het gebruik van next in lussen
next stopt een specifieke iteratie en springt naar de volgende cyclus. In feite springt het naar de evaluatie van de voorwaarde van de huidige lus.
In andere talen vind je misschien het (licht verwarrende) equivalent “continue”, dat hetzelfde betekent: waar je je ook bevindt, spring bij verificatie van de voorwaarde naar de evaluatie van de lus.
Een eenvoudiger voorbeeld om de lus gaande te houden terwijl je een specifieke cyclus overslaat bij het optreden van een voorwaarde is:
m = 20
for (k in 1:m) {
if (!k %% 2)
next
print(k)
}
Deze code drukt alle oneven getallen af binnen het interval 1:m (hier m=20). Met andere woorden, alle gehele getallen behalve die met een niet-nul rest bij deling door 2 (vandaar de modulus-operator %%), zoals gespecificeerd door de if-test, worden afgedrukt.
Getallen met rest nul worden niet afgedrukt, omdat het programma springt naar de evaluatie van de voorwaarde i in 1:m en eventuele volgende instructies negeert. In dit geval wordt print(k) genegeerd.
Gebruik van lussen in R samenvattend
- Probeer zo weinig mogelijk code in de lus te stoppen door zoveel mogelijk instructies erbuiten te halen. Onthoud: alles binnen de lus wordt meerdere keren herhaald en is misschien niet nodig.
- Wees voorzichtig met
repeat: zorg dat er expliciet een beëindiging is ingesteld door een voorwaarde te testen, anders kun je in een oneindige lus belanden. - Het is beter om binnen de lus één of meer functieaanroepen te gebruiken als een lus (te) groot wordt. De functieaanroepen maken het voor andere gebruikers makkelijker om de code te volgen. Maar het gebruik van een geneste for-lus voor matrix- of array-bewerkingen is waarschijnlijk een teken dat je het niet op de beste manier hebt geïmplementeerd voor een matrixgebaseerde taal als R.
- Het is niet aan te raden om een variabele of dataset te “laten groeien” door bij elke iteratie een toekenning te doen. In sommige talen zoals Matlab wordt een waarschuwingsfout gegeven: je kunt doorgaan, maar je wordt uitgenodigd om alternatieven te overwegen. Een typisch voorbeeld volgt in de volgende sectie.
Wanneer gebruik je R-lussen
Allemaal goed en wel, maar wanneer moet je lussen in R gebruiken en wanneer niet?
Elke keer dat een of meer bewerkingen herhaald moeten worden, kan een lus handig zijn.
Je hoeft alleen te specificeren hoe vaak of onder welke voorwaarden die bewerkingen moeten worden uitgevoerd: je kent beginwaarden toe aan een stuurvariabele voor de lus, voert de lus uit en doet, zodra de lus is afgelopen, meestal iets met de resultaten.
Maar wanneer hoor je lussen te gebruiken?
Zou je de gewenste instructie niet gewoon voldoende vaak kunnen dupliceren?
Een vuistregel kan zijn dat als je een actie (zeg) drie keer of vaker moet uitvoeren, een lus je beter van dienst is. Het maakt de code compacter, leesbaarder en onderhoudbaarder en je bespaart typwerk: stel dat je ontdekt dat een bepaalde instructie één keer vaker herhaald moet worden dan aanvankelijk voorzien: in plaats van de volledige instructie opnieuw te schrijven, kun je gewoon de waarde van een variabele in de testvoorwaarde aanpassen.
Toch suggereert de eigenaardigheid van R om lussen helemaal niet te gebruiken(!) wanneer er alternatieven bestaan.
Gelukkig zijn er alternatieven!
R heeft een functie die maar weinig programmeertalen hebben, namelijk vectorisatie.
De alternatieven voor lussen in R
Wat is vectorisatie?
Zoals het woord al aangeeft, is vectorisatie het omzetten van herhaalde bewerkingen op simpele getallen (“scalaren”) in enkele bewerkingen op vectors of matrices. Je hebt hiervan al verschillende voorbeelden gezien in de subsecties hierboven.
Een vector is de elementaire datastructuur in R en is “één entiteit die bestaat uit een verzameling dingen”, volgens de R base-handleiding.
Dus een verzameling getallen is een numerieke vector.
Als je vectors (van gelijke lengte) combineert, krijg je een matrix. Dat kan verticaal of horizontaal, met verschillende R-instructies. In R wordt een matrix dus gezien als een verzameling horizontale of verticale vectors. Bij uitbreiding kun je herhaalde bewerkingen op vectors vectoriseren.
Veel van de bovenstaande lusconstructies kun je impliciet maken door vectorisatie te gebruiken.
Ik zeg “impliciet”, omdat ze niet echt verdwijnen. Op een lager niveau wordt de alternatieve gevectoriseerde vorm vertaald naar code die één of meer lussen bevat in de lagere taal waarin de vorm is geïmplementeerd en gecompileerd (Fortran, C of C++).
Deze zijn verborgen voor de gebruiker en zijn meestal sneller dan de equivalente expliciete R-code, maar tenzij je van plan bent je eigen R-functies te implementeren in een van die talen, is dit volledig transparant voor jou.
Het meest elementaire geval dat je kunt bedenken is het optellen van twee vectors v1 en v2 tot een vector v3, wat element-voor-element kan met een for-lus:
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
Of je kunt ook de “native” gevectoriseerde vorm gebruiken:
v3 = v1 + v2
v3
Let op dat we “native” zeggen omdat R alle rekenkundige operatoren herkent als werkend op vectors en matrices.
Evenzo, voor twee matrices A en B, in plaats van de elementen A[i,j] en B[i,j] op overeenkomstige posities op te tellen, waarvoor je twee indexen i en j moet bijhouden, vertel je R het volgende te doen:
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
En dat is inderdaad heel eenvoudig!
Vectorisatie in R uitgelegd
Waarom zou vectorisatie sneller zijn, gegeven dat het aantal elementaire bewerkingen ogenschijnlijk hetzelfde is?
Dit leg je het best uit door naar de interne werking van R te kijken, wat een apart artikel zou vergen, maar kort samengevat: R is om te beginnen een geïnterpreteerde taal en als zodanig worden alle details over variabeledefinitie afgehandeld door de interpreter. Je hoeft bijvoorbeeld niet te specificeren dat een getal een drijvende-kommawaarde is of geheugen te alloceren met een pointer.
De R-interpreter “begrijpt” deze zaken uit de context terwijl je je commando’s invoert, maar doet dit commando voor commando. Daarom moet hij met zulke definities omgaan elke keer dat je een commando geeft, zelfs als je het herhaalt.
Een compiler daarentegen lost letterlijk alle definities en declaraties op compileertijd op over de volledige code; die wordt vertaald naar compacte en geoptimaliseerde binaire code, vóór je iets uitvoert. Nu, omdat R-functies in een van deze lagere talen zijn geschreven, zijn ze efficiënter.
In de praktijk zou je op laag niveau aanroepen van C of C++ zien, meestal geïmplementeerd in wat een wrapper-code heet.
Ten tweede, in talen die vectorisatie ondersteunen (zoals R of Matlab), werkt elke instructie die een numeriek gegeven gebruikt op een object dat native is gedefinieerd als een vector, zelfs als het maar uit één element bestaat. Dit is de standaard wanneer je bijvoorbeeld een enkele numerieke variabele definieert: de interne representatie in R is altijd een vector, ook al bestaat die uit slechts één getal.
De lussen blijven onder de motorkap bestaan, maar dan op het lagere en veel snellere gecompileerde C/C++-niveau. Het voordeel van een vector is dat de definities door de interpreter slechts één keer worden opgelost, op de hele vector, ongeacht de grootte, in tegenstelling tot een lus op een scalaire waarde, waar definities en allocaties element-voor-element moeten worden gedaan, en dat is trager.
Tot slot maakt werken met het native vectorformaat het mogelijk om zeer efficiënte lineaire-algebaroutines te gebruiken (zoals BLAS of Basic Linear Algebra Subprograms), zodat R bij het uitvoeren van gevectoriseerde instructies op deze efficiënte numerieke routines leunt. De boodschap is dus: verwerk indien mogelijk hele datastructuren binnen één functieaanroep om alle kopieerbewerkingen te vermijden.
Maar genoeg uitweidingen, laten we een algemeen voorbeeld van vectorisatie maken, en in de volgende subsectie duik je in specifiekere en populaire gevectoriseerde R-functies ter vervanging van lussen.
Een voorbeeld van vectorisatie in R
Terug naar het idee van “data laten groeien”, een typisch inefficiënte manier om een data frame te “updaten”.
Eerst maak je een m x n-matrix met replicate(m, rnorm(n)) met m=10 kolomvectors van n=10 elementen elk, geconstrueerd met rnorm(n), dat willekeurige normale getallen maakt.
Vervolgens zet je die om in een data frame (dus 10 observaties van 10 variabelen) en voer je een algebraïsche bewerking uit op elk element met een geneste for-lus: bij elke iteratie verhoogt een sinusoïdale functie elk element waarnaar door de twee indexen wordt verwezen.
Het volgende voorbeeld is wat kunstmatig, maar het kan de toevoeging van een signaal aan wat willekeurige ruis voorstellen:
# 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)
}
}
Hier bestaat het grootste deel van de uitvoeringstijd uit kopiëren en het beheren van de lus.
Kijk eens hoe een gevectoriseerde oplossing eruitziet:
set.seed(42)
m = 10
n = 10
mymat <- replicate(m, rnorm(n))
mydframe <- data.frame(mymat)
mydframe <- mydframe + 10 * sin(0.75 * pi)
mydframe
Dit oogt eenvoudiger: de laatste regel vervangt de geneste for-lus. Let op het gebruik van set.seed() om te zorgen dat de twee implementaties exact hetzelfde resultaat geven.
Laten we nu de uitvoeringstijd voor de twee oplossingen kwantificeren.
Dat kan met R-systeemcommando’s, zoals system.time() waaraan je een codeblok kunt doorgeven zoals dit:
Tip: zet de code die je wilt evalueren tussen de haakjes van de functie 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)
In de bovenstaande code doe je het kiezen van m en n, het maken van de matrix en de omzetting naar een data frame slechts één keer aan het begin, en evalueer je het for-blok tegenover de “one-liner” van de gevectoriseerde versie met de twee afzonderlijke aanroepen van system.time().
Je ziet dat al met een minimale instelling van m=n=10 de gevectoriseerde versie 7 keer sneller is, hoewel dat bij zulke lage waarden voor de gebruiker nauwelijks uitmaakt.
Verschillen worden merkbaar (op menselijke schaal) als je m=n=100 zet, terwijl opschalen naar 1000 de for-lus doet lijken te hangen gedurende vele tientallen seconden, terwijl de gevectoriseerde vorm het nog steeds in een oogwenk doet.
Voor m=n=10000 hangt de for-lus meer dan een minuut, terwijl de gevectoriseerde 2,54 sec nodig heeft. Natuurlijk moet je deze metingen met een korrel zout nemen en hangen ze af van de hardware- en softwareconfiguratie; vermijd bij voorkeur het overladen van je laptop met tientallen open browsertabbladen en meerdere toepassingen op de achtergrond; maar de metingen illustreren de verschillen.
Eerlijkheidshalve: het vergroten van m en n beïnvloedt ook de matrixgeneratie sterk, zoals je gemakkelijk ziet door nog een system.time()-aanroep rond de replicatiefunctie te plaatsen.
Speel gerust met m en n om te zien hoe de uitvoeringstijd verandert, door de uitvoeringstijd te plotten als functie van het product m x n. Dit is de relevante indicator, omdat die de afmeting van de gemaakte matrices uitdrukt. Het kwantificeert dus ook het aantal iteraties dat nodig is om de taak via de nset-ed for-lus te voltooien.
Dit is dus een voorbeeld van vectorisatie. Maar er zijn er veel meer. In R News, een nieuwsbrief voor het R-project op pagina 46, staan zeer efficiënte functies voor het berekenen van sommen en gemiddelden voor bepaalde dimensies in arrays of matrices, zoals: rowSums(), colSums(), rowMeans() en colMeans().
Verder vermeldt de nieuwsbrief ook dat “...de functies in de ‘apply’-familie, genaamd [s,l,m,t]apply, bestaan om een andere functie toe te passen op de elementen/dimensies van objecten. Deze ‘apply’-functies bieden een compacte syntaxis voor soms nogal complexe taken die leesbaarder en sneller is dan slecht geschreven lussen.”
De apply-familie: slechts verborgen lussen?
De zeer rijke en krachtige familie van apply-functies bestaat uit intrinsiek gevectoriseerde functies. Als deze op het eerste gezicht geen lus lijken te bevatten, wordt die eigenschap duidelijk wanneer je onder de motorkap kijkt.
Het apply-commando, of beter gezegd de familiegroep van commando’s, behoort tot het R base-pakket. Het bevat een aantal functies (de [s,l,m,r, t,v]apply) om plakjes data in de vorm van matrices of arrays herhaaldelijk te bewerken, waarmee je de data kunt kruisen of doorkruisen en expliciet gebruik van lusconstructies kunt vermijden. De functies werken op een invoermatrix of -array en passen een gekozen benoemde functie toe met een of meer optionele argumenten.
Let op dat ze daarom behoren tot de zogeheten ‘functionals’ (zoals op Hadley Wickham’s Advanced R).
De aangeroepen functie kan een aggregerende functie zijn, zoals een simpel gemiddelde, of een andere transformerende of subsetselecterende functie.
Let erop dat het gebruik van deze functies niet per se tot snellere uitvoering leidt: de verschillen zijn niet enorm; het vermijdt eerder het coderen van omslachtige lussen en verkleint zo de kans op fouten.
De functies binnen de familie zijn: apply(), sapply(), lapply(), mapply(), rapply(), tapply(), vapply().
Maar wanneer en hoe gebruik je deze?
Het is goed om te weten dat een pakket als plyr de functionaliteit van de familie dekt; hoewel het lastig kan zijn om ze allemaal te onthouden zonder de officiële documentatie te raadplegen, vormen deze functies toch de basis voor complexere en nuttige combinaties.
De eerste drie worden het meest gebruikt:
apply()
Je wilt een gegeven functie toepassen op de rijen (index “1”) of kolommen (index “2”) van een matrix. Zie deze pagina voor meer informatie.
lapply()
Je wilt een gegeven functie toepassen op elk element van een lijst en als resultaat een lijst krijgen (daarom de “l” in de functienaam). Lees meer over deze functie op R documentation.
sapply()
Je wilt een gegeven functie toepassen op elk element van een lijst, maar je wilt liever een vector dan een lijst ontvangen. Wil je meer weten? Bezoek de R-documentatiepagina.
Gerelateerde functies zijn sweep(), by() en aggregate() en worden af en toe samen met de elementen van de apply()-familie gebruikt.
Ik beperk de bespreking in deze blogpost tot de functie apply() (een uitgebreidere bespreking kan het onderwerp zijn van een toekomstige post).
Gegeven een matrix M, zal de aanroep apply(M,1,fun) of apply(M, 2,fun) de opgegeven functie fun toepassen op de rijen van M, als 1 is opgegeven; of op de kolommen van M, als 2 is opgegeven. Dit numerieke argument heet “margin” en is beperkt tot de waarden 1 en 2 omdat de functie op een matrix werkt. Je zou echter ook een array met tot 8 dimensies kunnen hebben.
De functie mag elke geldige R-functie zijn, maar het kan ook een User Defined Function (UDF) zijn, zelfs gecodeerd binnen de apply(), wat handig is.
apply(): een voorbeeld
# 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))
Je gebruikt vaak data frames: in dit specifieke geval moet je ervoor zorgen dat de data van hetzelfde type zijn, anders kunnen geforceerde datatypconversies optreden, wat waarschijnlijk niet is wat je wilt. In een gemengd tekst- en getallendataframe worden numerieke data bijvoorbeeld omgezet naar strings of characters.
Laatste overwegingen over het gebruik en de alternatieven voor lussen in R
Deze reis bracht ons dus van de fundamentele lusconstructies uit het programmeren naar het (basis)begrip van vectorisatie en naar een voorbeeld van het gebruik van een van de functies uit de apply()-familie, die vaak in R voorkomen.
Qua codebesturing heb je je alleen met lussen beziggehouden: for, while, repeat en de manier om deze te onderbreken en te verlaten.
Aangezien de laatste subsecties suggereren dat lussen in R vermeden moeten worden, kun je je afvragen waarom je ze in hemelsnaam zou leren.
Naar mijn mening moet je deze programmeerstructuren leren omdat:
- Het waarschijnlijk is dat R niet je enige taal zal zijn in data science of elders, en algemene constructies zoals lussen begrijpen is een nuttige vaardigheid om in je gereedschapskist te stoppen. De lussyntaxis kan per taal verschillen, maar als je ze in de ene beheerst, pas je ze snel toe in elke andere taal die je tegenkomt.
- Het R-universum is enorm en het is heel moeilijk, zo niet onmogelijk, om alle bestaande R-functies te kennen. Er zijn veel manieren om dingen te doen, sommige efficiënter of eleganter dan andere en je leercurve zal incrementeel zijn; als je ze gebruikt, ga je jezelf afvragen hoe je dingen efficiënter kunt doen en kom je uiteindelijk uit bij functies waar je nog nooit van hebt gehoord. Tegen de tijd dat je dit leest, zijn er weer nieuwe bibliotheken ontwikkeld en uitgebracht, en de kans dat je ze allemaal kent is klein, tenzij je al je tijd als R-specialist doorbrengt.
- Tot slot, althans wanneer je niet met bijzonder hoog-dimensionale datasets werkt, is een lusoplossing gemakkelijk te coderen en te lezen. En misschien wordt je als data scientist gevraagd om een eenmalige proefopzet te maken die gewoon werkt. Misschien ben je niet geïnteresseerd in verfijnde of elegante manieren om een resultaat te krijgen. Misschien moet je in de data-analyseworkflow vooral domeinkennis tonen en je concentreren op de inhoud. Iemand anders, waarschijnlijk een backendspecialist, zorgt voor de productiefase (en misschien codeert diegene het in een andere taal!)
Als for-lussen in R geen uitdaging meer voor je zijn na het lezen van deze tutorial, overweeg dan DataCamp's Intermediate R-cursus. Deze cursus versterkt je kennis van de onderwerpen in Intermediate R met een reeks nieuwe en leuke oefeningen. Als lussen echter geen geheimen meer voor je hebben, dan is DataCamp's cursus Writing Functions in R, gegeven door Hadley en Charlotte Wickham, misschien iets voor jou. Bekijk ook de For Loops in R-tutorial.
Voer de code van deze tutorial online uit en bewerk deze
Code uitvoerenVeelgestelde vragen over R-lussen
Wat zijn lussen in R en waarom zijn ze nuttig?
Lussen zijn een manier om een codeblok meerdere keren te herhalen. Ze zijn handig wanneer je dezelfde actie wilt uitvoeren op meerdere datapunten of objecten, of wanneer je een actie wilt herhalen totdat aan een bepaalde voorwaarde is voldaan.
Wat zijn de verschillende soorten lussen in R?
Er zijn twee hoofdtypen lussen in R: for-lussen en while-lussen.
- For-lussen herhalen een codeblok voor elk item in een lijst of reeks. Je kunt bijvoorbeeld een for-lus gebruiken om alle elementen van een vector af te drukken, of om een functie toe te passen op elk element van een lijst.
- While-lussen herhalen een codeblok zolang een bepaalde voorwaarde waar is. Je kunt bijvoorbeeld een while-lus gebruiken om de gebruiker om invoer te blijven vragen totdat hij of zij een geldige reactie invoert.
Hoe stop ik een lus in R?
Om een lus in R te stoppen, kun je de break-instructie gebruiken. Hiermee verlaat je de lus onmiddellijk en ga je verder met de rest van de code.
Hoe sla ik de volgende iteratie van een lus in R over?
Om naar de volgende iteratie van een lus in R te gaan, kun je de next-instructie gebruiken. Hiermee sla je de rest van de huidige iteratie over en ga je door naar de volgende.
Kan ik lussen binnen andere lussen plaatsen in R?
Ja, je kunt lussen binnen andere lussen plaatsen in R. Dit heet "geneste" lussen. Je kunt bijvoorbeeld een for-lus binnen een andere for-lus gebruiken om over de elementen van een matrix te itereren.
