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

##      [,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


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


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.

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

## $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


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

##     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

##     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


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

## $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 struktura
  • rapply, koja rekurzivno primjenjuje funkcije unutar strukture
  • tapply, koja primjenjuje funkcije nad podskupovima unutar strukture definirane faktorima
  • Map, inačica mapply koja ne pojednostavljuje rezultat
  • by, inačica tapply 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 srodnih apply¸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 funkcija llply kao ulaz prima te kao izlaz daje listu, dok funkcija mdply kao ulaz prima matricu a kao izlaz daje podatkovni okvir
  • purrr - paket koji zamjenjuje funkcije porodice apply 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 prefiks map_ a imena funkcija često odgovaraju očekivanim rezultatima (npr. map2_lgl znači da kao rezultat očekujemo logički vektor, a map2_df podatkovni okvir)
  • dplyr - relativno novi paket koji u izvjesnom smislu predstavlja “nasljednika” paketa plyr za rad sa podatkovnim okvirima; funkcije ovog paketa nisu toliko orijentirane zamjeni funkcijama porodice apply 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

  1. 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.

  1. 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

Creative Commons License
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/