R – Funktionaalinen ohjelmointi

R Ei kommentteja

Funktionaalinen ohjelmointi (eng. functional programming) on R:n taustalla oleva ohjelmointiparadigma. Lyhyesti sanottuna kieli perustuu pääasiassa matemaattisten funktioiden käyttöön, ja tämä ominaisuus on yksi syy R:n suosion taustalla. R sisältää suuren määrän valmiita funktioita [kuten mean(), sd()], mutta lisäksi sillä on helppo luoda omia funktioita, sekä soveltaa purrr -paketin funktionaaliseen ohjelmointiin luotuja funktioita, joita voi käyttää valmiiden sekä omien funktioiden iteroimiseen, eli ajamaan sama funktio esim joka riville erikseen yhdellä komennolla.

Aineiston siistiminen, analysointi ja kuvien hienosäätö vie huomattavan määrän aikaa tutkimusta tehtäessä. Näihin kaikkiin edellä mainittuihin osioihin kuuluu mekaanista leikkaa-liimaa -tyylin toisteista koodin kirjoittamista, mikä on usein sellaisenaan melko tehotonta ohjelmointia. Koska ohjelmoinnissa kannattaa pyrkiä mahdollisimman tehokkaaseen ja turhaa toistoa välttävään kirjoitustyyliin, kannattaa opetella kirjoittamaan omia funktioita. Omien funktioiden hyötynä on kirjoitetun koodin lyheneminen, parantunut luettavuus sekä kirjoitusvirheistä johtuvien virheiden väheneminen.

Funktion rakenne

R:ssä funktio rakennetaan funktiolla function(), jonka sisälle tulee argumentit eli tarvittavat syötettävät arvot, joilla funktion toiminto totetutetaan. Funktio tallennetaan itse nimettyyn objectiin [funktion_nimi]. function() käskyn jälkeen tulee aaltosulkeet joihin kirjoitetaan funktion runko, eli toiminto joka halutaan toteuttaa.

funktion_nimi <- function(Argumentit){
  # Funktion runko
}

Funktion rakenne

  • Funktion nimi – Vapaavalintainen nimi luodulle funktiolle. Funktio tallentuu R ympäristöön tällä nimellä.
  • Argumentit – Funktioon syötettävät arvot, joilla funktion rungossa toteutetaan halutut laskut tai muut toiminnot. Argumenteille voidaan myös asettaa haluttu vakioarvo, jota funktio käyttää ellei arvoa erikseen määritellä funktiota käyttäessä.
  • Funktion runko – toteuttaa halutun laskun tai muun toimenpiteen. Tulee sisältää sulkeissa edellä mainitut argumentit.

Funktiot voi kirjoittaa myös nimettominä (eng. anonymous functions), eli ns. lamda-funktioina. Silloin funktiota ei tallenneta erikseen vaan funktion käyttö tapahtuu samalla rivillä kun millä funktio luodaan.

(function (Argumentit) Runko) (sijoitettava arvo)

Nimetön funktio lyhentää koodia entisestään ja on varsin käyttökelpoinen kun muun koodin keskellä halutaan tehdä jokin laskutoimitus esimerkiksi kuvaa varten.

Esimerkkinä funktio joka kertoo siihen sijoitetun arvon (1) kolmella. Funktion argumenttina on x ja runkona x*3. Tallennetaan funktio nimellä kertoja().

kertoja <- function(x){
  x*3
}

kertoja(1)
> 3

Sama funktio nimettömänä:

(function (x) x*3) (1)

> 3

Käytännönläheisempänä esimerkkinä tehdään funktio, joka muuttaa paunat kilogrammoiksi. Argumentiksi laitetaan lbs – eli paunat, jotka funktio jakaa muuntokertoimella (2.2046), jotta ne saadaan muutettua kilogrammoiksi:

lbs_to_kg <- function(lbs){
  lbs/2.2046
}

Nyt paunoja vastaava kilomäärä saadaan laskettua syöttämällä haluttu paunamäärä funktioon lbs_to_kg().

lbs_to_kg(100)

[1] 45.3597

Eli 100 paunaa vastaa noin 45 kiloa.

Sama funktio ja muunnos sadasta paunasta kiloiksi voidaan kirjoittaa nimettömästi seuraavalla tavalla:

(function (lbs) lbs/2.2046) (100)

> 45.3597

Tilanteet, joissa kannatta harkita oman funktion kirjoittamista.

Aineiston puhdistamisen toistaminen

Käyttökelpoinen paikka omalle funktiolle on tilanne, joissa aineiston muuttujia muokataan toiseen muotoon. Jos samat muutokset tulee tehdä useampaan eri aineistoon on helpompaa luoda muutoksista funktio ja käyttää tätä funktiota aina tarpeen tullen.

Esimerkkinä on funktio, joka laskee henkilötunnuksesta syntymäajan. Funktio kannattaa aina rakentaa palasista, jotka varmasti toimii ja lopuksi ne liitetään yhteen funktioksi.

Ensin luodaan aineisto jossa on kolme keksittyä henkilötunnusta.

hetudata <- tibble(
  hetu = c("051191-999Ä",
           "110702A999Ö",
           "211211A777Ä")
)
	
hetu
051191-999Ä
110702A999Ö
211211A777Ä

Suomalainen henkilötunnus koostuu syntymäajasta, jossa ensimmäiset 2 numeroa ovat päiviä, seuraavat 2 kuukausia ja sitä seuraavat 2 vuosia. Syntymäajan jälkeen tulee joko “-” (syntyny 1900 luvulla) tai “A” (syntynyt 2000 luvulla). Syntymäaika saadaankin erottelemalla päivä, kuukausi, vuosi, sekä “-” tai “A” omiin muuttujiin. Tässä käytetään funktiota substr(), joka valitsee järjestysnumeron mukaan halutut merkit uuteen muuttujaan. Lisätään samalla oikea vuosituhat ifelse() funktiolla, joka luo uuden sarakkeen niin, että jos hetu-muuttujan 7. merkki on “-“, arvo on 19 ja jos se on jotain muuta niin arvo on 20.

hetudata %>% 
  mutate(date = substr(hetu,1,2),
         month = substr(hetu,3,4),
         year = substr(hetu,5,6),
         sepr = substr(hetu,7,7),
         decade = ifelse(sepr=="-", 19, 20))

Seuraavaksi yhdistetään vuosikymmen ja vuosi käyttämällä funktiota unite() ja sen jälkeen yhdistetään vuosi, kuukausi ja päivämäärä käyttämällä samaa funktiota. Lisäki uusi syntymäaika -muuttuja muutetaan päivämäärä -formaattiin funktiolla as.Date, ja poistetaan turhaksi jäänyt muuttuja sepr.

hetudata %>% 
  mutate(date = substr(hetu,1,2),
         month = substr(hetu,3,4),
         year = substr(hetu,5,6),
         sepr = substr(hetu,7,7),
         decade = ifelse(sepr=="-", 19, 20)) %>% 
  unite(yr, c("decade", "year"), sep="") %>% 
  unite(syntymäaika, c("yr", "month", "date"), sep="-") %>% 
    mutate(syntymäaika = as.Date(syntymäaika)) %>% 
  select(-sepr) # Poistetaan turha muuttuja

Näin saamme aineistoon uuden muuttujan jossa on syntymäaika, joka on päivämäärä (as.Date) -formaatissa. Koska haluamme tehdä saman muutokset useampaan aineistoon ja käyttää näitä muutoksia kätevästi jatkossa, kannattaa se muuttaa funktioksi.

Funktion saa kätevästi luotua kuten edellisissä esimerkeissä. Nimetään uusi funktio nimellä Hetulaskuri ja laitetaan argumenteiksi data (aineisto jota käytetään) ja hetu (muuttuja, jossa henkilötunnus on). Tämän jälkeen laitetaan aaltosulkeet ja avataan paketit, joita funktio käyttää (Tidyverse). Sitten kopioidaan edellä käytetty koodipätkä ja muutetaan koodissa olevan aineiston (edellä hetudata) kohdalle sama nimi kuin argumenteissa (data). Samoin argumentti hetu tulee funktioon kohtiin jossa muuttujaa hetu käytetään.

Hetulaskuri <- function(data, hetu) {
  library(tidyverse)
data %>% 
  mutate(date = substr(hetu,1,2),
         month = substr(hetu,3,4),
         year = substr(hetu,5,6),
         sepr = substr(hetu,7,7),
         decade = ifelse(sepr=="-", 19, 20)) %>% 
  unite(yr, c("decade", "year"), sep="") %>% 
  unite(syntymäaika, c("yr", "month", "date"), sep="-") %>% 
    mutate(syntymäaika = as.Date(syntymäaika)) %>% 
  select(-sepr)
}

Nyt voimme käyttää funktiota yksinkertaisesti yhdellä rivillä, joka laskee automaattisesti aineistoon uuden sarakkeen jossa on henkilötunnuksesta laskettu syntymäaika.

Hetulaskuri(hetudata, hetu)

Analyysien toistaminen

Tilanteissa, joissa samaa koodia täytyy toistaa useamman kuin kerran peräkkäin, se kannattaa lähes poikkeuksetta kirjoittaa funktioksi, jolloin kirjoitettu koodi lyhenee ja kirjoitusvirheiden mahdollisuus pienenee. Sama pätee niin muuttujien puhdistamiseen, analyyseihin kuin kuviinkin.

Käytetään esimerkissä samaa mtcars -aineistoa kuin aiempien R-tutoriaalien esimerkeissä. Esimerkkinä tarkastelemme auton kulutuksen (mpg) ja painon (wt) suhdetta lineaarisella regressiolla. Olemme kuitenkin kiinnostuneita tutkimaan myös iskutilavuuden (disp) ja hevosvoimien (hp) vaikutusta kulutukseen, joten teemme kolme erillistä mallia.

lm(mpg ~ wt + cyl + gear, data=mtcars)
lm(mpg ~ disp + cyl + gear, data=mtcars)
lm(mpg ~ hp + cyl + gear, data=mtcars)

Tämä vaatii kuitenkin lähes saman rivin toistamisen useaan kertaan. Tämän vuoksi teemme lineaarisesta mallista funktion nimeltä lin_fun(), ja laitamme funktion argumentiksi muuttuja1, joka on mallissa muuttujan wt tilalla.

lin_fun <- function(muuttuja1) {
  lm(mpg ~ muuttuja1 + cyl + gear, data=mtcars)
}

Nyt voimme laskea lineaariset mallit kaikille muuttujille lisäämällä muuttuja1 -argumentin tilalle halutun muuttujan. Nyt luomamme funktio ei tunnista muuttujia wt, disp tai hp, koska dataa ei ole määritelty funktiossa erikseen. Tämän vuoksi nimeämme kaikki muuttujat erikseen käyttäen datan nimeä ja dollarimerkkiä (mtcars$).

lin_fun(mtcars$wt)
lin_fun(mtcars$disp)
lin_fun(mtcars$hp)

Näin saamme täysin samanlaisen outputin kuin tekemällä samanlaisen mallin jokaiselle muuttujalle erikseen.

Kuvien toistaminen

Myös kuvien kanssa voidaan toimia samoin. Otetaan esimerkkikuva datan visualisointi -artikkelista.

library(tidyverse) # Ladataan tarvittavat paketit

mtcars %>% 
  mutate(cyl=as.factor(cyl)) %>% 
  ggplot(aes(x=wt, y=mpg)) + 
  geom_point() +
  theme_bw() + 
  theme(text = element_text(size=16)) +
  labs(y="Miles/(US) gallon")

Haluamme tehdä vastaavan pisteparvikuvaajan edellä tehtyjen lineaaristen mallien mukaisesti niin että x-akselilla on muuttuja disp tai hp, se vaatisi koko 7-rivisen koodin uudelleenkirjoittamisen. Ongelma ratkaistaan rakentamalla funktion kuvasta. Argumentiksi tulee ainoastaan x-akselille tuleva muuttuja (x_akseli), sillä kaikki muu kuvassa pysyy ennallaan. Nyt argumentti x_akseli muutetaan ggplot() -koodiin kohdalle johon normaalisti laitettaisiin x-akselille tuleva muuttuja.

kuva1 <- function(x_akseli){
mtcars %>% 
  mutate(cyl=as.factor(cyl)) %>% 
  ggplot(aes(x=x_akseli, y=mpg)) + 
  geom_point() +
  theme_bw() + 
  theme(text = element_text(size=16)) +
  labs(y="Miles/(US) gallon")
}

Nyt ajamalla funktio kuva1() ja laittamalla argumentiksi haluttu x-akselin arvo, saadaan printattua kolme kuvaa identtisellä teemalla ja y-akselilla.

kuva1(mtcars$wt)
kuva1(mtcars$disp)
kuva1(mtcars$hp)

Tällä yksinkertaisella funktiolla lyhennetään siis huomattavasti kirjoitetun koodin pituutta, sillä 3 x 7 = 21 rivin sijasta meidän tarvitsee kirjoittaa funktio kerran (9 riviä) ja printtaaminen vaatii yhden rivin koodia. Mikäli kuvan koodi on pidempi (kuten yleensä on), säästettyjen rivien määrä kasvaa entisestään. On hyvä huomioida että x-akselin nimi määräytyy funktiossa määritellyn argumentin mukaisesti, joten se on “x_akseli”. Mikäli kuvasta haluaa printtauskelpoisen niin sen saa muutettua kätevästi lisäämällä ggplot():n labs() -funktioilla.

kuva1(mtcars$hp) +
  labs(x="Horsepower")

Yhteenveto

Ohjelmointiin kannattaa suhtautua maksimaalisen laiskuuden periaatteella minimoiden kirjoitettavan koodin pituus. On siis koodin ymmärrettävyyden ja toistettavuuden kannalta suositeltavaa miettiä aktiivisesti vaihtoehtoja, joilla koodia pystyisi lyhentämään. Omat funktiot ovat tärkeä työkalu, jota on hyvä hyödyntää aineiston puhdistamisessa, tilastollisissa analyyseissa sekä kuvissa , mikäli ne sisältävät toistoa. Tässä artikkelissa käytiin perusteet yksinkertaisen funktion rakentamiseen. Tällä pääsee hyvään alkuun, mutta ainoat rajat omien funktioiden soveltamisen mahdollisuuksiin luo niiden käyttäjän mielikuvitus. Suosittelemme siis kokeilemaan funktioita rohkeasti omassa data-analytiikassa.

Kirjoittanut Ville Ponkilainen, vertaisarvioinut Mikko Uimonen ja Aleksi Reito

Mitä mieltä olit artikkelin sisällöstä?

Klikkaa tähteä arvostellaksi artikkelin!

Keskiarvo 0 / 5. Arvostelujen lukumäärä: 0

Kukaan ei ole vielä äänestänyt, ole ensimmäinen

Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *