Programmering i R - del 2 14. februar 2004 1 Simulering fra modell Når vi skal analysere et gitt konkret innsamlet datasett vil vi gjøre dette med utgangspunkt i en statistisk modell. Vi kan si at en slik statistisk modell representerer hva vi på forhånd tror om den prosessen som har generert dataene vi studerer. Modellen består av et sett antakelser vi velger å tro på som spesifiserer hvordan dataene er fordelt gitt verdien av visse ukjente størrelser som inngår i modellformuleringen modellens parametere. Vi ønsker å trekke slutninger om disse parameterne. I mange tilfeller kan vi finne egenskaper ved interessante størrelser slik som estimatorer, testobservatorer og andre funksjoner av dataene analytisk. Dersom vi for eksempel antar at vi har uavhengig identisk normalfordelte data, vil størrelsen ( X µ)/(s/ n) være t-fordelt. I en lineær regresjonsmodell antas det at responsvariabelen Y i for den i te observasjonen normalfordelt med forventning a + bx i og varians σ 2. I denne modellen har estimatorene for de ulike parameterne kjente fordelinger. I mange tilfeller kan vi finne interessante egenskaper som forventningsverdi og varians til eksempelvis ulike estimatorer analytisk. Avhengig av hvilke antakelser vi finner det rimelig å bygge inn i en modell vil det imidlertid ikke alltid være slik at vi kan finne egenskaper til interessante størrelser analytisk. Det er i slike tilfeller at stokastisk simulering vil være et nyttig hjelpemiddel. 1.1 Generell algoritme Anta at vi har en modell som spesifiserer fordelingen til et sett observasjoner X 1, X 2,..., X n. I mange tilfeller vil vi være interessert i å se på egenskapene til en eller annen funksjon av observasjonene, la oss si, W = W (X 1, X 2,..., X n ). (1) 1
I øving 3 brukte vi estimatoren ˆλ = n/ X i av λ i eksponentialfordelingen som eksempel på en slik funksjon og vi fant at denne ikke var forventningsrett ved å simulere. Generelt vil funksjonen W kunne være en estimator, en testobservator, eller en annen stokastisk variabel som vi ønsker å finne egenskapene til. I estimeringssammenheng trenger vi å bestemme standardfeil og eventuell forventningsfeil til estimatoren gitt ved E(W ) og Var(W ). I forbindelse med hypotese testing vil vi kunne være interessert i sannsynligheter for eksempel av typen P (W a). Når vi skal undersøke reellt konfidensnivå til konfidensintervaller trenger vi å se på to funksjoner av dataene, nedre og øvre intervallgrense, gitt ved funksjonene θ(x 1, X 2,..., X n ) og θ(x 1, X 2,..., X n ). Vi er så interessert i sannsynligheten for at intervallgrensene ligger rundt den ukjente parameteren θ, altså P (θ θ θ). Også utenfor området statistisk inferens vil stokastisk simulering være et nyttig verktøy. Senere i kurset og i videregående kurs vil vi lage stokastiske modeller som spesifiserer fordelingen til frekvensen av for eksempel en ny mutasjon i fremtidige generasjoner, la oss si, p 1, p 2,..., p n, gitt frekvensen i generasjon t = 0. Spørsmål som vil kunne være av interesse er sannsynligheten for at en mutasjonen er tilstede i generasjon n, P (p n > 0). Alt dette er tallstørrelser som vi finne med den nøyaktighet vi måtte ønske ved hjelp av simuleringer. En generell algoritme (tenk kakeoppskrift ) for å gjennomføre dette er som følger: 1. Gjenta følgende for i = 1, 2,..., m. Antall simuleringer velges gjerne lik m = 1000. (a) Simuler et utvalg X1, X2,..., Xn fra modellen. Utvalget kalles gjerne et bootstrap-sample. (b) Beregn Wi = W (X1, X2,..., Xn) og eventuelt andre variable av som er av interesse. Dette vil utgjøre i te bootstrap-replikat av variabelen W. 2. Nå kan ulike størrelser estimeres fra bootstrap- replikatene på følgende måte: 2
(a) E(W ) kan estimeres ved hjelp av estimatoren W = 1 m Wi. (2) n (b) Var(W ) kan estimeres ved vanlig estimator for varians, SW 2 = 1 m (W i m 1 W ) 2. (3) (c) Sannsynligheter av typen P (W a) kan estimeres ved m A /m hvor m A er antall bootstrap-replikater W i a. I praksis utføres en slik algoritme eller oppskrift lettest ved hjelp av datamaskin ved å lage et program i et passende programmeringsspråk. 1 1.2 Eksempel - forventningsverdi til estimator I øving 3 så vi at ˆλ = i=1 i=1 n Xi (4) ikke var forventningsrett for parameteren λ i ekponentiell modell. Dette kan også relativt enkelt vises analytisk. Det kan vises at forventningsverdien er gitt ved E(ˆλ) = n λ. (5) n 1 Lager vi oss en ny estimator basert på ˆλ, følger det at denne er forventningsrett fordi ˆλ = n 1 n ˆλ = n 1 Xi, (6) E(ˆλ ) = n 1 n E(ˆλ) = λ. (7) La oss kontrollere dette ved hjelp av simuleringer. Det er alltid hensiktsmessig å programmere den funksjonen vi vil se på som en egen funksjon i R. Estimatoren gitt (6) kan programmeres i R på følgende måte: 1 Algoritmen over kalles gjerne parametrisk bootstrapping. I kursene anvendt statistikk og moderne statistiske metoder vil såkalt ikke-parametrisk bootstrapping bli behandlet. Dette er metoder hvor vi simulerer bootstrap-sample (trinn 1a i algoritmen) på en annen måte uten å gjøre antakelser om hvilken fordeling dataene har. Uttrykket bootstrapping henspeiler på hvordan vi i en slik situasjon så og si letter fra bakken (greier å gjennomføre statistiske slutninger) ved å trekke oss opp etter skolissene. 3
lambdahatmerket <- function(x) { n <- length(x) return((n-1)/sum(x)) Merk kallet til length som gjør at funksjonen lambdahatmerket håndterer utvalg av vilkårlig størrelse (representert av vektoren x som er innargument.) Funksjonen svarer til funksjonen W gitt ved (1) i det generelle oppsettet. Vi programmerer så en funksjon som utfører algoritmen over. Det er hensiktsmessig å la antall simuleringer m være et argument med defaultverdi 1000 slik at vi eventuelt kan utføre et større antall simuleringer senere om vi måtte ønske dette uten å endre funksjonsdefinisjonen. Når vi skal simulere fra modellen vår må vi også anta en eller annen verdi for parameteren λ. Denne (samt utvalgsstørrelsen n) bør derfor også være argument i funksjonen som skal utføre simuleringsalgoritmen: lambdasim <- function(lambda,n=10,m=1000) { lambdaboot <- rep(na,m) for (i in 1:m) { X <- rexp(n=n,rate=lambda) lambdaboot[i] <- lambdahatmerket(x) return(lambdaboot) La oss gå gjennom de ulike delene av funksjonsdefinisjonen over. Hoveddelen av funksjonen består av en for-løkke hvor løkkevariabelen i tar verdiene 1, 2,..., 1000 ved de gjentatte utførelsene av løkke-kroppen (uttrykkene mellom krøllparantesene). I første linje av løkke-kroppen simuleres et tilfeldig utvalg (et bootstrapsample) fra modellen ved hjelp av et kall til funksjonen rexp. Resultatet lagres i den lokale variabelen X. Dette svarer til trinn 1a) i den generelle algoritmen i avsnitt 1.1. I andre linje av løkke-kroppen beregnes det i te bootstrapreplikatet av variabelen vi ser på ved hjelp av et kall til vår egen funksjon lambdahatmerket. Dette svarer til trinn 1b i algoritmen. Merk at det er det simulerte bootstrapsamplet som går inn som argument i kallet til lambdahatmerket. Vi tar vare på resultatet i det i te elementet av den lokale variabelen (vektoren) lambdaboot. Det siste funksjonen gjør etter at for-løkken er gjennomløpt er at hele vektoren lambdaboot returneres som funksjonsverdi. 4
Merk også at hele vektoren lambdaboot opprettes som en tom vektor av lengde m i første linje ved at verdien av uttrykket rep(na,m) tilordnes til lambdaboot. Dette er hensiktsmessig å gjøre dersom vi kjenner lengden til lambdaboot på forhånd slik som her vi unngår at R må allokere minneplass til stadige utvidelser av vektoren lambdaboot ved gjennomkjøring av løkken slik tilfelle ville vært om vi hadde initiert lambdaboot til å ha bare ett element i linje 1. Resultatet er at kall til lambdasim utføres på langt kortere tid. 2 Går vi tilbake til vårt opprinnelige problem kan vi nå finne forventningsverdien til estimatoren ˆλ ved å gjøre ett kall til lambdasim og så beregne gjennomsnittsverdien til de returnerte bootstrapreplikatene: > mean(lambdasim(lambda=1)) [1] 0.9921228 > mean(lambdasim(lambda=2)) [1] 2.00689 > mean(lambdasim(lambda=3)) [1] 3.035254 > mean(lambdasim(lambda=1,m=10000)) [1] 1.003821 > mean(lambdasim(lambda=2,m=10000)) [1] 1.987892 > mean(lambdasim(lambda=3,m=10000)) [1] 3.014727 > mean(lambdasim(lambda=3,m=10000,n=2)) [1] 2.972106 > mean(lambdasim(lambda=2,m=10000,n=2)) [1] 1.978133 > mean(lambdasim(lambda=1,m=10000,n=2)) [1] 1.003817 Vi ser at forventningsverdien estimert ved (2) nå blir så og si lik den sanne parameterverdien for λ = 1, 2, 3 også for små utvalgsstørrelser n. At dette virkelig er tilfelle kan selvsagt testes med mer formelle hypotesetestingsmetoder. 2 Ta eventuelt tiden ved å skrive f.eks. system.time(mean(lambdasim(lambda=1))). Endre så første linje til lambdaboot <- NA og ta tiden på nytt. 5
1.3 Eksempel - beregning av dekningsgrad til konfidensintervall Dersom X 1, X 2,..., X n er uavhengige normalfordelte data med ukjent forventning og varians µ og σ 2 er ( X t n 1,α/2 S/ n, X + t n 1,α/2 S/ n) (8) et (1 α) konfidensintervall for parameteren µ. Dette betyr at endepunktene i intervallet (som er funksjoner av dataene og dermed stokastiske variable) skal ligger rundt µ med sannsynlighet (1 α) (konfidensnivået). At dette faktiske er tilfelle kan kontrolleres ved hjelp av simuleringer. Vi følger den samme generelle oppskrift som i avsnitt 1.1. Først programmer vi en funksjon som beregner endepunktene i intervallet som funksjon av dataene. Dette svarer til to funksjoner på samme form som (1). Kvantilen i t-fordelingen, t n 1,α/2, finner vi ved et kall til qt. Merk at kvantiler i R alltid er definert på grunnlag av nedre hale i fordelingene i motsetning til i mange lærebøker hvor øvre hale ofte brukes som utgangspunkt. konfintmu <- function(x,alpha=.05) { n <- length(x) t <- qt(df=n-1,p=alpha/2,lower.tail=false) xbar <- mean(x) s <- sd(x) return(list(nedre=xbar-t*s/sqrt(n),ovre=xbar+t*s/sqrt(n))) Navn på listekomponenter spesifiseres foran likhetstegnene; komponentene verdi blir like verdien av uttrykkene etter likhetstegnene. Tester vi intervallet på ett simulert datasett får vi: > x <- rnorm(n=30,mean=10,sd=2) > konfintmu(x) $nedre [1] 9.317239 $nedre [1] 10.73102 En funksjon som beregner dekningsgraden kan se slik ut: dekningsgrad <- function(mu,sigma2,n,alpha=.05,nsim=10000) { 6
ntreff <- 0 for (i in 1:nsim) { X <- rnorm(n=n,mean=mu,sd=sqrt(sigma2)) ki <- konfintmu(x,alpha) if (ki$nedre<=mu & mu<=ki$ovre) { ntreff <- ntreff + 1 return(ntreff/nsim) Denne funksjonen følger essensielt samme oppsett som den generelle algoritmen i avsnitt 1.1. I første linje i løkke-kroppen simuleres et tilfeldig utvalg (et bootstrap-sample) fra den antatte modellen, og i neste linje beregnes konfidensintervallet og vi tar vare på dette i listen ki. Vi søker sannsynligheten for at intervallgrensene ligger rundt µ. Derfor tester vi dette med en if-setning; hvis det logiske uttrykket er oppfylt økes tellevariabelen ntreff med 1 i neste linje. Når for-løkken er gjennomløpt vil uttrykket ntreff/nsim gi oss et estimat av sannsynligheten vi er ute etter intervallets reelle dekningsgrad. Vi må huske å initiere tellevariabelen i første linje før vi har begynt å telle skal denne ha verdi 0. Vi ser at dekningsgraden i dette tilfelle blir lik det nominelle nivået uansett hvilke verdier modellparameterne har. Dette er forventet i og med at intervallet bygger ikke på noen tilnærminger i motsetning til intervallet for p i øving 4 og 5: > dekningsgrad(mu=10,sigma2=2^2,n=10,alpha=.05) [1] 0.9495 > dekningsgrad(mu=10,sigma2=2^2,n=10,alpha=.1) [1] 0.8979 > dekningsgrad(mu=10,sigma2=2^2,n=2,alpha=.05) [1] 0.9506 > dekningsgrad(mu=10,sigma2=2^2,n=100,alpha=.05) [1] 0.9496 2 Newton s metode 2.1 While-setningen Vi har tidligere sett på bruk av før-løkker. Slike løkker er hensiktsmessig å bruke når vi skal gjenta visse beregninger (løkke-kroppen) et antall ganger 7
og når antallet er kjent på forhånd. I en del tilfeller vil vi imidlertid ønske å stoppe løkken når en visse betingelse er oppfylt. Et eksempel er løsning av ikke-lineære ligninger ved hjelp av Newton s metode. La oss først se på den generelle virkemåten til en while-løkke. En while-løkke er bygget opp på følgende måte while (logisk uttrykk) { sammensatt uttrykk Før hver utførelse av løkke-kroppen (et eller flere uttrykk mellom krøllparantesene) vil det logiske uttrykket i parenteser beregnes. Hvis dette har verdi FALSE vil løkken avbrytes, hvis ikke utføres løkke-kroppen. La oss se på følgende eksempel. i <- 1 while (i<100) { i <- i*2 print(i) Her initieres først verdien av variabelen i til 1. Ved første gangs beregning av det logiske uttrykket i<100 har dette dermed verdi TRUE og løkke-kroppen utføres slik at i får verdien 2 som så skrives til skjerm ved med funksjonen print. Så gjentas det hele (det logiske uttrykket beregnes på nytt og løkkekroppen utføres) helt til i blir ikke lenger er mindre 100. Dette inntreffer når i har fått verdien 128 etter sjuende gangs utførelse av løkke-kroppen. Da avbrytes løkken: [1] 2 [1] 4 [1] 8 [1] 16 [1] 32 [1] 64 [1] 128 > i [1] 128 2.2 Newton s metode Algoritmen er beskrevet i detalj i Neuhauser (2004) kap. 5.7. Metoden er egnet til å finne røtter til ligninger på formen f(x) = 0 forutsatt at vi kjenner den deriverte av funksjonen f. Algoritmen er som følger: 8
1. Velg en passende x 0 i nærheten av løsningen. 2. Beregn for n = 1, 2,... inntil x n x n 1 < ɛ. x n+1 = x n f(x n) f (x n ), (9) Størrelsen ɛ bestemmer den numeriske nøyaktigheten på løsningen og velges lik f.eks. 10 8 hvis vi ønsker en løsning med 8 desimalers nøyaktighet. Andre stoppkriterier kan brukes avhengig av hva slags problem vi studerer. 2.3 Eksempel Anta at vi ønsker å finne roten av et positivt tall a, altså løsningen av ligningen x 2 = a x 2 a = 0 (10) som er på formen f(x) = 0 om vi lar f(x) = x 2 a. Deriverer vi f får vi og iterasjonsligningen blir dermed f (x) = 2x, (11) x n+1 = x n x2 n a 2x n x n+1 = x n x n 2 + a. 2x n (12) En funksjon som beregner roten av a ved bruk av metoden over kan se slik ut: minrot <- function(a,x0=a/2,tol=1e-8) { x <- x0 forrigex <- x0-1 while (abs(x-forrigex)>tol) { forrigex <- x x <- x - x/2 + a/(2*x) return(x) 9
Vi trenger i praksis ikke å ta vare på alle x n ene; det er nok å bruke to lokale variabler x og forrigex. Variabelen forrigex brukes til å ta vare på forrige verdi av x før vi beregner neste verdi av x, slik at vi kan sammenligne x n+1 og x n i det logiske uttrykket i første linje av while-løkken. Merk at verdiene til av begge disse variablene må tilordnes passende og forskjellige start verdier (linje 1 og 2) slik at while-løkken ikke stopper umiddelbart. I dette tilfelle har ligningen f(x) = 0 flere røtter, disse kan finnes ved å gi x 0 passende start verdier i nærheten av den roten vi søker: > minrot(2) [1] 1.414214 > minrot(2,x0=-2) [1] -1.414214 Newton s metode vil ikke alltid konverger mot noen løsning, se Neuhauser (2004) for eksempler. En rekke varianter av Newtons metode og andre algoritmer eksisterer for å løse tilsvarende problem. R s innebygde funksjon uniroot bruker en annen og langsommere algoritme og søker seg fram til røtter til en funksjon f forutsatt at f(x) har forskjellig fortegn på endepunktene av intervallet x = a og x = b: > f <- function(x,a) x^2-a > uniroot(f,lower=0,upper=4,a=2) $root [1] 1.414208 $f.root [1] -1.503627e-05 $iter [1] 8 $estim.prec [1] 6.103516e-05 > uniroot(f,lower=-4,upper=0,a=2) $root [1] -1.414208 $f.root [1] -1.503627e-05 10
$iter [1] 8 $estim.prec [1] 6.103516e-05 3 Generelle programmeringstips Del opp problemet i mindre naturlig avgrensede deler. Lag funksjoner som løser avgrensede deler av problemet og lag disse så generelle at de kan gjenbrukes i resten av løsningen. Identifiser hva som skal være inn og utdata til funksjonen (argumenter og funksjonsverdi). Skriv ned skjelettet av funksjonsdefinisjonen når du har gjort dette og gi funksjonens argumenter passende navn. Hvilke beregninger må gjennomføres med utgangspunkt i funksjonens inndata for å komme fram til det funksjonen skal returnere som utdata? Tilordne verdien av mellomberegninger til lokale variable. Hvis beregningen krever bruk av for- eller while-løkke kan det lønne seg å ta utgangspunkt i løkken. Skriv ned skjelettet av løkken. Hvilke beregninger må gjennomføres inne i løkke-kroppen? Hvordan skal vi ta vare på verdien av uttrykk beregnet inne i løkke-kroppen? Initier om nødvendig verdien av lokale på først i funksjonen. Kommenter egen kode! Hver linje kan kommenteres ved å avslutte hver linje med kommentartegnet # etterfulgt av kommentarer. Gi variable navn som forteller hva variablene inneholder. 11