Programmering i R 6. mars 2004 1 Funksjoner 1.1 Hensikt Vi har allerede sette på hvordan vi i et uttrykk kan inkludere kall til funksjoner som er innebygd i R slik som funksjonene sum, plot o.s.v. Generelt virker funksjoner ved at funksjonen først mottar visse inndata som vi oppgir som argumenter, funksjonen utfører så en bestemt oppgave, f.eks. visse beregninger, og returnerer deretter resultatet av beregningene som sine utdata gjennom funksjonsverdien. Om vi for eksempel gjør vi et kall til mean utgjør vektoren vi oppgir som argument inndataene, mean beregner så gjennomsnittet av elementene i vektoren, og returnerer dette via funksjonsverdien. Når vi skal gjøre løse problemer som krever større beregninger vil det være hensiktsmessig å definere egne funksjoner som løser bestemte avgrensede deloppgaver av hele problemet. Definering av egne funksjoner er særlig hensiktsmessig når vi skal utføre varianter av samme oppgave flere ganger. Ved å definere en funksjon slipper vi i slike tilfeller å skrive programkode for å løse problemet mer en gang; det å definere funksjoner innebærer gjenbruk av programkode som slik sparer oss for masse arbeid. I tillegg til denne direkte arbeidsbesparelsen skaper bruk av egendefinerte funksjoner bedre oversikt ved at det blir mindre programkode å holde styr på. Når vi har skrevet en funksjon ferdig som løser en avgrenset veldefinert del av hele oppgaven kan vi glemme hvordan funksjonen virker i detalj og konsentrere oss om å løse resten av problemet. 1
Mindre programkode gjør at det blir lettere på et senere tidspunkt å endre programkoden hvis oppgaven vi ønsker å løse skulle endre seg eller hvis programkoden skulle vise seg å inneholde feil. 1.2 Formell virkemåte La oss først se på hvordan vi definerer en funskjon og hvordan en funksjon virker formelt ved å se på et enkelt eksempel. I likhet med andre objekter som vektorer o.l. lager vi en funksjon ved å bruke tilordningsoperatoren. Funksjonen blir altså selv et objekt i R sitt workspace på samme måte som andre variable. Fordi funksjonsdefinisjonen vanligvis går over flere linjer er det praktisk å skrive funskjonsdefinisjonen inn i en teksteditor, f.eks. notepad i windows. Skriv følgende inn i notepad, merk teksten og kopier denne ved å trykke ctrl-c, å lim så dette inn i R-vinduet ved å trykke ctrl-v i R. minfunksjon <- function(arg1,arg2=20) { arg1 <- arg1 + 5 a <- arg1+arg2 return(a^2) Gjør vi dette får vi opprettet et objekt av type funksjon med navn minfunksjon i R sitt workspace. Funksjonen har to formelle argumenter med navn arg1 og arg2. Argumentene vil være å betrakte som variable når uttrykkene inne i funksjonen blir beregnet (når vi gjør et kall til funksjonen). I linje to tilordnes verdien av uttrykket arg1 + 5 til arg1 (arg1 øker sin verdi med 5). I linje tre tilordnes verdien uttrykket arg1+arg2 nå har fått til variabelen a. I linje 4 beregnes verdien av uttrykket aˆ2 som returneres som funksjonsverdi. Dette uttrykket trenger strengt tatt ikke å omsluttes av return( ); lar vi være vil funksjonen returnere verdien av uttrykket på siste linje i funksjonsdefinisjonen. Som for innebygde funksjoner i R kan vi nå gjøre et kall til funksjonen: > minfunksjon(10,5) [1] 400 Vi ser at funksjonen oppfører seg som forventet; først legges 5 til 10 slik at arg1 får verdien 15. Variabelen a blir så lik summen av variablene arg1 og arg2 (20). Dette opphøyes så i andre slik at funksjonsverdien blir lik 400. Merk at variabelen a og argumentene arg1 og arg2 ikke lenger er tilstede etter kallet til funksjonen; disse er å betrakte som lokale variable som bare er synlige fra innsiden av funksjonen og som forsvinner når funksjonen har gjort sitt: 2
> a Error: Object "a" not found > arg1 Error: Object "arg1" not found > arg2 Error: Object "arg2" not found Dette er også ønskelig oppførsel i og med at vi ikke ønsker at variable vi bruker lokalt inne i funksjonen for å løse funksjonens oppgaver skal komme i konflikt med variabler vi har opprettet for å ta vare på beregningsresultat utenfor funksjonen. Om vi har opprettet en variabel a før kallet til minfunksjon vil a fortsatt ha samme verdi etter funksjonskallet: > a <- c(1,2,3) > minfunksjon(10,5) [1] 400 > a [1] 1 2 3 Vektoren a her er å betrakte som en global variabel som ikke endrer seg når vi tilordner verdier til den lokale variabelen a inne i funksjonen minfunksjon. Sett fra utsiden trenger vi med andre ord ikke å bekymre oss for hvilke variabelnavn som brukes internt av funksjonen. Eksempelet over illusterer også bruken av default-verdier. I første linje i funksjonsdefinisjonen har vi spesifisert at arg2 har 100 som sin defaultverdi. Lar vi være å oppgi arg2 i kallet til funksjonen vil arg2 ta verdien 100 i påfølgende beregninger internt i funksjon slik at vi får: > minfunksjon(10) [1] 1225 1.3 Eksempel La oss betrakte tenke oss en bestand på N = 50 moskusokser. m = 15 av disse bærer på en bestemt genvariant på sitt Y-kromosom (hver hann har bare ett Y-kromosom). Fordi bestanden er blitt for stor blir det besluttet at bestanden skal reduseres til n = 20 okser. La oss anta at disse blir valgt tilfeldig fra den opprinnelige bestanden. Lar vi X betegne antall kopier av genvarianten som er i igjen i utvalget etter bestandsreduksjonen følger det 3
at X blir hypergeometrisk fordelt med sannsynlighetsfordeling ( )( ) m N m p X (x) = x n x ( ) N (1) n Vi ønsker å lage et plot over sannsynlighetsfordelingen. I første omgang ønsker vi å beregne uttrykket over, la oss si for x = 10. Vi ser at uttrykket innebærer at vi må beregne tre binomialkoeffisienter. Videre en binomial koeffisienten definert som ( ) n x = n! x!(n x)! som innbærer at vi i beregningen av hver binomialkoeffisient må beregne verdier av fakultetsfunksjonen tre ganger. Lager vi oss funksjoner som utfører disse deloppgavene på en generell måte kan vi altså spare oss for mye arbeid. La oss først lage en funksjon som beregner n! = 1 2 n. Dette kan vi gjøre ved å lage en vektor av tallene fra 1 til n med uttrykket 1:n og så beregne produktet av elementene i vektoren med et kall til funksjonen prod (tilsvarer funksjonen sum). En definisjon av hele funksjonen vil kunne være fac <- function(n) { resultat <- prod(1:n) return(resultat) Tester vi funksjonen ser vi at den virker slik den skal: > fac(4) [1] 24 Når vi har løst denne deloppgaven kan vi lett definere en funksjon for å beregne binomialkoeffisienten: binomcoef <- function(n,x) { fac(n)/(fac(x)*fac(n-x)) Til sist definerer vi en funksjon som beregner hele punktsannsynligheten i en gitt verdi X = x, p X (x) gitt ved (1): pgenfordeling <- function(x,n=50,m=15,n=20) { binomcoef(m,x)*binomcoef(n-m,n-x)/binomcoef(n,n) 4 (2)
Ved å spesifisere default-verdier i funksjonsdefinisjonen slipper vi å oppgi modellparameterene i kall til funksjonen. Vi kan nå beregne, f.eks. p X (3), sannsynligheten for at 3 av de 20 oksene etter bestandsreduksjonen vil bære genet: > pgenfordeling(3) [1] 0.04380708 Oppgaven er nå løst ved vi har definert funksjonen pgenfordeling som gjør tre kall til til funksjonen binomcoef som tilsammen gjør tre ganger tre kall til funksjonen fac. 2 If-setninger 2.1 Eksempel La oss fortsette eksempelet fra del 1.3. Forsøker vi å beregne sannsynligheten for at genvarianten blir helt borte, altså p X (0) = P (X = 0) får vi > pgenfordeling(0) [1] Inf Dette er åpenbart feil, en punktsannsynlighet i en diskret modell kan ikke ta verdier som går mot uendelig. Etter litt mer testing av de funksjonene vi har laget finner vi at vår fakultetsfunksjon fac returnerer følgende for 0!: > fac(0) [1] 0 hvilket jo er feil i og med at 0! er definert lik 1. Ser vi på verdiene uttrykkene i funksjonen fac får om vi bruker 0 som argument ser vi følgende > 1:0 [1] 1 0 > prod(1:0) [1] 0 Løsningen er å bruke en såkalt if-setning som gjør at vi kan håndtere spesialtilfeller som dette. Vi skriver om funksjonen til følgende: fac <- function(n) { if (n==0) resultat <- 1 else 5
resultat <- prod(1:n) return(resultat) En if-setning består generellt av et logisk uttrykk etterfulgt av et eller to uttrykk hvorav et av disse beregnes avhengig av om verdien av det logiske uttrykket har verdi TRUE eller FALSE. I vår modifiserte definisjon av funksjonen fac vil uttrykket n==0 få verdi TRUE dersom argumentet n er lik 0 og i slike tilfeller vil tilordningsuttrykket resultat <- 1 utføres, i motsatt fall utføres uttrykket resultat <- prod(1:n). Til siste returnes verdien av den lokale variabelen resultat. Med denne modifikasjonen begynner ting å fungerer: > fac(0) [1] 1 > fac(4) [1] 24 > pgenfordeling(0) [1] 6.891571e-05 Vi ser at det er temmelig usannsynlig at genvarianten forsvinner helt men ikke helt umulig. Om vi antar at genvarianten i utgangspunktet er mer skjelden, la oss si tilstede i bare m = 5 kopier, får vi at sannsynligheten for at det forsvinner blir langt større: > pgenfordeling(0,m=5) [1] 0.06725915 Unntakshåndtering slik som i dette eksempelet er en ganske vanlig bruk av if-setninger men if-setninger kan selvsagt også brukes til en rekke andre formål. Generellt kan de to alternative uttrykkene i en if-setning være såkalt sammensatte uttrykk. Ett sammensatt uttrykk kan skrives på formen {uttrykk1; uttrykk2; uttrykk3 eller { uttrykk1 uttrykk2 uttrykk3 Sammensatte uttrykk brukes også i definisjoner av funksjoner, slik vi har sett for eksempel i del 1.2, og i andre programstrukturer. 6
3 Løkker 3.1 Eksempel Vi har allerede sett på hvordan vi mange funksjoner kan operere elementvis på vektorer, f.eks. vil logaritmefunksjonen beregne logaritmer elementvis for vektoren som vi oppgir som argument: > weight [1] 60 72 57 90 95 72 > log(weight) [1] 4.094345 4.276666 4.043051 4.499810 4.553877 4.276666 Hvorvidt en funskjon kan operere elementvis på vektorer som er oppgitt som argument vil avhenge av hvordan funksjonen virker internt. Ser vi på vår funksjon pgenfordeling ser vi at den ikke håndterer vektorer som sitt x argument: > pgenfordeling(0:15) [1] 6.891571e-05 Warning messages: 1: the condition has length > 1 and only the first element will be used in: if (n == 0) return(1) else return(prod(1:n))... Slik vi har programmert vår funksjonen beregnes i tilfelle over bare p X (x) for x = 0 i tillegg til at vi får en rekke feilmeldinger. Skal vi beregne la oss si en vektor av sannsynlighetene p X (0), p X (1),..., p X (15) må vi derfor tilordne verdiene til resultatvektoren eksplisitt. Vi kunne ha gjort dette på følgende måte: > pvektor<-rep(0,16) > pvektor[0]<-pgenfordeling(0) > pvektor[1]<-pgenfordeling(1) > pvektor[2]<-pgenfordeling(2) Meningen med for-løkker er å forenkle slike operasjoner. I stedet for alle linjene i koden over kan vi utføre det samme ved å bruke en såkalt for-løkke: > pvektor<-rep(0,16) > for (i in 0:15) pvektor[i+1] <- pgenfordeling(i) 7
Det som her skjer er at uttrykket pvektor[i] <- pgenfordeling(i) utføres et antall ganger. Ved hver enkelt utførelse av uttrykket vil løkke-variabelen i ha elementer hentet fra vektoren 0:15 som verdi. Resultatet er at alle elementene i vektoren pvektor blir tilordnet riktige verdier (sannsynlighetene for at 0, 1, 2 kopier av genvarianten o.s.v. er tilstede i populasjonen etter bestandsreduksjonen): > pvektor [1] 1.292170e-03 1.010933e-02 4.380708e-02 1.175874e-01 2.069539e-01 [6] 2.463737e-01 2.015785e-01 1.139356e-01 4.430831e-02 1.169739e-02 [11] 2.044999e-03 2.272221e-04 1.498168e-05 5.166095e-07 6.888127e-09 [16] 0.000000e+00 Vi kan nå lage et plot av resultatet: > plot(0:15,pvektor,type="h",xlab="antall genkopier x",ylab="sannsynlighet") Argumentet type= h spesifiserer at plottet skal være av type histogram. Default-verdien til argument type er p. Resultatet blir som i figur 1 0 5 10 15 antall genkopierx sannsynlighet 0.00 0.10 0.20 Figur 1: Resultatet av kallet til plot funksjonen Generellt er for-løkker på formen 8
for (var in vektor {sammensatt uttrykk 9