Ristivalidointia ja grafiikkaa Jari Oksanen Maanantai 12. syyskuuta 2005 Tiivistelmä Tässä monisteessa on maantain tapahtumien yhteenveto. Aloitimme Eija Hurmeen kurssipäiväkirjalla ja sen jälkeen päätiomme kerrata ristivalidoinnin ennustavassa mallissa. Lopuksi käsittelimme R-grafiikkaa. 1 Ristivalidointi Aluksi asetan joitain parametreja, joiden avulla tästä monisteesta tulee hieman helppolukuisempi. > options(digits = 4) > options(width = 72) Seuraavaksi luemme aineiston internetistä. > load(url("http://cc.oulu.fi/~jarioksa/piikurssi.rda")) Tällä kertaa sovitamme yksinkertaisen mallin, jossa on mukana vain yksi selittävä ja tärkeä tekijä sekä pakollinen laskettujen levien kokonaismäärä: > mod = lm(spno ~ log(tot) + ph, data = kurssi) > summary(mod) Call: lm(formula = spno ~ log(tot) + ph, data = kurssi) Residuals: Min 1Q Median 3Q Max -23.71-6.82 1.39 6.25 22.86 Coefficients: Estimate Std. Error t value Pr(> t ) (Intercept) -89.75 19.61-4.58 1.3e-05 *** log(tot) 19.95 2.93 6.80 6.2e-10 *** ph 3.63 1.56 2.32 0.022 * --- Signif. codes: 0 *** 0.001 ** 0.01 * 0.05. 0.1 1 Residual standard error: 9.91 on 107 degrees of freedom Multiple R-Squared: 0.323, Adjusted R-squared: 0.31 F-statistic: 25.5 on 2 and 107 DF, p-value: 8.59e-10 1
Teemme 5-kertaisen ristivalidoinnin ja sitä varten tarvitsemme vektorin, jossa on kokonaislukuja 1... 5 satunnaisessa järjestyksessä. Aineistossa on 110 riviä, joten tämä on vektorin pituus: > take = rep(1:5, len = 110) > take [1] 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 [34] 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 [67] 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 1 2 3 4 [100] 5 1 2 3 4 5 1 2 3 4 5 > sample(take) [1] 2 2 3 1 1 2 3 5 5 4 2 5 5 4 3 4 3 4 5 3 2 2 4 3 1 3 4 1 1 5 2 1 4 [34] 1 2 3 2 3 2 2 5 4 3 2 3 5 5 5 5 1 1 2 1 5 4 4 1 5 2 2 1 5 4 3 4 3 [67] 5 4 4 1 2 3 1 2 4 4 5 5 3 4 5 2 2 3 4 3 1 4 4 4 5 5 3 5 4 2 1 3 3 [100] 1 1 3 5 1 2 1 1 1 2 3 > take = sample(take) Teeme vektorin toistamalla (rep) listaa yhdestä viiteen (1:5) niin että kokonaispituudeksi tulee 110. Sen jälkeen sekoitamme (sample) indeksit satunnaiseen järjesykseen. Ristivalidoinnissa poistetaan yksi aineisto (aluksi indeksi on 1 eli ==1), malli sovitetaan lopulla aineistolla (indeksi ei ole 1, eli!=1), ja sen jälkeen ennustetaan poistetulle osalle (indeksi ==1). > k5 = update(mod, data = kurssi[take!= 1, ]) > mod Call: lm(formula = spno ~ log(tot) + ph, data = kurssi) Coefficients: (Intercept) log(tot) ph -89.75 19.95 3.63 > k5 Call: lm(formula = spno ~ log(tot) + ph, data = kurssi[take!= 1, ]) Coefficients: (Intercept) log(tot) ph -87.13 19.82 3.26 > pre1 = predict(k5, newdata = kurssi[take == 1, ]) Käytännössä ongelmana on laittaa kunkin osa-aineiston ennusteet alkuperäiselle, oikealle rivilleen. Tässäkin käytämme jälleen apuna yhtäsuurutta indeksiin. Teemme aluksi vektorin, johon kaikki ennusteet mahtuvat, eli vektorin pituus on sama kuin aineiston havaintojen määrä (110): 2
> prek = rep(0, 110) > prek [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 [34] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 [67] 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 [100] 0 0 0 0 0 0 0 0 0 0 0 > take [1] 4 3 5 4 4 4 5 3 3 2 4 3 5 3 4 5 3 4 4 1 2 5 3 2 1 2 3 5 3 1 1 5 2 [34] 4 2 5 4 1 3 5 4 4 3 1 2 1 2 3 3 2 2 5 1 5 2 4 5 1 2 5 2 2 4 1 3 1 [67] 3 2 4 2 4 3 5 4 5 2 1 1 3 5 5 4 1 4 1 2 3 5 1 1 5 3 3 2 1 1 4 2 2 [100] 3 1 2 5 4 4 5 1 5 3 1 > prek[take == 1] = predict(k5, newdata = kurssi[take == + 1, ]) > round(prek, 1) [1] 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 [14] 0.0 0.0 0.0 0.0 0.0 0.0 47.6 0.0 0.0 0.0 0.0 49.7 0.0 [27] 0.0 0.0 0.0 54.7 47.6 0.0 0.0 0.0 0.0 0.0 0.0 44.8 0.0 [40] 0.0 0.0 0.0 0.0 49.5 0.0 54.8 0.0 0.0 0.0 0.0 0.0 0.0 [53] 44.2 0.0 0.0 0.0 0.0 45.0 0.0 0.0 0.0 0.0 0.0 48.2 0.0 [66] 46.4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 50.2 51.8 [79] 0.0 0.0 0.0 0.0 46.5 0.0 50.5 0.0 0.0 0.0 50.5 48.8 0.0 [92] 0.0 0.0 0.0 42.3 50.1 0.0 0.0 0.0 0.0 51.2 0.0 0.0 0.0 [105] 0.0 0.0 54.2 0.0 0.0 42.2 Nyt osa-aineiston 1 ennusteet ovat oikeilla paikoillaan. Meidän on toistettava komentoketju kullkein osajoukolle erikseen. Kierrätämme komentoja R:n muistista (nuolinäppäimet) ja vaihdamme aina indeksiä takelle: > k5 = update(mod, data = kurssi[take!= 2, ]) > prek[take == 2] = predict(k5, newdata = kurssi[take == + 2, ]) > k5 = update(mod, data = kurssi[take!= 3, ]) > prek[take == 3] = predict(k5, newdata = kurssi[take == + 3, ]) > k5 = update(mod, data = kurssi[take!= 4, ]) > prek[take == 4] = predict(k5, newdata = kurssi[take == + 4, ]) > k5 = update(mod, data = kurssi[take!= 5, ]) > prek[take == 5] = predict(k5, newdata = kurssi[take == + 5, ]) Tehty. Ei kovin työlästäkään, mutta erehtymisen mahdollisuus on melkoinen, sillä joka askelella on indeksin arvo muutettava kolmeen kohtaan. Lopuksi arvioimme mallien hyvyyksiä. Varmuuden vuoksi lasketaan vielä alkuperäisen mallin ristivalidoimattomat ennusteet pre joita vertaamme ristivalidoituihin ennusteisiin prek. 3
> pre = predict(mod) > plot(pre, prek, col = take, pch = 16) > abline(0, 1) Perinteinen (mutta huono) tapa on sirotakuvio, jossa arvot plotataan toisiaan vasten. Argumentti col muuttaa kunkin ennusteen arvoa sen mukaan mihin viidestä otoksesta ne kuuluvat. Värin voi ilmoittaa monella tavalla, ja yksi vaihtoehto on numero. Tästä enemmän myöhemmin, mutta viisi ensimmäistä väriä ovat oletusasetuksessa: > palette() [1] "black" "red" "green3" "blue" "cyan" "magenta" [7] "yellow" "gray" Komento abline voidaan antaa vaihtoehtoisissa muodoissa. Jos sen argumenttina on kaksi lukua, ensimmäistä pidetään vakiona ja toista kulmakertoima, eli abline(0,1) piirtää samanarvosuoran. Arvioitaessa ennusteiden hyvyyttä, parempi tapa kuin edellä on esittää ennustevirheet (residuaalit) ennustettua arvoa tai todellista arvoa vasten: > plot(pre, prek - pre, col = take, pch = 16) > abline(h = 0, col = "gray") > attach(kurssi) > plot(spno, prek - spno, col = take, pch = 16) > abline(h = 0, col = "gray") Nyt abline piirtää vaakaviivan kohtaan (h=0); argumentilla v=mean(spno) se piirtäisi pystyviivan lajiluvun keskiarvon kohdalle. Näistä kuvista ilmenee paremmin, että ennusteet ovat huonoja ja ennustevirheet ovat suuria. Lisäksi näkyy, että ennusteet ovat systemaattisesti harhaisia: korkea lajiluku arvioidaan liian alhaiseksi ja alhainen liian korkeaksi. Ilmeisesti malli on melko kelvoton ennustaja. Edellä teimme ristivalidoinnin käsin. Virheiden mahdollisuus oli suuri ja työ tylsää. Käyttämällä for-komentoa. Komennon muoto on for(i in 1:5), mikä tarkoittaa että i saa arvot 1... 5. Tällöin koko ristivalidointiketju voidaan korvata vertailulla indeksiin i: > for (i in 1:5) { + k5 = update(mod, data = kurssi[take!= i, ]) + prek[take == i] = predict(k5, newdata = kurssi[take == + i, ]) + } Tämän menettelyn ansiona on ainakin että virheiden mahdollisuus on pienempi ja että on helpompi toistaa ristivalidointi sekoittamalla uudelleen take-vektori. Tämä on hyvä mallin hyvyyden arvioimiseksi ja välttämätöntä, jos haluamme havaintokohtaisia arvioita ennustustarkkuudesta. Tällöin ristivalidointi voidaan joutua toistamaan esim. sata kertaa. Muutama saana ennusteiden hyvyyden arvioinnista. Edellä näimme jo, että ihanteellin graafinen kuvio on ennustevirhe vastaan ennuste tai todellinen arvo. Numeerisia mittoja taas on usein kaksikin erilaista: harha ja erhe. Harha on ennustevirheiden keskiarvo ja erhe on ennustevirheiden keskivirhe (harhaisten keskiarvon suhteen). 4
> mean(prek - spno) [1] -0.1699 > sd(prek - spno) [1] 10.37 > take = sample(take) > for (i in 1:5) { + k5 = update(mod, data = kurssi[take!= i, ]) + prek[take == i] = predict(k5, newdata = kurssi[take == + i, ]) + } > mean(prek - spno) [1] -0.1608 > sd(prek - spno) [1] 10.62 Ilmeisestikin uudelleen otostus muutti jossain määrin arvioitamme mallin hyvyydestä. Yksittäisen havainnon ennusteeseen vaikuttaa etenkin se, mihin seuraan se on sattunut joutumaan. Tätä pystyy usein parhatien tarkastelemaan grafiikan avulla. Sitten hieman asiaa, jonka yli hyppäsin luennolla: ehkä tämä on helpompi ymmärtää luettuna. Harhan ja erheen sijaan tai lisäksi käytetään usein yhtä mittaa, josta käytetään nimeä neliöity keskivirhe (Root Mean Squared Error, rmse). Se määritellään: > rmse = sqrt(sum((prek - spno)^2)/length(spno)) > rmse [1] 10.57 Neliöidyt virheet lasketaan yhteen, jaetaan havaintojen lukumäärällä ja lopuksi otetaan neliöjuuri. Tämä on hyvin samantapainen kuin keskihajonta, mutta keskihajonta on laskettu (harhaisen) keskiarvon suhteen eikä oikean arvon suhteen. Tämän takia myös keskihajonnassa on jakajana n 1 eikä n: jakaja on vapausasteet, ja keskihajonnan laskussa on kulutettu yksi vapausaste keskiarvoa laskettaessa. Rmsen laskussa taas ei ole laskettu keskiarvoja, joten jakaja on n. 2 Hieman grafiikkaa Lopuksi hieman grafiikasta. Tässäkään kappaleessa ei ole kuvia, mutta voitte itse piirtää kuvat R-istunnossa. R:n plot-komento on monipuolinen. Sen oletustoiminta riippuu siitä, mitä plotatann. Oletustoiminta on usein melko yksinkertainen, mutta kuvaa pystyy muuttamaan antamalla argumentteja. Useimmilla argumenteilla on oletusarvo: jos se kelpaa, argumentteja ei tarvitse antaa. Vertailun vuoksi yksinkertainen ja hieman monimutkaisempi kuva: 5
> plot(spno, prek) > plot(spno, prek, col = take, pch = 16) > abline(h = 0, col = "lavender") Edellisessä kuvassa pisteen väri määräytyi vektorin take numeerisen arvon perusteella. Yllä näimme jo, oletusvärit. Numeroita vastaavia värejä voi kuitenkin muuttaa määrittelemääl uusi väripaletti. Valmiita vaihtoehtoja on lukuisia. Tässä sateenkaari, kuumat värit ja maastonvärit (lopuksi kuitenkin palautamme oletuspaletin): > palette(rainbow(5)) > palette() [1] "red" "#CCFF00" "#00FF66" "#0066FF" "#CC00FF" > palette(heat.colors(5)) > palette() [1] "red" "#FF5500" "#FFAA00" "yellow" "#FFFF80" > palette(terrain.colors(5)) > plot(spno, prek - spno, col = take, pch = 16) > palette(heat.colors(10)) > plot(spno, prek - spno, col = take, pch = 16, cex = 2) > palette("default") Värien nimet ovat tällä kertaa yleensä rgb-lukuja (tai nimiä) eivätkä kovin helposti avaudu asiaan vihkiytymättömälle. Myös plottaussymbolia (pch) voi muuttaa. Myös perussymbolit on numeroitu, tai vaihtoehtoisesti voimme käyttää merkkiä kuten pch="+" tai oletussymbolille pch="o" taipch=1. Isommat numerot merkitsevät täytettyjä symboleja (esim 16 on täytetty ympyrä) ja vielä isommat numerot kahdella värillä täytettäviä symboleita: > plot(spno, prek - spno, col = take, bg = (take + 2), pch = 21, + cex = 1.5) Tässä taustan väri bg kertoo täytteen värin ja col reunan värin. Tällaiset värit voi myös ilmoittaa niminä (joita on paljon: katso colours()) ja myös symbolin kokoa voi muttaa (cex): plot(spno, prek-spno, col= red, bg= yellow, pch=21, cex=1.5). Monimutkaisemmista graafisten parametrien muutoksesta puhumme perusteellisemmin seuraavalla kerralla. 2.1 Kuvan piirtäminen R:n peruskuva syntyy vaiheittain. Tässä yksinkertainen kuva: > plot(spno, prek - spno) Tätä kuvaakin piirrettäessä on laskettu monta asiaa ja varauduttu siihen, että käyttäjä myöhemmin haluaa lisätä kuvaan symboleja tai tekstejä. Seuraava komento ei piirrä mitään näkyvää, mutta se on tehnyt samat laskut ja varaukset: 6
> plot(spno, prek - spno, type = "n", axes = F, xlab = "", + ylab = "") Saamme tyhjän kuvan, mutta voimme lisätä tähän puuttuvat aiheet. Aluksi pisteet, joiden piirtämisen esti type="n"): > points(spno, prek - spno, pch = 16, col = take) Akseleita ei piirretty eikä nimetty, koska olimme myös tämän estäneet, mutta voimme lisätä ne nyt: > axis(2, col = "red") > axis(1, col = "red") > box(col = "blue") > title(xlab = "lajiluku") > title(ylab = "Ennustevirhe") Kuvan sivut on numeroit vastapäivään pohjalta alkaen: alareuna on siis 1 ja vasen sivu on 2. Komennolle axis on kerrottava mille sivulle se piirretään. Akselin asteikot laskettin jo tyhjä kuva piirrettäessä, joten nyt ne vain lisätään kuvaan. Komento axis tuottaa kuitenkin vain asteikon annettuun reunaan eivätkä akselit yhdy kulmassa. Oletuskuvassa on myös box, joka kehystää kuvan. Kuvaa piirrettäessä reuniin jätettiin tyhjää tilaa, jotta sinne mahtuisi lisäämään tekstiä: > title(main = "5-kertainen ristivalidointi") > title(sub = "Piileväaineisto, 110 järveä") > mtext("r-kurssi 2005", 4) Jos olemme varmoja, ettemme halua lisätä myöhemmin tekstiä reunoihin, voimme kaventaa reunoja, jolloin kuva-ala kasvaa. Tämä on hyödyllistä etenkin, kun piirrämme useamman paneelin (mfrow) samaan kuvaan: > par(mfrow = c(2, 2)) > plot(mod) > omar = par(mar = c(4, 4, 1, 1) + 0.1) > plot(mod) > par(omar) Graafinen parametri mar määrittää reunan koon tekstiriveinä tutussa järjestyksessä: ala, vasen, ylä, oikea. Kun parametri muutetaan, sen vanha arvo palautetaan. Talletimme sen muuttujaan omar, joten voimme helposti kuvan piirron jälkeen palauttaa asetukset. 7