10 Rad sa datumima i znakovnim nizovima
10.1 Rad sa datumima
Upravljanje datumima i vremenskim oznakama uvijek predstavlja izazov kod rada sa podatkovnim skupovima budući da moramo voditi računa o stvarima kao što su:
- različiti oblici prikaza datuma i vremena
- različite interne reprezentacije
- različite vremenske zone
- razlika između matematičkog i kalendarskog poimanja vremenskih perioda
Jedan od češće korištenih standarda je tzv “unix vrijeme” (ili “POSIX vrijeme”) koja vrijeme računa kao broj sekundi protekao od ponoći 1. siječnja 1970. UTC (Coordinated Universal Time). Ne koriste svi informacijski sustavi POSIX vrijeme; npr. Microsoft Excel ima svoj format gdje broji dane od 1.1.1900., a onda za određeni dan broj sati, minuta i sekundi protekao od ponoći.
Programski jezik R ima tri klase za upravljanje datumima / vremenskim oznakama:
Date
za prikaz datumaPOSIXct
za kompaktni prikaz vremenske oznakePOSIXlt
za “dugi” prikaz vremenske oznake (u obliku liste)
10.1.1 Klasa Date
Klasu Date koristimo kada nas zanima datum ali ne i vrijeme neke obzervacije ili poslovnog događaja. Ova klasa nema svoj konstruktor, već objekte ovog tipa (najčešće) stvaramo uz pomoć sljedećih funkcija:
Sys.Date()
koja vraća današnji datumas.Date()
kojoj kao parametar prosljeđujemo znakovni niz koji reprezentira datum
Funkcija as.Date()
po default-u prihvaća datume oblika %Y-%m-%d
, gdje %Y
predstavlja četveroznamenkastu godinu a %m
i %d
dvoznamenkasti mjesec i dan. Ukoliko želimo interpretirati datum koji je zapisan u nekom drugom obliku onda moramo funkciji dodati parametar format koji će parametarski opisati oblik kojeg koristimo (npr. za 28/10/1978
bi vrijednost parametra format trebala biti %d/%m/%Y
). Sve moguće oblikovne specifikacije mogu se pogledati uz pomoć naredbe ?strptime
iako, kao što ćemo se uvjeriti kasnije, jednostavnija metoda jest koristiti se funkcijama paketa lubridate
.
Zadatak 10.1 - klasa Date
# ispišite današnji datum
# pretvorite sljedeće znakovne nizove u objekt tipa `Date` i ispišite rezultat na zaslon:
# '1986-12-27','2016-31-05', '17. 10. 2015.', '01#01#2001'
# ispišite današnji datum
Sys.Date()
# pretvorite sljedeće znakovne nizove u objekt tipa `Date` i ispišite rezultat na zaslon:
# '1986-12-27','2016-31-05', '17. 10. 2015.', '01#01#2001'
as.Date('1986-12-27')
as.Date('2016-31-05', format = '%Y-%d-%m')
as.Date('17. 10. 2015.', format = '%d. %m. %Y')
as.Date('01#01#2001', format = '%d#%m#%Y')
## [1] "2020-01-21"
## [1] "1986-12-27"
## [1] "2016-05-31"
## [1] "2015-10-17"
## [1] "2001-01-01"
Sa datumima možemo raditi jednostavne računske operacije kao što je dodavanje i oduzimanje dana (koristimo operatore + i - i cijele brojeve) ili razliku u danima između dva datuma (operator -).
Zadatak 10.2 - aritmetika s datumima
# ispišite koji je datum bio 1000 dana prije današnjeg datuma
# dodajte jedan dan datumima 28.2.2015. i 28.2.2016 i ispišite rezultat
# ispišite koliko je dana prošlo od 1.1.2000. do danas
# ispišite koji je datum bio 1000 dana prije današnjeg datuma
Sys.Date() - 1000
# dodajte jedan dan datumima 28.2.2015. i 28.2.2016 i ispišite rezultat
as.Date('2015-02-28') + 1
as.Date('2016-02-28') + 1
# ispišite koliko je dana prošlo od 1.1.2000. do danas
Sys.Date() - as.Date('2000-01-01')
## [1] "2017-04-26"
## [1] "2015-03-01"
## [1] "2016-02-29"
## Time difference of 7325 days
Zadnji izraz će zapravo rezultirati objektom klase difftime
koja označava vremenski interval. Ispis koristi tzv. “automatski” odabir jedinice (konkretno, parametar units
postavljen na "auto"
) koji će pokušati odabrati najprikladniju vremensku jedinicu za ispis. Ukoliko želimo eksplicitno odabrati koju vremensku jedinicu želimo (sekunde, minute, sate, dane ili tjedne) onda možemo umjesto operatora -
koristiti funkciju difftime
uz parametar units
postavljen na znakovni niz odabrane vremenski jedinice ("seconds"
, "minutes"
, itd.).
Zadatak 10.3 - funkcija difftime
# Koliko je prošlo tjedana između 1.3.2016. i 1.3.2015.?
# koristite funkciju `difftime`
# NAPOMENA: ne morate eksplicitno pozvati funkciju `as.Date`, funkcija `difftime`
# će to sama učiniti ako pošaljete datum u default-nom obliku
# koliko je prošlo sati od 1.3.2015. do danas?
# Koliko je prošlo tjedana između 1.3.2016. i 1.3.2015.?
# koristite funkciju `difftime`
# NAPOMENA: ne morate eksplicitno pozvati funkciju `as.Date`, funkcija `difftime`
# će to sama učiniti ako pošaljete datum u default-nom obliku
difftime('2016-03-01', '2015-03-01', units = "weeks")
# koliko je prošlo sati od 1.3.2015. do danas?
difftime(Sys.Date(), '2015-03-01', units = "hours")
## Time difference of 52.28571 weeks
## Time difference of 42889 hours
Funkcija difftime
zapravo radi i sa vremenskim oznakama, tj. ne moramo nužno raditi na razini datuma, već se možemo spustiti do razine sekunde. Ovo ćemo isprobati kada naučimo klasu POSIXct
u nastavku. Isto tako, ako nam treba samo broj (sekundi, sati, dana i sl.), lako transformiramo izlaz u cijeli broj uz pomoć funkcije as.numeric
.
Jezik R implementira i posebnu varijantu funkcije seq
za rad sa datumima koja ima sljedeći potpis:
Parametri ove funkcije su sljedeći:
from
- početni datum (obvezni parametar)to
- konačni datumby
- korak sekvence u danima ili znakovni niz tipa"7 days"
,"2 weeks"
i sl. (za sve mogućnosti pogledati dokumentaciju!)length.out
- duljina sekvencealong.with
- vektor čiju duljinu uzimamo za referencu
Isprobajmo ovu funkciju.
Zadatak 10.4 - funkcija seq
i datumi
# ispišite sekvencu datuma od 1.1.2010. do 1.1.2030. u koracima od 6 mjeseci
# napravite raspored čišćenja zajedničkih prostora za stambenu zgradu
# prostori se moraju čistiti svaka 3 tjedna
# svaki stan mora imati svoj datum čišćenja
# stanovi su opisani sljedećim podatkovnim okvirom
stanovi <- data.frame(broj_stana = 1:10,
prezime = c("Ebert", "Ladovac", "Cerić", "Dikla", "Anić",
"Perić", "Žužić", "Babić", "Ibiz", "Radler"))
# dodajte stupac `ciscenje` sa po jednim datumom za svaki pojedini stan
# redom po brojevima stana, počevši od današnjeg datuma
# ispišite podatkovni okvir `stanovi`
# ispišite sekvencu datuma od 1.1.2010. do 1.1.2030. u koracima od 6 mjeseci
seq(as.Date('2010-01-01'), as.Date('2030-01-01'), by = "6 months")
# dodajte stupac `ciscenje` sa po jednim datumom za svaki pojedini stan
# redom po brojevima stana, počevši od današnjeg datuma
seq(Sys.Date(), by = "3 weeks",
along.with = stanovi$broj_stana) -> stanovi$ciscenje
# ispišite podatkovni okvir `stanovi`
stanovi
## [1] "2010-01-01" "2010-07-01" "2011-01-01" "2011-07-01" "2012-01-01"
## [6] "2012-07-01" "2013-01-01" "2013-07-01" "2014-01-01" "2014-07-01"
## [11] "2015-01-01" "2015-07-01" "2016-01-01" "2016-07-01" "2017-01-01"
## [16] "2017-07-01" "2018-01-01" "2018-07-01" "2019-01-01" "2019-07-01"
## [21] "2020-01-01" "2020-07-01" "2021-01-01" "2021-07-01" "2022-01-01"
## [26] "2022-07-01" "2023-01-01" "2023-07-01" "2024-01-01" "2024-07-01"
## [31] "2025-01-01" "2025-07-01" "2026-01-01" "2026-07-01" "2027-01-01"
## [36] "2027-07-01" "2028-01-01" "2028-07-01" "2029-01-01" "2029-07-01"
## [41] "2030-01-01"
## broj_stana prezime ciscenje
## 1 1 Ebert 2020-01-21
## 2 2 Ladovac 2020-02-11
## 3 3 Ceric 2020-03-03
## 4 4 Dikla 2020-03-24
## 5 5 Anic 2020-04-14
## 6 6 Peric 2020-05-05
## 7 7 Žužic 2020-05-26
## 8 8 Babic 2020-06-16
## 9 9 Ibiz 2020-07-07
## 10 10 Radler 2020-07-28
10.1.2 Klase POSIXct
i POSIXlt
Klasa POSIXct
nam je pogodna kada nam nije dosta samo pohraniti datum, već moramo znati i točno vrijeme za neku obzervaciju ili poslovni događaj. Objekt ove klase najčešće stvaramo uz pomoć sljedećih funkcija:
Sys.time()
koja vraća trenutnu vremensku oznaku uzimajući u obzir postavljenu vremensku zonuas.POSIXct()
kojoj kao parametar prosljeđujemo znakovni niz koji predstavlja datum i vrijeme
Funkcija as.POSIXct()
kao parametar očekuje vremensku oznaku tipa %Y-%m-%d %H:%M:%S
gdje su prve tri oblikovne specifikacije istovjetne već poznatoj specifikaciji datuma, dok %H
, %M
i %S
predstavljaju dvoznamenkaste sate, minute i sekunde (gleda se 24-satni oblik prikaza vremena). Za parsiranje drugih oblika vremenskih oznaka potrebno je - kao i kod klase Date
- dodati parametar format sa oblikovnom specifikacijom kako intepretirati zadani znakovni niz. Opet, za popis svih parametara nam pomaže poziv ?strptime
, iako je u praksi poželjna opcija za lakši rad proučiti paket lubridate
kojeg ćemo raditi kasnije. Funkciji as.POSIXct
možemo dodati i parametar tz
postavljen na znakovni niz koji definira vremensku zonu.
Zadatak 10.5 - klasa POSIXct
# ispišite trenutni datum i vrijeme
# pretvorite sljedeće znakovne nizove u vremenske oznake i ispišite ih na zaslon:
# "2015-10-28 15:30:42"
# "01-12-2001 14:30" <-- oznaka očitana u New York-u, SAD, vremenska zona EST
# ispišite trenutni datum i vrijeme
Sys.time()
# pretvorite sljedeće znakovne nizove u vremenske oznake i ispišite ih na zaslon:
# "2015-10-28 15:30:42"
# "01-12-2001 14:30" <-- oznaka očitana u New York-u, SAD, vremenska zona EST
as.POSIXct("2015-10-28 15:30:42")
as.POSIXct("01-12-2001 14:30", tz = "EST", format = "%d-%m-%Y %H:%M")
## [1] "2020-01-21 14:11:51 CET"
## [1] "2015-10-28 15:30:42 CET"
## [1] "2001-12-01 14:30:00 EST"
Imena vremenskih zona su standardizirana (tzv. “Olsonove vremenske zone”) a dohvaćaju se uz pomoć operativnog sustava a možemo ih ispisati uz pomoć funkcije OlsonNames()
. Trenutnu vremensku zonu platforme možemo ispisati uz pomoć funkcije Sys.timezone()
.
Zadatak 10.6 - vremenske zone
# ispišite trenutnu vremensku zonu
# ispišite 10 nasumično odabranih oznaka vremenskih zona instaliranih na trenutnoj platformi
# ispišite trenutnu vremensku zonu
Sys.timezone()
# ispišite 10 nasumično odabranih oznaka vremenskih zona instaliranih na trenutnoj platformi
sample(OlsonNames(), 10)
## [1] "Europe/Warsaw"
## [1] "Pacific/Kosrae" "America/Havana" "Asia/Aqtau"
## [4] "Africa/Timbuktu" "Israel" "America/Campo_Grande"
## [7] "Asia/Hong_Kong" "Pacific/Johnston" "Asia/Tashkent"
## [10] "Etc/GMT-2"
Vremenske oznake također mogu koristiti operatore +
i -
uz cjelobrojne vrijednosti kao drugi operand pri čemu se od vremenske oznake oduzimaju ili dodaju sekunde. Isto tako, možemo oduzimati dvije vremenske oznake kako bi dobili razliku u sekundama, ili koristiti funkciju difftime
sa odabranom vrijednosti vremenske jedinice.
Zadatak 10.7 - aritmetika sa vremenskim oznakama
# ispišite koje će biti vrijeme 1000 sekundi od ovog trenutka
# ispišite koliko je prošlo sati od ponoći 1.1.2015. do sad
# ispišite koje će biti vrijeme 1000 sekundi od ovog trenutka
Sys.time() + 1000
# ispišite koliko je prošlo sati od ponoći 1.1.2015. do sad
difftime(Sys.time(), "2015-01-01 00:00:00", units = "hours")
## [1] "2020-01-21 14:28:31 CET"
## Time difference of 44318.2 hours
Klasa POSIXlt
se ponaša isto kao i POSIXct
(za stvaranje se koristimo funkcijom as.POSIXlt
), no zapravo se radi o listi koja nam omogućuje jednostavno izvlačenje određenih parametara iz vremenske oznake, kao što su broj sekundi, broj minuta, dan u tjednu i sl. Sve elemente liste lako možemo vidjeti ako napravimo POSIXlt
objekt i onda pozovemo funkciju unclass
nad njim, pri čemu će se on pretvoriti u “običnu” listu. Možemo ići i korak dalje - ako listu ubacimo u funkciju unlist
, kao rezultat dobivamo obični znakovni vektor.
Zadatak 10.8 - klasa POSIXlt
# pretvorite sljedeći znakovni niz u vremensku oznaku tipa `POSIXlt`
# pohranite rezultat u varijablu `t_long`
# "1.5.2013 13:35"
# ispišite broj sati i broj minuta vremenske oznake `t_long`
# ispisom njenih elemenata naziva `hour` i `min`
# uklonite klasu i svojstvo liste varijable `t_long`
# i ispišite ju na zaslon
# pretvorite sljedeći znakovni niz u vremensku oznaku tipa `POSIXlt`
# pohranite rezultat u varijablu `t_long`
# "1.5.2013 13:35"
t_long <- as.POSIXlt("1.5.2013 13:35", format = "%d.%m.%Y %H:%M")
# ispišite broj sati i broj minuta vremenske oznake `t_long`
# ispisom njenih elemenata naziva `hour` i `min`
t_long$hour
t_long$min
# uklonite klasu i svojstvo liste varijable `t_long`
# i ispišite ju na zaslon
t_long %>% unclass() %>% unlist()
## [1] 13
## [1] 35
## sec min hour mday mon year wday yday isdst zone
## "0" "35" "13" "1" "4" "113" "3" "120" "1" "CEST"
## gmtoff
## NA
10.1.3 Paket lubridate
Iako jezik R ima relativno dobru podršku za rad sa datumima i vremenskim oznakama, upravljanje istima možemo učiniti znatno učinkovitijim uz paket lubridate
. Ukoliko analiziramo podatke gdje je vremenska komponenta jako bitna, ili upravljamo podatkovnim skupovima koji koriste različite oblike zapisa datuma i vremenskih oznaka, onda si proces analize uvelike olakšavamo i ubrzavamo korištenjem funkcija iz ovog paketa.
Jedna od stvari koja je možda najkorisnija programerima koji ne vole pisati oblikovne specifikacije za parsiranje datuma je porodica funkcija za parsiranje datuma čija imena odgovaraju okvirnom izgledu zapisa kojeg želimo parsirati. Npr. funkcija imena ymd
zna parsirati znakovne nizove u kojima je datum zapisan redoslijedom godina-mjesec-dan. Funkcija je dovoljno “pametna” da sama interpretira pojedinosti oko zapisa, kao što su delimiteri, znakovna polja i sl. Ukoliko zapis ima drugi raspored dana, mjeseca i godine, potrebno je samo adekvatno razmjestiti slova u nazivu funkcije.
Zadatak 10.9 - funkcije paketa lubridate
za parsiranje datuma
# library(lubridate) #učitati ako je potrebno!
# koristeći se funkcijama iz paketa `lubridate`
# parsirajte u datume i ispišite sljedeće znakovne nizove
# "2016-07-31"
# "28.2.1983."
# "07#31#1996"
# "20010830"
# koristeći se funkcijama iz paketa `lubridate`
# parsirajte u datume i ispišite sljedeće znakovne nizove
# "2016-07-31"
# "28.2.1983."
# "07#31#1996"
# "20010830"
ymd("2016-07-31")
dmy("28.2.1983.")
mdy("07#31#1996")
ymd("20010830")
## [1] "2016-07-31"
## [1] "1983-02-28"
## [1] "1996-07-31"
## [1] "2001-08-30"
Navedeni koncept može se koristiti i za vremenske oznake, samo imenu funkcije dodamo podcrtu i “specifikaciju” sati, minuta i sekundi (npr. ymd_hms
).
Zadatak 10.10 - funkcije paketa lubridate
za parsiranje vremenskih oznaka
# koristeći se funkcijama iz paketa `lubridate`
# parsirajte u vremenske oznake i ispišite sljedeće znakovne nizove
# "17.5.1977. 10:15pm"
# "20160429 10.05.17"
# koristeći se funkcijama iz paketa `lubridate`
# parsirajte u vremenske oznake i ispišite sljedeće znakovne nizove
# "17.5.1977. 10:15pm"
# "20160429 10.05.17"
dmy_hm("17.5.1977. 10:15pm")
ymd_hms("20160429 10.05.17")
## [1] "1977-05-17 22:15:00 UTC"
## [1] "2016-04-29 10:05:17 UTC"
Uočimo da ove funkcije uvijek za vremensku zonu postavljaju UTC
. Ovo je namjerno napravljeno s ciljem da se motivira korištenje jedinstvene vremenske zone u podatkovnom skupu kojeg analiziramo. Ukoliko želimo, tijekom parsiranja možemo postaviti vremensku zonu uz pomoć parametra tz
. Isto tako, kod već inicijaliziranih vremenskih oznaka možemo upravljati vremenskim zonama uz pomoć sljedećih funkcija
force_tz
- “nameće” novu vremensku zonu tj. ostavlja iste vrijednosti vremenske oznake ali postavlja vremensku zonu koju definiramowith_tz
- provodi transformaciju vremenske oznake u onu koja odgovara traženoj vremenskoj zoni
## [1] "2016-11-29 10:05:17 EST"
## [1] "2016-11-29 10:05:17 CET"
## [1] "2016-11-29 16:05:17 CET"
Paket lubridate
također uvelike olakšava izvlačenje segmenata datuma i vremena iz vremenskih oznaka uz pomoć funkcija kao što su year
, week
, month
i sl. Uz pomoć istih funkcija možemo lako i izmijeniti neku od komponenti vremena.
Zadatak 10.11 - izvlacenje elemenata vremenskih oznaka
x <- dmy_hms("19.7.1996. 16:15:27")
# iz gornje vremenske oznake izvucite i ispišite sate te potom minute
# postavite godinu gornje vremenske oznake na 2011., a mjesec na lipanj
# ispišite `x`
x <- dmy_hms("19.7.1996. 16:15:27")
# iz gornje vremenske oznake izvucite i ispišite sate te potom minute
hour(x)
minute(x)
# postavite godinu gornje vremenske oznake na 2011., a mjesec na lipanj
year(x) <- 2011
month(x) <- 6
# ispišite `x`
x
## [1] 16
## [1] 15
## [1] "2011-06-19 16:15:27 UTC"
Za potpuni popis funkcija pogledajte dokumentaciju paketa lubridate
.
Za trenutni datum i vrijeme lubridate
nudi alternative funkcijama Sys.Date()
i Sys.time()
koje se jednostavno zovu today()
i now()
.
Zadatak 10.12 - funkcije today
i now
# ispišite sutrašnji datum
today() + 1
# ispišite koliko je bilo sati, minuta i sekundi prije točno sat vremena
now() - 60*60
## [1] "2020-01-22"
## [1] "2020-01-21 13:11:52 CET"
Već smo rekli da upravljanje vremenskom komponentom u podacima može postati vrlo kompleksno, pogotovo ako uzmemo u obzir da vremenski intervali mogu biti zadani generički (npr. “2 godine”) ili konkretno (raspon između dva datuma) te da se matematički i kalendarski način računanja vremena često ne poklapaju (npr. “za godinu dana” može značiti točan matematički izračun sekundi u 365 dana, ili kalendarski “isti datum iduće godine”).
Paket lubridate
definira četiri mogućnosti kod definiranja vremena i vremenskih intervala:
- trenutak (instant) - vremenska oznaka zaokružena na sekundu
- trajanje (duration) - “generički” definiran interval u sekundama
- period - slično trajanju, ali omogućuje definiranje trajanja koja matematički ne traju uvijek isto (npr. “3 mjeseca”)
- interval - vrijeme omeđeno između dva točno određena trenutka
Trenutke smo već upoznali, to su vremenske oznake koje smo do sada stvarali. Za stvaranje trajanja i perioda imamo intuitivno definirane funkcije koje se nazivaju po engleskim nazivima za vremenske jedinice, pri čemu trajanja imaju dodano slovo d kao prefiks (od duration). Tako imamo funkcije minutes
i dminutes
, hours
i dhours
, weeks
i dweeks
i sl. (uočite da ne postoji funkcija dmonths
, budući da “mjesec dana” ne možemo jednoznačno pretvoriti u sekunde!).
Zadatak 10.13 - trajanja i periodi
# ispišite objekte koji predstavljaju trajanje i period od 3 tjedna
# u varijablu `v` upišite period od 5 godina, 3 mjeseca i 2 dana
# dodajte gornji period današnjem datumu
# ispišite objekte koji predstavljaju trajanje i period od 3 tjedna
weeks(3)
dweeks(3)
# u varijablu `v` upišite period od 5 godina, 3 mjeseca i 2 dana
v <- years(5) + months(3) + days(2)
# dodajte gornji period današnjem datumu
today() + v
## [1] "21d 0H 0M 0S"
## [1] "1814400s (~3 weeks)"
## [1] "2025-04-23"
Uočite da gornji izraz nismo lako mogli dobiti “matematički”.
Konačno, interval stvaramo uz pomoć funkcije interval
kojoj dajemo početni i konačni trenutak, ili uz pomoć funkcije as.interval
kojoj dajemo trajanje/period i početni trenutak. Možemo također koristiti i operator %--%
sa dvije vremenske oznake (tj. trenutka) kao operandima.
Zadatak 10.14 - intervali
# stvorite varijablu `interval1` koja će pohraniti interval
# od 6 mjeseci prije današnjeg dana
# do 6 mjeseci iza današnjeg dana
# stvorite varijablu `interval2` koja će pohraniti interval od današnjeg dana
# do datuma koji će se dogoditi za 4 mjeseca, 3 tjedna i 2 dana
# stvorite varijablu `interval3` koja će pohraniti interval
# od 1.5.2002. do 1.7.2002.
# ispišite sva tri intervala
# stvorite varijablu `interval1` koja će pohraniti interval
# od 6 mjeseci prije današnjeg dana
# do 6 mjeseci iza današnjeg dana
interval1 <- interval(today() - months(6), today() + months(6))
# stvorite varijablu `interval2` koja će pohraniti interval od današnjeg dana
# do datuma koji će se dogoditi za 4 mjeseca, 3 tjedna i 2 dana
interval2 <- as.interval(months(4) + weeks(3) + days(2), today())
# stvorite varijablu `interval3` koja će pohraniti interval
# od 1.5.2002. do 1.7.2002.
interval3 <- dmy("1.5.2002.") %--% dmy("1.7.2002.")
# ispišite sva tri intervala
interval1
interval2
interval3
## [1] 2019-07-21 UTC--2020-07-21 UTC
## [1] 2020-01-21 UTC--2020-06-13 UTC
## [1] 2002-05-01 UTC--2002-07-01 UTC
Kod intervala je zgodno što između ostalog možemo:
- provjeriti da li se neki trenutak nalazi unutar nekog intervala uz pomoć operatora
%within%
- provjeriti da li se intervali preklapaju uz pomoć funkcije
int_overlaps()
- lako dohvatiti početak i kraj intervala uz pomoć funkcija
int_start()
iint_end
- “spojiti” dva intervala uz pomoć funkcije
union
ili naći presjek uz pomoć funkcijeintersect
- brojne druge mogućnosti koje možemo naučiti gledajući dokumentaciju
Zadatak 10.15 - pomoćne funkcije za rad sa intervalima
# provjerite da li je današnji dan unutar intervala definiranog varijablom `interval1`
# ukoliko se `interval1` i `interval2` preklapaju
# ispišite njihov presjek
# provjerite da li je današnji dan unutar intervala definiranog varijablom `interval1`
today() %within% interval1
# ukoliko se `interval1` i `interval2` preklapaju
# ispišite njihov presjek
if (int_overlaps(interval1, interval1)) intersect(interval1, interval2)
## [1] TRUE
## [1] 2020-01-21 UTC--2020-06-13 UTC
U ovom dijelu upoznali smo se sa jednim dijelom funkcionalnosti koje nude klase za upravljanje datumima i vremenskim oznakama jezika R te paket lubridate
. Za dodatne informacije pogledajte službenu dokumentaciju jezika R i paketa lubridate
, a dobar izvor je i članak “Dates and Times Made Easy with lubridate” kojeg je napisao sam autor paketa Hadley Wickham a koji je dostupan na ovoj poveznici.
10.2 Rad sa znakovnim nizovima
R ima jako dobru podršku za rad sa znakovnim nizovima, no funkcije koje nudi osnovni R nisu intuitivne niti konzistentne ako ih uspoređujemo sa sličnim funkcijama u drugim programskim jezicima.
Upravo iz ovih razloga pojavio se paket stringr
koji nudi vrlo učinkovitu alternativu postojećim funkcijama vezanih uz znakovne nizove te predstavlja jedan od najpopularnijih “dodatnih” R paketa. No prije upoznavanja sa funkcijama koje nudi ovaj paket potrebno je kratko se osvrnuti na općenitu problematiku upravljanja znakovnim nizovima u analizi podataka te na tehnologiju bez koje je provedba analize znakovnih nizova gotovo nezamisliva - tzv. regularni izrazi.
10.2.1 Analiza teksta i regularni izrazi
Analiza teksta neizbježan je element kod analize podatkovnih skupova. Bilo da se radi o jednostavnoj identifikaciji kategorija, traženju podnizova ili nečem daleko složenijem kao što su specijalizirane metode rudarenja teksta (engl. text mining), teško je zamisliti bilo kakvu smislenu analizu podataka koja prije ili kasnije ne zahtijeva poznavanje barem osnovnih metoda analize znakovnih nizova.
Neovisno o razini složenosti analize znakovnih nizova koju želimo provesti, jedna tehnologija je sveprisutna i univerzalno primjenjiva - regularni izrazi. Ovdje se radi o posebnom jeziku uz pomoć kojeg definiramo “uzorke” (patterns) na osnovu kojih pretražujemo neki tekst, radimo izmjene i sl.
Detaljni pregled tehnologije regularnih izraza izlazi iz okvira ovog udžbenika. U nastavku ćemo navesti samo kratke informacije u svrhu brzog pregleda ili podsjetnika. Ukoliko se do sada uopće niste susretali s ovom tehnologijom, snažno preporučujemo ulaganje truda i svladavanje barem osnovnih koncepata, npr. uz pomoć nekog od dostupnih internetskih. Jedan od vrlo kratkih ali učinkovitih mini-tečajeva regularnih izraza možete naći ovdje .
Regularni izraz jednostavno je niz znakova koji predstavlja uzorak kojeg tražimo unutar nekog teksta. Npr. regularni izraz gram
se nalazi u znakovnom nizu Programski jezik R
ali se ne nalazi u znakovnom nizu Analiza teksta
. Kažemo da smo pronašli “slaganje” (match) regularnog izraza sa prvim nizom ali ne i sa drugim.
Ovakav regularni izraz nije previše fleksibilan - prava moć regularnih izraza krije se u mogućnosti slaganja posebnih izraza koji će se uz pomoć posebnog opisa moći slagati sa “općenitijim” oblicima znakovnih nizova. Tipičan primjer je adresa elektroničke pošte - konkretnije, provjera da li je korisnik unio adresu koja odgovara “općenitom” obliku adrese elektroničke pošte. Jedno od mogućnosti je jednostavno izraz @
kojim zapravo samo provjeravamo postojanje tog znaka u danom znakovnom nizu. Time smo osigurali određenu razinu kontrole, ali isto tako smo dopustili “adrese” tipa @@@
i @23456
. Uz malo “rada” na izrazu mogli bi doći do malo boljeg rješenja, koje npr. može izgledati ovako:
Iako izgleda kao niz nasumičnih znakova, osnovnim poznavanjem regularnih izraza možemo relativno lako interpretirati gornji izraz. Znak \w
označava “slovo ili znamenku”, znak +
znači “1 ili više” i sl. Ako bi htjeli “prepričati” gornji regularni izraz govornim jezikom, to bi bilo “jedno ili više slova ili znamenki, potom znak @
, pa jedno ili više slova ili znamenki, zatim točka i onda konačno opet jedno ili više slova ili znamenki”. Iako ovo nije pretjerano sofisticirani izraz, on je ipak kvalitetniji od prvog pokušaja. Daljnje proširenje je itekako moguće i iako naknadnim dodavanjima sve više gubimo neposrednu čitljivost, isto tako postižemo sve višu i višu razinu kontrole koja se približava formalnim odrednicama kako adresa elektroničke pošte mora izgledati (za ovakve specifične uporabe često se isplati i provjeriti javno dostupne repozitorije regularnih izraza gdje možemo naći složene ali kvalitetne i pomno testirane izraze koje je dovoljno jednostavno prekopirati u naš programski kod).
Važno je napomenuti da nema jednog jedinstvenog standarda za regularne izraze. Postoji tzv. “POSIX standard” u dvije inačice - BRE i ERE (Basic Regular Expressions i Extended Regular Expressions) koje su zapravo gotovo iste, osim što BRE zahtjeva malo intenzivniju uporabu znaka \
. Još jedan popularni standard je tzv. “Perl standard” koji predstavlja inačicu regularnih izraza implementiranih u jeziku Perl. Budući da je Perl jedan od vodećih jezika za upravljanje tekstom tako je i ovaj standard postao jedan od najšire prihvaćenih načina korištenja regularnih izraza.
U općenitom slučaju, gotovo svi popularniji programski jezici imaju podršku za regularne izraze, bilo već ugrađene u jezik, bilo uz pomoć dodatnih paketa. R je jedan od jezika koji već sadrži podršku za regularne izraze u svojem osnovnom paketu. Štoviše, R ima ugrađenu paralelnu podršku za tri najraširenija standarda - POSIX ERE, POSIX BRE i Perl. POSIX ERE je zadana postavka, a određenim parametrima možemo se lako “prebaciti” na BRE (extended = FALSE
) ili Perl (perl = TRUE
). U daljnjim poglavljima držati ćemo se ERE standarda, ali bitno je znati i za prethodno navedene postavke želimo li koristiti već gotove izraze koji su razvijeni u nekom drugom standardu (a ne želimo se zamarati sa prebacivanjem iz jednog standarda u drugi).
Sljedeća tablica daje kratki pregled nekih češće korištenih elemenata regularnih izraza u jeziku R:
Element | Značenje |
---|---|
abcd |
niz slova “abcd” |
1234 |
niz znamenki “1234” |
\\d ili [:digit:] |
ili [0-9] bilo koja znamenka |
\\D ili [:alpha:] |
ili [A-Za-z] bilo koje slovo |
[:alnum:] |
bilo koje slovo ili znamenka |
. |
bilo koji znak |
\\. |
točka |
[abc] |
samo navedeni znakovi |
[^abc] |
svi znakovi osim navedenih |
* |
nula ili više ponavljanja |
+ |
jedno ili više ponavljanja |
{n} |
točno n ponavljanja |
{m, n} |
najmanje m, najviše n ponavljanja |
? |
opcionalni znak |
[:space:] ili \\s |
bilo kakva praznina |
[:punct:] |
znakovi interpunkcije |
^...$ |
oznaka za početak i kraj |
(ab|cd) |
niz “ab” ili niz “cd” |
Uočite da kod korištenja specijalnog znaka \
zapravo moramo koristiti “dvostruki znak” \\
(prvi put da naznačimo R-u da se radi o specijalnom znaku, drugi put da ga doslovno upotrijebimo kao dio regularnog izraza).
Osnovne funkcije jezika R za rad sa znakovnim nizovima (a time i regularnim izrazima) su između ostalog grep
, grepl
, regexrp
, gregexrp
, regmatches
, sub
, gsub
itd. No budući da paket stringr
nudi skup alternativnih funkcija sa gotovo istim funkcionalnostima ali uz daleko intuitivnija imena i konzistentnije potpise, mi ćemo se usredotočiti na upravo te funkcije a učenje osnovnih ostavljamo čitateljima koji žele upotpuniti svoje znanje učenjem svih dostupnih alata koje nudi jezik R.
10.2.2 Paket stringr
Već smo rekli da paket stringr
zapravo u izvjesnoj mjeri reimplementira već postojeće funkcije jezika R, ali na intuitivniji i konzistentniji način. Ako želimo biti precizni, funkcije paketa stringr
su zapravo funkcionalno nešto skromnije, no to je napravljeno s konkretnom namjerom - funkcionalnost je reducirana na poslove za koje se smatra da predstavljaju daleko najčešće korištene funkcionalnosti kod analize teksta. Funkcionalnost koja je “izbačena” tiče se specifičnih slučajeva za koje će programer morati potražiti alternativna rješenja (često u obliku osnovnih funkcija), no dobitak je u jednostavnijim, intuitivnijim funkcijama koje su lakše za učenje i učinkovito dugoročno korištenje.
Pored navedenih “popravljanja” paket stringr
omogućuje i sljedeće:
- konzistentno tretiranje faktora kao znakovnih nizova
- lakše korištenje izlaza funkcija za ulaz nastupajuće funkcije, što je pogotovo korisno uz operator
%>%
Možemo početi sa nekim jednostavnijim funkcijama za koje ne trebamo regularne izraze (navodimo pojednostavljene potpise funkcija, za potpune pogledajte dokumentaciju):
str_c(string1, string2, ...)
- spajanje znakovnih nizova, alternativa funkciji paste0str_length(string)
- vraća duljinu znakovnog nizastr_sub(string, start, end)
- vraća podnizstr_sub(string, start, end) <- string2
- umetanje novog podniza (ne mora biti iste duljine kao izbačeni podniz!)str_trim(string)
- uklanjanje praznina s početka i kraja niza
Zadatak 10.16 - osnovne funkcije za rad sa znakovnim nizovima
niz1 <- " Ovo je primjer "
niz2 <- "spajanja nizova! "
# uz pomoć jedne naredbe spojite gornje nizove, potom
# iz rezultata uklonite praznine s početka i kraja niza,
# zatim izdvojite podniz od 8. do 23. znaka te konačni rezultat ispišite na zaslon
niz <- "R je pretjerano kompliciran i nimalo lagan jezik!"
# u gornjem nizu znakova zamjenite sve od 9. znaka (brojeno od početka)
# do 13. (brojeno od kraja) sa praznim nizom
# ispišite niz
niz1 <- " Ovo je primjer "
niz2 <- "spajanja nizova! "
# uz pomoć jedne naredbe spojite gornje nizove, potom
# iz rezultata uklonite praznine s početka i kraja niza,
# zatim izdvojite podniz od 8. do 23. znaka te konačni rezultat ispišite na zaslon
str_c(niz1, niz2) %>% str_trim() %>% str_sub(8, 23)
niz <- "R je pretjerano kompliciran i nimalo lagan jezik!"
# u gornjem nizu znakova zamjenite sve od 9. znaka (brojeno od početka)
# do 13. (brojeno od kraja) sa praznim nizom
str_sub(niz, 9, -13) <- ""
# ispišite niz
niz
## [1] "primjer spajanja"
## [1] "R je prelagan jezik!"
Funkcija str_c
ima i parametar sep
za dodavanje separatora, te parametar collapse
za spajanje elemenata znakovnog vektora u jedinstveni niz uz vrijednost parametra kao separator.
Zadatak 10.17 - spajanje znakovnih nizova
niz1 <- "Za spajanje"
niz2 <- "ovih nizova"
niz3 <- "potreban je razmak!"
# spojite gornje nizove u jedinstveni niz i ispišite rezultat
nizovi <- c("A", "ovi", "nizovi", "su", "elementi", "vektora...")
# spojite elemente gornjeg vektora u jedan niz i ispišite rezultat
niz1 <- "Za spajanje"
niz2 <- "ovih nizova"
niz3 <- "potreban je razmak!"
# spojite gornje nizove u jedinstveni niz i ispišite rezultat
str_c(niz1, niz2, niz3, sep = " ")
nizovi <- c("A", "ovi", "nizovi", "su", "elementi", "vektora...")
# spojite elemente gornjeg vektora u jedan niz i ispišite rezultat
str_c(nizovi, collapse = " ")
## [1] "Za spajanje ovih nizova potreban je razmak!"
## [1] "A ovi nizovi su elementi vektora..."
Pogledajmo sada neke funkcije koje rade sa regularnim izrazima:
str_detect(string, pattern)
- vraćaTRUE
ako niz sadrži uzorak, inačeFALSE
str_extract(string, pattern)
- vraća niz znakova koji odgovara prvoj pojavi uzorkastr_extract_all(string, pattern)
- vraća listu sa svim pojavama koje odgovaraju uzorkustr_replace(string, pattern, replacement)
- mijenja prvu pojavu uzorka sa zadanim novim nizomstr_replace_all(string, pattern, replacement)
- mijenja sve pojave uzorka sa zadanim novim nizom
Sve ove funkcije su vektorizirane, tj. ponašaju se logično (tj. “paralelizirano”) kad im kao određeni parametar pošaljemo vektor - npr. ako funkciji str_replace
pošaljemo vektor znakovnih nizova i vektor “zamjena”, svaka prva pojava uzorka biti će zamijenjena odgovarajućim elementom u poretku zamjena. Za detalje oko ovakvog proširenog korištenja uputno je pogledati dokumentaciju.
Zadatak 10.18 - funkcije i regularni izrazi
adrese <- c("pero.peric@fer.hr", "iva.ivic@etfos.hr", "ppetrovic@gmail.com",
"branko1987@yahoo.com", "jaRULZ4EVR@gmail.nz", "dperkovic@efzg.hr",
"lalaic1998@gmail.co.uk", "perica.markic@fer.hr")
# prebrojite i ispišite koliko u gornjem popisu ima mail adresa iz domene `fer.hr`
# ispišite sve adrese koje sadrže bar jednu znamenku
# ispišite sve adrese koje na drugom mjestu imaju samoglasnik
# ispišite sve jedinstvene domene adresa elektroničke pošte iz gornjeg niza adresa
# (domenom smatramo dio adrese iza znaka `@`)
# anonimizirajte gornje adrese: niz znakova ispred znaka '@'
# zamijenite nasumičnim šesteroznamenkastim prirodnim brojem
# prebrojite i ispišite koliko u gornjem popisu ima mail adresa iz domene `fer.hr`
str_detect(adrese, "fer\\.hr") %>% sum
# ispišite sve adrese koje sadrže bar jednu znamenku
adrese[str_detect(adrese, '[:digit:]')]
# ispišite sve adrese koje na drugom mjestu imaju samoglasnik
str_detect(adrese, "^.[aeiouAEIOU]") %>% adrese[.] #ili `]`(adrese, .)
# moze i str_detect(adrese, "^.(a|e|i|o|u|A|E|I|O|U)") %>% adrese[.]
# ispišite sve jedinstvene domene adresa elektroničke pošte iz gornjeg niza adresa
# (domenom smatramo dio adrese iza znaka `@`)
str_extract(adrese, '@(.*)') %>% str_sub(2) %>% unique
# anonimizirajte gornje adrese: niz znakova ispred znaka '@'
# zamijenite nasumičnim šesteroznamenkastim prirodnim brojem
sample(100000:999999, length(adrese)) %>% as.character %>% str_replace(adrese, '^[^@]*', .)
## [1] 2
## [1] "branko1987@yahoo.com" "jaRULZ4EVR@gmail.nz"
## [3] "lalaic1998@gmail.co.uk"
## [1] "pero.peric@fer.hr" "jaRULZ4EVR@gmail.nz"
## [3] "lalaic1998@gmail.co.uk" "perica.markic@fer.hr"
## [1] "fer.hr" "etfos.hr" "gmail.com" "yahoo.com" "gmail.nz"
## [6] "efzg.hr" "gmail.co.uk"
## [1] "755066@fer.hr" "477550@etfos.hr" "521135@gmail.com"
## [4] "733004@yahoo.com" "812154@gmail.nz" "609008@efzg.hr"
## [7] "371161@gmail.co.uk" "736284@fer.hr"
Konačno, naučimo jednu relativno korisnu funkciju nazvanu str_split
. Ova funkcija rastavlja znakovni niz na vektor znakovnih nizova, ovisno o danom separatoru (koji može biti razmak, neki odabrani znak ali i regularni izraz), a često se koristi kao “primitivnija” alternativa funkcijama read.csv
i read.table
kada ulazne podatke želimo “ručno” rastaviti i parsirati, ili za analizu teksta kada paragrafe teksta “razbijamo” na pojedinačne riječi. Ova funkcija pretpostavlja da ćemo joj proslijediti niz znakovnih nizova za rastavljanje te nam kao rezultat vraća listu; ukoliko rastavljamo samo jedan niz, lako rezultat pretvaramo u vektor korištenjem funkcije unlist
.
## [1] "Primjer" "funkcije" "str_split"
Pokažimo sada jedan vrlo jednostavni primjer analize teksta. Izračunati ćemo frekvenciju pojave različitih u riječi u sljedećem paragrafu teksta. Za potrebe ovog zadatka morati ćemo pročitati podatke iz tekstualne datoteke. Jedan od najjednostavnijih načina kako ovo možemo učiniti jest kombinacija funkcija file
(koja otvara konekciju prema tekstualnoj datoteci, slično načinu upravljanja datotekama u C-u) i readLines
koja čita određeni broj redaka iz datoteke i sprema ih u znakovni vektor. Ako je datoteka dovoljno mala da sve retke možemo čitati odjednom, onda ove dvije funkcije možemo pozvati ovako:
con <- file("datoteka.txt", "r") # r = "read"
rows <- readLines(con) # ili readLines(con, n = 100) za čitanje samo određenog broja redaka
close(con) # zatvaranje toka
Sljedeći zadatak koristiti će dvije tekstualne datoteke
HobbitChapterOne.txt
- datoteka čiji tekst analiziramo istopwords.txt
- datoteka sa čestim riječima koje “nisu vrijedne” za analizu koju ćemo izvesti
Zadatak 10.19 - jednostavna analiza teksta
# u varijablu `hobbit` učitajte tekst iz datoteke `HobbitChapterOne.txt`
# a u varijablu `stopwords` stop riječi iz datoteke `stopwords.txt`
# provedite sljedeće korake:
# - spojite sve elemente iz varijable `hobbit` u jedan jedinstveni znakovni niz
# - uklonite interpunkcijske znakove iz teksta
# - prebacite tekst u "lowercase" (funkcija `tolower`)
# - rastavite tekst na riječi po prazninama
# - izbacite "prazne" riječi (riječi duljine 0) ako ih ima
# - izbacite sve riječi koje se nalaze u vektoru `stopwords`
# - izračunajte frekvencije riječi
# - ispišite na zaslon 20 najčešće korištenih riječi
# provedite sljedeće korake:
# - spojite sve elemente iz varijable `hobbit` u jedan jedinstveni znakovni niz
# - uklonite interpunkcijske znakove iz teksta
# - prebacite tekst u "lowercase" (funkcija `tolower`)
# - rastavite tekst na riječi po prazninama
# - izbacite "prazne" riječi (riječi duljine 0) ako ih ima
# - izbacite sve riječi koje se nalaze u vektoru `stopwords`
#
# - izračunajte frekvencije riječi
# - ispišite na zaslon 20 najčešće korištenih riječi
hobbit %>% str_c(collapse = " ") %>%
str_replace_all('[:punct:]', '') %>%
tolower %>% str_split('[:space:]') %>%
unlist -> hobbit2
hobbit3 <- hobbit2[!(hobbit2 %in% stopwords | nchar(hobbit2) == 0)]
freq <- table(hobbit3) %>% sort(decreasing = T)
freq[1:20]
## hobbit3
## bilbo gandalf one thorin dwarves door baggins good hobbit
## 39 36 36 34 31 27 25 25 25
## little long went know time away go old things
## 24 24 23 20 19 17 17 17 17
## come morning
## 16 16
Zadaci za vježbu
Sljedeći zadaci odnose se na podatkovni skup pohranjen u CSV datoteci crimeSample.csv
a koji predstavlja uzorak iz evidencije kriminalnih incidenata u gradu Philadelphiji (originalni podatkovni skup može se naći na ovoj poveznici). Originalni skup stupaca je reduciran a iz skupa svih obzervacija slučajnim odabirom uzorkovano je 1000 incidenata.
Prije rješavanja zadataka učitajte podatke u podatkovni okvir crimes i upoznajte se s podatkovnim skupom (str
, head
, itd.)
Pretvorite stupac sa vremenskom oznakom iz znakovnog tipa u
POSIXct
tip.Podatkovnom okviru dodajte sljedeće stupce:
Year
,Month
,Hour
. Stupce popunite odgovarajućim informacijama iz vremenske oznake. Odgovorite na pitanje: u kojem mjesecu se događa najviše zločina? Koji sat u danu je prema podacima “najopasniji”?Odgovorite na pitanje: koliki je postotak incidenata gdje opis incidenta sadrži riječ
"burglary"
ili"robbery"
? (savjet: pretvorite cijeli stupac sa opisom zločina u mala slova uz pomoć funkcijetolower()
).Ispišite na zaslon sve jedinstvene četveroznamenkaste brojeve koje možete naći u nazivima ulica u kojima je evidentiran kriminalni incident.
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/