8 Porodica funkcija apply
8.1 Što su apply
funkcije?
Vrlo često se poznavanje osnova jezika R procjenjuje znanjem korištenja tzv. apply
porodice funkcija dostupnih u paketu base
. Ove funkcije specifično su dizajnirane za provođenje repetitivnih zadataka nad skupovima podataka i kao takve zamjenjuju programsku logiku koja bi se u nekom drugom jeziku realizirala programskom petljom. Dodatno, ove funkcije u pravilu primaju druge funkcije kao ulazne argumente i tako u izvjesnoj mjeri potiču paradigmu funkcionalnog programiranja.
Naziv porodice potiče od činjenice da funkcije iz nje imaju sufiks “apply”. Neke funkcije iz ove porodice su:
apply
lapply
sapply
vapply
tapply
,mapply
,rapply
…
Sve ove funkcije rade na sličan način - kao ulazne argumente primaju skup podataka, funkciju koju želimo primijeniti na elemente tog skupa te opcionalne dodatne parametre, a kao izlaz daju skup rezultata funkcije, najčešće “upakirane” u prigodni format. Razlika se uglavnom svodi na tipove ulaznih i izlaznih argumenata, te konkretne detalje oko provedbe same funkcije i/ili pripreme rezultata.
Ovu porodicu funkcija najlakše je upoznati preko primjera. Započnimo sa “osnovnom” funkcijom - apply
.
8.2 Funkcija apply
Funkcija apply
jedina je koja doslovno dijeli ime sa porodicom ovih funkcija. Namijenjena je radu s matricama (zapravo sa poljima, ali budući da se relativno rijetko radi sa strukturama koje imaju više od dvije dimenzije, ovdje ćemo se usredotočiti samo na matrice).
Sintaksa naredbe je sljedeća:
Ili, opisano riječima, za provođenje funkcije apply
:
- odaberemo matricu
- odlučimo se da li ju “režemo” po retcima ili stupcima
- primjenjujemo odabranu funkciju na svaki redak (ili stupac)
Ovisno o tome što funkcija radi, kao rezultat dobivamo matricu ili (što je češći slučaj) vektor.
Pokušajmo primijeniti ovu funkciju na konkretnom primjeru.
Zadatak 8.1 - funkcija apply
m <- matrix(1:9, nrow = 3, ncol = 3, byrow = TRUE)
# ispišite matricu `m`
# uz pomoć funkcije `apply` izračunajte
# i ispišite zbrojeve stupaca matrice `m`
# uz pomoć funkcije `apply` izračunajte
# i ispišite umnoške redaka matrice `m`
m
cat("------------\n") # za pregledniji ispis
apply(m, 2, sum)
cat("------------\n")
apply(m, 1, prod)
## [,1] [,2] [,3]
## [1,] 1 2 3
## [2,] 4 5 6
## [3,] 7 8 9
## ------------
## [1] 12 15 18
## ------------
## [1] 6 120 504
Ukoliko želimo nad retcima / stupcima provesti neki specifičan zadatak, za to vrlo često koristimo anonimnu funkciju, npr:
Zadatak 8.2 - funkcija apply
i anonimne funkcije
# uz pomoć funkcije `apply` za svaki redak matrice `m`
# izračunajte prirodni logaritam sume elemenata
# zaokružen na 2 decimale (funkcija `round`!)
## [1] 1.79 2.71 3.18
Ponovimo - funkcija apply
(i srodne funkcije) implicitno “rastavljaju” ulaznu podatkovnu strukturu na elemente. U primjerima gore ti elementi - retci ili stupci - su zapravo numerički vektori. Argument x
kojeg prima anonimna funkcija je upravo taj vektor, ili bolje reći svaki od tih vektora koji joj se prosljeđuju jedan po jedan. Rezultati funkcije se “pamte” i “pakiraju” u konačni rezultat.
Pokušajmo isprogramirati zadnji primjer bez korištenja funkcije apply
.
Zadatak 8.3 - petlja kao alternativa funkciji apply
# uz pomoć programske petlje za svaki redak matrice `m`
# izračunajte prirodni logaritam sume elemenata
# zaokružen na 2 decimale (funkcija `round`!)
## [1] 1.79 2.71 3.18
Ako usporedimo sintakse primjera sa i bez korištenja funkcije apply
, možemo se uvjeriti koliko je sintaksa koja koristi apply
zapravo “čišća” i jasnija. Ako koristimo petlje moramo eksplicitno navesti logiku prolaženja strukturom i čuvanja međurezultata, što odvlači pažnju od opisa posla kojeg zapravo želimo obaviti.
Što ako apply
funkciji želimo proslijediti više parametara? Npr. recimo da umjesto gornje funkcije koja izvlači prvi element retka želimo funkciju sa dva parametra - prvi je vektor a drugi cijeli broj koji označava koji broj treba izvući. Odgovor je jednostavan - dodatne parametre jednostavno navedemo na kraju poziva funkcije.
# apply funkcija i ulazna funkcija sa više parametara
apply(m, 1, function(x,y) x[y], 2) # izvlačimo drugi element svakog retka
Konačno, treba napomenuti da za sličnu obradu podataka u matričnom obliku ne moramo nužno koristiti apply
- dosta popularnih operacija kao što su zbrajanje elemenata redaka ili stupaca, računanje prosjeka elemenata redaka i stupaca i sl. već je implementirano kroz funkcije kao što su rowSums
, colSums
, rowMeans
, colMeans
i sl. One su jednostavnije za uporabu, no specijalizirane - za dodatnu fleksibilnost najčešće je apply
najpogodnija opcija.
8.3 Funkcije lapply
, sapply
i vapply
Ime funkcije lapply
dolazi od “list apply” - tj. apply
funkcija koja radi sa listama. Jednostavno - radi se o funkciji koja će kao ulazni argument primiti listu i neku funkciju, primijeniti funkciju na svaki pojedini element liste i vratiti opet rezultat u obliku liste.
Zadatak 8.4 - funkcija lapply
l <- list(a = 1:3, b = rep(c(T, F), 10), c = LETTERS)
# pomoću funkcije `lapply` izračunajte duljinu (broj elemenata)
# svakog elementa liste `l`
## $a
## [1] 3
##
## $b
## [1] 20
##
## $c
## [1] 26
Isto kao kod funkcije apply
, kod funkcije lapply
često kao parametar koristimo anonimne funkcije. Sljedeći zadatak nema posebnu praktičnu uporabu, ali će nam pomoći da shvatimo funkcioniranje funkcije lapply
te usvojimo činjenicu kako anonimne funkcije ne moraju nužno biti kratke i jednostavne.
Zadatak 8.5 - funkcija lapply
i anonimne funkcije
# obradite elemente liste `l` na sljedeći način:
# - izračunajte srednju vrijednost ako se radi o numeričkom vektoru
# - prebrojite vrijednosti TRUE ako se radi o logičkom vektoru
# - ispišite duljinu vektora za sve ostale slučajeve
# koristite funkciju `lapply` i anonimnu funkciju
# ne zaboravite da i anonimna funkcija može koristiti blokove!
# obradite elemente liste `l` na sljedeći način:
# - izračunajte srednju vrijednost ako se radi o numeričkom vektoru
# - prebrojite vrijednosti TRUE ako se radi o logičkom vektoru
# - ispišite duljinu vektora za sve ostale slučajeve
# koristite funkciju `lapply` i anonimnu funkciju
# ne zaboravite da i anonimna funkcija može koristiti blokove!
lapply(l, function(x) {
if (is.numeric(x)) {
mean(x)
} else if (is.logical(x)) {
sum(x)
} else length(x)
})
## $a
## [1] 2
##
## $b
## [1] 10
##
## $c
## [1] 26
Funkcija lapply
je u suštini dosta jednostavna za korištenje i baš zbog te činjenice vrlo popularna. No nakon što ju koristimo jedno vrijeme može nas zasmetati činjenica da ona uvijek kao rezultat vraća listu, iako bi nam nekad više odgovarala neka druga podatkovna struktura, kao npr. vektor, pogotovo ako rezultantna lista ima kao elemente jednostavne brojeve. Upravo iz ovog razloga R nudi funkciju unlist
za “pojednostavljivanje” liste u vektor ako ona sadrži jednostavne elemente.
Zadatak 8.6 - funkcija unlist
l <- list(a = 1:10, b = 10:20, c = 100:200)
# izračunajte srednje vrijednosti elemenata liste `l`
# rezultate ispišite kao numerički vektor
# koristite lapply i unlist
## a b c
## 5.5 15.0 150.0
Prikazana kombinacija lapply
i unlist
će nam kao rezultat dati jednodimenzionalni vektor, što nam u velikom broju slučajeva odgovara. No ponekad bi nam više odgovarala neka druga podatkovna struktura - npr. matrica. U ovom slučaju potreban nam je i dodatni korak preoblikovanja jednodimenzionalnog vektora u matricu uz pomoć funkcije matrix
, pri čemu moramo eksplicitno zadati broj redaka i stupaca.
Može se postaviti pitanje - zašto funkcija lapply
ne bi mogla “pogledati” rezultat kojeg je dobila i sama odrediti optimalnu podatkovnu strukturu za oblikovanje rezultata (vektor, matrica ili lista)? Upravo je to ideja iza funkcije sapply
, ili “simplified list apply”. Ova funkcija prvo interno obavlja lapply
, a potom se rezultat pojednostavljuje na vektor, matricu ili polje, ovisno o karakteristikama dobivenih rezultata.
Zadatak 8.7 - funkcija sapply
l <- list(a = 1:10, b = 10:20, c = 100:200)
# izračunajte medijane elemenata liste `l`
# i rezultate ispišite kao numerički vektor
# koristite funkciju `sapply`
# izvucite prvi i zadnji element svakog od elemenata liste `l`
# koristite `sapply` i anonimnu funkciju
sapply(l, median)
cat("------------\n") # za pregledniji ispis
sapply(l, function(x) c(x[1], x[length(x)]))
## a b c
## 5.5 15.0 150.0
## ------------
## a b c
## [1,] 1 10 100
## [2,] 10 20 200
Uočite da smo kao rezultat zadnjeg primjera dobili matricu, ali da ju je R oblikovao “po stupcima”. Ukoliko bismo htjeli matricu sa elementima poredanim po retcima, za to nažalost ne možemo koristiti sapply
jer se matrica formira interno, bez mogućnosti prosljeđivanja parametra byrow = T
. Za dobivanje takve matrice jedna opcija nam je već spomenuta kombinacija funkcija lapply
, unlist
i matrix
ili - što je jednostavnije - transponiranje rezultata sapply
uz pomoć funkcije t
(od engl. transpose).
Funkcija sapply
je prilično omiljena zbog svoje jednostavnosti i učinkovitosti tako da se relativno često koristi u interaktivnoj analizi. S druge strane, korištenje ove funkcije u programskim skriptama se ne preporučuje budući da je rezultat u općenitom slučaju nepredvidiv - npr. skripta može u nastavku programskog koda očekivati matricu, a funkcija sapply
je zbog specifičnosti ulaznih podataka vratila vektor, što može uzrokovati nepredviđene rezultate u nastavku skripte a što nije lako naknadno uočiti te dijagnosticirati gdje je nastala greška.
Ukoliko razvijamo vlastite programe u R-u i želimo koristiti sapply
, onda će nam bolji izbor predstavljati funkcija vapply
(od engl. “verified sapply”) koja radi identično funkciji sapply
, ali koristi dodatni parametar nazvan FUN.VALUE
pomoću kojeg eksplicitno definiramo kakvo “pojednostavljenje” očekujemo. Npr. numeric(3)
znači da bi rezultat primjene funkcije na svaki element originalne liste trebao biti numerički vektor od tri elementa. Ukoliko se rezultat za bilo koji element liste razlikuje od očekivanog, funkcija će izbaciti grešku.
Zadatak 8.8 - funkcija vapply
podaci <- list(brojevi <- c(1:5),
imena <- c("Ivo", "Pero", "Ana"),
abeceda <- LETTERS)
# razmislite koji će od sljedećih poziva biti uspješan,
# a koji će izbaciti grešku
# rezultate provjerite na konzoli
vapply(podaci, length, FUN.VALUE = numeric(1))
vapply(podaci, function(x) as.character(c(x[1], x[2])), FUN.VALUE = character(2))
vapply(podaci, function(x) as.logical(x), FUN.VALUE = character(1))
Konačno, vratimo se nakratko funkciji lapply
i razmotrimo jednu bitnu činjenicu - ona je namijenjena uporabi nad listama, a podatkovni okviri su zapravo liste. Drugim riječima, funkcija lapply
je vrlo zgodna za obradu tabličnih podataka kada želimo određenu funkciju primijeniti na stupce podatkovnog okvira.
Jedna od češćih operacija koje se provode kod analize podataka jest tzv. “normalizacija” numeričkih stupaca podatkovnog okvira - tj. svođenje svih numeričkih vrijednosti na “normalnu” distribuciju aritmetičke sredine 0 i standardne devijacije 1. Ovo možemo uraditi tako da svaku pojedinu vrijednost umanjimo za aritmetičku sredinu stupca (funkcija mean
) te podijelimo sa standardnom devijacijom stupca (funkcija sd
). Ovo je odličan scenarij za demonstraciju korištenja funkcije lapply
.
Zadatak 8.9 - funkcija lapply
i podatkovni okviri
df <- data.frame( a = 1:10, b = seq(100, 550, 50),
c = LETTERS[1:10], d = rep(c(T,F), 5),
e = -10:-1)
# normalizirajte numeričke stupce uz pomoć funkcije `lapply`
# ostale stupce nemojte mijenjati
# normalizirane vrijednosti zaokružite na tri decimale
# rezultat pohranite u varijablu df
# ispišite df
## $a
## [1] -1.486 -1.156 -0.826 -0.495 -0.165 0.165 0.495 0.826 1.156 1.486
##
## $b
## [1] -1.486 -1.156 -0.826 -0.495 -0.165 0.165 0.495 0.826 1.156 1.486
##
## $c
## [1] A B C D E F G H I J
## Levels: A B C D E F G H I J
##
## $d
## [1] TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE
##
## $e
## [1] -1.486 -1.156 -0.826 -0.495 -0.165 0.165 0.495 0.826 1.156 1.486
Vidimo da smo nakon korištenja lapply
dobili listu te da ukoliko želimo rezultat u obliku podatkovnog okvira moramo dodati još jedan korak korištenja funkcije as.data.frame
. Ukoliko tražimo jednostavniji način koji odmah daje podatkovni okvir kao rezultat, postoji jedan zgodan “trik” kojeg ćemo objasniti u nastavku.
Pogledajmo rješenje prethodnog zadatka, konkretno red gdje rezultat poziva funkcije lapply
pohranjujemo u varijablu df
koji se sastoji u tome da umjesto pridruživanja rezultata samoj varijabli (df <-
) rezultat pridružimo varijabli sa “praznim” operatorom indeksiranja (df[] <-
):
Na ovaj način R neće napraviti “novu” varijablu imena df
, već će rezultat funkcije lapply
upisati u “sve retke i stupce okvira df
”. Time smo postigli da smo umjesto liste rezultat dobili u obliku podatkovnog okvira, što smo zapravo i htjeli. Upravo zbog ovoga vrlo često ćemo u R skriptama vidjeti sličnu sintaksu (df[] <- lapply...
). Pokušajte preinačiti gornji primjer na navedeni način i uvjerite se da će rezultat biti podatkovni okvir.
Još jedan često korišteni trik u radu sa podatkovnim okvirima i funkcijama iz porodice apply
jest sljedeća naredba:
Ova naredba nam zapravo daje odgovor na pitanje - kojeg su tipa stupci navedenog podatkovnog okvira? Iako postoje i drugi načini da dođemo do ove informacije, ovaj način popularan je kako zbog kompaktnosti rezultata, tako i neovisnosti o dodatnim paketima.
8.4 Ostale funkcije iz porodice apply
i dostupne alternative
U prethodnim poglavljima naveli smo vjerojatno najpopularnije članove porodice apply
. Ova porodica broji još članova, uključujući i neke koji nemaju sufiks -apply:
mapply
, koja primjenjuje funkcije paralelno nad više podatkovnih strukturarapply
, koja rekurzivno primjenjuje funkcije unutar strukturetapply
, koja primjenjuje funkcije nad podskupovima unutar strukture definirane faktorimaMap
, inačicamapply
koja ne pojednostavljuje rezultatby
, inačicatapply
predviđena za podatkovne okvire- itd.
Razlog zašto ove funkcije nećemo detaljno obrađivati jest dvojak: prvo, kao što je već rečeno, ove funkcije se u praksi primjenjuju puno rjeđe od funkcija koje smo prikazali u prethodnim poglavljima. Drugo, porastom popularnosti jezika R pojavio se i veliki broj paketa orijentiranih upravo poboljšanju postojećih funkcija jezika R u smislu lakšeg i učinkovitijeg programiranja, poglavito u radu s podatkovnim okvirima.
Ukoliko tražimo zgodne alternative funkcijama iz porodice apply
, preporučuje se pogledati neke od sljedećih paketa
plyr
- iznimno popularan paket koji između ostalog nudi niz funkcija vrlo srodnihapply
¸funkcijama, ali izvedenih na način da imaju konzistentan potpis te eksplicitno definirane ulazne i izlazne oblike koji se lako čitaju iz samog imena funkcije (konkretno, prvih slova); tako funkcijallply
kao ulaz prima te kao izlaz daje listu, dok funkcijamdply
kao ulaz prima matricu a kao izlaz daje podatkovni okvirpurrr
- paket koji zamjenjuje funkcije porodiceapply
sa funkcijama koje odgovaraju sličnim funkcijama iz drugih programskih jezika za funkcijsko programiranje; budući da se primjena iste funkcije na niz elemenata neke podatkovne strukture u funkcijskim jezicima često zove “mapiranje”, niz funkcija ovog paketa nosi prefiksmap_
a imena funkcija često odgovaraju očekivanim rezultatima (npr.map2_lgl
znači da kao rezultat očekujemo logički vektor, amap2_df
podatkovni okvir)dplyr
- relativno novi paket koji u izvjesnom smislu predstavlja “nasljednika” paketaplyr
za rad sa podatkovnim okvirima; funkcije ovog paketa nisu toliko orijentirane zamjeni funkcijama porodiceapply
koliko pružanju svojevrsne platforme za rad sa podatkovnim okvirima na način sličan jezicima domenski orijentiranim upravo za tu svrhu, kao što je npr. jezik SQL
U lekciji o upravljanju podatkovnim skupovima upoznati ćemo se upravo sa paketom dplyr
, upravo zbog činjenice da ovaj paket uvelike olakšava i ubrzava proces analize podataka te je iznimno dobro prihvaćen u R zajednici.
Zadaci za vježbu
- Uzmimo matricu
m
stvorenu sljedećom naredbom:
Uz pomoć funkcije apply
i nove anonimne funkcije stvorite vektor koji će sadržavati prvi parni element svakog retka, ili nulu ako pripadajući redak nema parnih elemenata.
- Sljedeće naredbe stvoriti će listu od 100 elemenata gdje će svaki element biti numerički vektor nasumične duljine od 1 do 10.
Uz pomoć funkcija lapply
/ sapply
(i dodatnih naredbi ako je potrebno) stvorite:
- numerički vektor
v
sa duljinama elemenata liste - listu
l
sa normaliziranim numeričkim vektorima originalne liste - numerički vektor
ind4
sa indeksima svih elemenata liste koji sadrže broj 4 - podatkovni okvir
df5
koji kao stupce sadrži sve elemente liste duljine 5
Programirajmo u R-u by Damir Pintar is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Based on a work at https://ratnip.github.io/FER_OPJR/