U R-u, faktori se koriste za rad sa kategoričkim promenljivama, promenljivama koje imaju fiksni i poznati skup mogućih vrednosti. Takođe, korisni su kada želimo da prikažemo vektore karaktera u nealfabetnom poretku.
Pošto je sa faktorima mnogo lakše raditi nego sa karakterima, mnoge bazne funkcije R-a automatski pretvaraju vektore karaktera u faktore. To znači da se faktori često pojavljuju na mestima gde oni zapravo i nisu od pomoći. Srećom, ne moramo brinuti o tome u tidyvrese
, i možemo se fokusirati na situacije u kojima su faktori zaista korisni.
Da bismo radili sa faktorima, koristićemo paket forcats
, koji obezbeđuje alate za rad sa kategoričkim promenljivama. On pruža širok spektar pomagača za rad sa faktorima. forcats
nije deo tidyverse
, tako da ga moramo eksplicitno učitati.
library(tidyverse)
library(forcats)
Pretpostavimo da želimo promenljivu koja čuva mesece:
x1 = c("Dec", "Apr", "Jan", "Mar")
Korišćenje vektora karaktera za čuvanje ove promenljive ima dva problema:
x2 = c("Dec", "Apr", "Jam", "Mar")
x2
## [1] "Dec" "Apr" "Jam" "Mar"
sort(x1)
## [1] "Apr" "Dec" "Jan" "Mar"
Oba ova problema možemo rešiti jednim faktorom. Da bismo kreirali faktor, počinjemo kreiranjem liste odgovarajućih nivoa:
month_levels = c(
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
)
Sada možemo da napravimo faktor:
y1 = factor(x1, levels = month_levels)
y1
## [1] Dec Apr Jan Mar
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
sort(y1)
## [1] Jan Mar Apr Dec
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Svaka vrednost koja nije među nivoima će automatski biti podešena na NA:
y2 = factor(x2, levels = month_levels)
y2
## [1] Dec Apr <NA> Mar
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
Ako želimo upozorenja, možemo koristiti readr::parse_factor()
:
y2 = parse_factor(x2, levels = month_levels)
## Warning: 1 parsing failure.
## row # A tibble: 1 x 4 col row col expected actual expected <int> <int> <chr> <chr> actual 1 3 NA value in level set Jam
Ako izostavimo nivoe, oni će biti uzeti iz podataka po alfabetnom redu:
factor(x1)
## [1] Dec Apr Jan Mar
## Levels: Apr Dec Jan Mar
Ponekad bismo više voleli da redosled nivoa odgovara redosledu pojavljivanja u podacima. To možemo da uradimo kada kreiramo faktor navođenjem unique(x)
, ili sa fct_inorder()
:
f1 = factor(x1, levels = unique(x1))
f1
## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar
f2 = x1 %>% factor() %>% fct_inorder()
f2
## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar
Ako nam je potreban direktan pristup skupu važećih nivoa, možemo to učiniti sa levels()
:
levels(f2)
## [1] "Dec" "Apr" "Jan" "Mar"
Za ostatak ovog poglavlja, fokusiraćemo se na forcats::gss_cat
. To je skup podataka iz Opšte socijalne ankete, koju je sprovela nezavisna istražavačka organizacija NORC na Univerzitetu u Čikagu. Anketa ima hiljade pitanja, a u gss_cat
je odabrana samo nekolicina, dovoljna za ilustrovanje nekih čestih problema sa kojima se srećemo pri radu sa faktorima.
gss_cat
## # A tibble: 21,483 x 9
## year marital age race rincome partyid relig denom tvhours
## <int> <fct> <int> <fct> <fct> <fct> <fct> <fct> <int>
## 1 2000 Never married 26 White $8000 t~ Ind,nea~ Prote~ South~ 12
## 2 2000 Divorced 48 White $8000 t~ Not str~ Prote~ Bapti~ NA
## 3 2000 Widowed 67 White Not app~ Indepen~ Prote~ No de~ 2
## 4 2000 Never married 39 White Not app~ Ind,nea~ Ortho~ Not a~ 4
## 5 2000 Divorced 25 White Not app~ Not str~ None Not a~ 1
## 6 2000 Married 25 White $20000 ~ Strong ~ Prote~ South~ NA
## 7 2000 Never married 36 White $25000 ~ Not str~ Chris~ Not a~ 3
## 8 2000 Divorced 44 White $7000 t~ Ind,nea~ Prote~ Luthe~ NA
## 9 2000 Married 44 White $25000 ~ Not str~ Prote~ Other 0
## 10 2000 Married 47 White $25000 ~ Strong ~ Prote~ South~ 3
## # ... with 21,473 more rows
(Ova baza je obezbeđena u paketu, pa se može dobiti više informacija o promenljivama sa ?gss_cat
.)
Kada se faktori smeštaju u tiblove, ne možemo lako videti njihove nivoe. Jedan od načina da ih vidimo je count()
:
gss_cat %>%
count(race)
## # A tibble: 3 x 2
## race n
## <fct> <int>
## 1 Other 1959
## 2 Black 3129
## 3 White 16395
Ili uz pomoć barchart-a:
ggplot(gss_cat, aes(race)) +
geom_bar()
Podrazumevano, ggplot2
će ispustiti nivoe koji nemaju vrednosti. Možemo podesiti da se i oni prikažu sa:
ggplot(gss_cat, aes(race)) +
geom_bar() +
scale_x_discrete(drop = FALSE)
Ovi nivoi predstavljaju validne vrednosti koje se jednostavno ne pojavljuju u ovom skupu podataka.
Kada radimo sa faktorima, dve najčešće operacije su promena redosleda nivoa i promena vrednosti nivoa. Ove operacije su opisane u nastavku.
Često je korisno promeniti redosled nivoa faktora radi bolje vizuelizacije. Na primer, recimo da želimo da istražimo prosečan broj sati provedenih u gledanju televizije za različite religije:
relig_summary = gss_cat %>%
group_by(relig) %>%
summarise(
age = mean(age, na.rm = TRUE),
tvhours = mean(tvhours, na.rm = TRUE),
n = n()
)
ggplot(relig_summary, aes(tvhours, relig)) + geom_point()
Teško je protumačiti ovaj grafik jer ne postoji opšti obrazac. Možemo ga poboljšati promenom redosleda nivoa religija koristeći fct_reorder()
. Glavni argumenti su:
f
, faktor čije nivoe želimo da sortiramo.
x
, numerički vektor koji želimo da koristimo za sortiranje nivoa.
Opciono, fun
, funkcija koja se koristi ako postoji više vrednosti x
za vrednosti iz f
. Podrazumevana vrednost je median
.
ggplot(relig_summary, aes(tvhours, fct_reorder(relig, tvhours))) +
geom_point()
Sada kada smo promenili redosled religija mnogo bolje vidimo šablone - vidi se da ljudi u kategoriji “Don’t know” gledaju mnogo više televiziju, a Hinduizam i druge istočnjačke religije mnogo manje.
Kada počnemo da pravimo komplikovanije trensformacije, preporučuje se da ih premestimo u zasebni mutate()
korak. Na primer, možemo dobiti gornji grafik sa:
relig_summary %>%
mutate(relig = fct_reorder(relig, tvhours)) %>%
ggplot(aes(tvhours, relig)) +
geom_point()
Napravimo sličan grafik gledajući odnos prosečne starosti i prijavljenog nivoa prihoda:
rincome_summary = gss_cat %>%
group_by(rincome) %>%
summarise(
age = mean(age, na.rm = TRUE),
tvhours = mean(tvhours, na.rm = TRUE),
n = n()
)
ggplot(rincome_summary, aes(age, fct_reorder(rincome, age))) + geom_point()
Ovde promena redosleda nivoa nije dobra ideja! To je zato što rincome
već ima principijelni poredak koji ne treba menjati.
Međutim, ima smisla povući “Not applicable” na početak sa drugim posebnim nivoima. Možemo koristiti fct_relevel()
. Njeni argumenti su faktor, f
, a zatim nivoi koje želimo da pomerimo na početak.
ggplot(rincome_summary, aes(age, fct_relevel(rincome, "Not applicable"))) +
geom_point()
(“Not applicable” se koristi prilikom popunjavanja formulara, kako bi se naznačilo da se tražena informacija ne odnosi na osobu koja popunjava određeni obrazac - npr. obrazac može da sadrži pitanja koja se tiču vašeg supružnika. Ako niste u braku, uobičajeno je da u bilo koje pitanje, koje bi se odnosilo na vašeg supružnika ako biste bili u braku, stavite N/A.)
Za bar plot-ove, možemo koristiti fct_infreq()
da uredimo nivoe prema učestalosti: ovo je najjednostavniji tip promene redosleda zato što ne zahteva dodatne promenljive. Možemo ga kombinovati sa fct_rev()
, koja okreće redosled nivoa.
gss_cat %>%
mutate(marital = marital %>% fct_infreq() %>% fct_rev()) %>%
ggplot(aes(marital)) +
geom_bar()
Značajnije od promene redosleda nivoa je to kako da promenimo njihovu vrednost. Najopštiji i najmoćniji alat je fct_recode()
- omogućava nam da promenimo vrednost svakog nivoa. Na primer, uzmimo gss_cat$partyid
:
gss_cat %>% count(partyid)
## # A tibble: 10 x 2
## partyid n
## <fct> <int>
## 1 No answer 154
## 2 Don't know 1
## 3 Other party 393
## 4 Strong republican 2314
## 5 Not str republican 3032
## 6 Ind,near rep 1791
## 7 Independent 4119
## 8 Ind,near dem 2499
## 9 Not str democrat 3690
## 10 Strong democrat 3490
Neki nivoi su kratki i nerazumljvi. Podesimo ih da budu razumljiviji:
gss_cat %>%
mutate(partyid = fct_recode(partyid,
"Republican, strong" = "Strong republican",
"Republican, weak" = "Not str republican",
"Independent, near rep" = "Ind,near rep",
"Independent, near dem" = "Ind,near dem",
"Democrat, weak" = "Not str democrat",
"Democrat, strong" = "Strong democrat"
)) %>%
count(partyid)
## # A tibble: 10 x 2
## partyid n
## <fct> <int>
## 1 No answer 154
## 2 Don't know 1
## 3 Other party 393
## 4 Republican, strong 2314
## 5 Republican, weak 3032
## 6 Independent, near rep 1791
## 7 Independent 4119
## 8 Independent, near dem 2499
## 9 Democrat, weak 3690
## 10 Democrat, strong 3490
fct_recode()
će ostaviti istim nivoe koji nisu eksplicitno pomenuti i upozoriće nas ako slučajno pokušamo da pristupimo nivou koji ne postoji.
Možemo više starih nivoa smestiti u jedan novi nivo:
gss_cat %>%
mutate(partyid = fct_recode(partyid,
"Republican, strong" = "Strong republican",
"Republican, weak" = "Not str republican",
"Independent, near rep" = "Ind,near rep",
"Independent, near dem" = "Ind,near dem",
"Democrat, weak" = "Not str democrat",
"Democrat, strong" = "Strong democrat",
"Other" = "No answer",
"Other" = "Don't know",
"Other" = "Other party"
)) %>%
count(partyid)
## # A tibble: 8 x 2
## partyid n
## <fct> <int>
## 1 Other 548
## 2 Republican, strong 2314
## 3 Republican, weak 3032
## 4 Independent, near rep 1791
## 5 Independent 4119
## 6 Independent, near dem 2499
## 7 Democrat, weak 3690
## 8 Democrat, strong 3490
Ovu tehniku moramo koristiti pažljivo: ako grupišemo kategorije koje su dosta različite, završićemo sa pogrešnim zaključcima.
Ako želimo da skupimo mnogo nivoa, fct_collapse()
je korisna varijanta fct_recode()
. Svakom novom nivou možemo pridružiti vektor starih nivoa:
gss_cat %>%
mutate(partyid = fct_collapse(partyid,
other = c("No answer", "Don't know", "Other party"),
rep = c("Strong republican", "Not str republican"),
ind = c("Ind,near rep", "Independent", "Ind,near dem"),
dem = c("Not str democrat", "Strong democrat")
)) %>%
count(partyid)
## # A tibble: 4 x 2
## partyid n
## <fct> <int>
## 1 other 548
## 2 rep 5346
## 3 ind 8409
## 4 dem 7180
Ponekad samo želimo da skupimo sve male grupe kako bismo napravili grafik ili tabelu jednostavnim. To radi fct_lump()
:
gss_cat %>%
mutate(relig = fct_lump(relig)) %>%
count(relig)
## # A tibble: 2 x 2
## relig n
## <fct> <int>
## 1 Protestant 10846
## 2 Other 10637
fct_lump()
progresivno spaja najmanje grupe. U ovom slučaju to nije od velike pomoći: tačno je da je većina Amerikanaca u ovom istraživanju protestanske vere, ali verovatno smo previše skupili.
Umesto toga, možemo koristiti parametar n
da odredimo koliko grupa želimo:
gss_cat %>%
mutate(relig = fct_lump(relig, n = 10)) %>%
count(relig, sort = TRUE) %>%
print(n = Inf)
## # A tibble: 10 x 2
## relig n
## <fct> <int>
## 1 Protestant 10846
## 2 Catholic 5124
## 3 None 3523
## 4 Christian 689
## 5 Other 458
## 6 Jewish 388
## 7 Buddhism 147
## 8 Inter-nondenominational 109
## 9 Moslem/islam 104
## 10 Orthodox-christian 95
Sada ćemo videti kako da radimo sa datumima i vremenima u R-u. Na prvi pogled, datumi i vremena izgledaju jednostavno - stalno ih koristimo u svom životu, i čini se da ne izazivaju mnogo konfuzije. Međutim, kad krenemo da radimo sa datumima i vremenima, vidimo da je to dosta komplikovanije. Kao uvod, razmotrimo sledeća naizgled jednostavna tri pitanja:
Da li svaka godina ima 365 dana?
Da li svaki dan ima 24 sata?
Da li svaki minut ima 60 sekundi?
Naravno, znamo da nema svaka godina 365 dana, jer postoje i prestupne godine.
Takođe, mnogi delovi sveta (pa i mi) koriste letnje računanje vremena (DST), tako da neki dani imaju 23 sata, a drugi 25.
Dodatno, neki minuti imaju 61 sekundu, jer se s vremena na vreme dodaju sekunde za skok, jer se Zemljina rotacija postepeno usporava.
Datumi i vremena su teški jer moraju pomiriti dva fizička fenomena (rotaciju Zemlje oko svoje ose i oko Sunca) sa čitavim nizom geopolitičkih fenomena, uključujući mesece, vremenske zone i letnje računanje vremena.
U ovom poglavlju nećemo naučiti sve detalje o datumima i vremenima, ali dobićemo solidnu osnovu praktičnih veština koje će nam pomoći u uobičajenim izazovima analize podataka.
Ovo poglavlje će se fokusirati na paket lubridate
, koji olakšava rad sa datumima i vremenom u R-u. lubridate
nije deo tidyverse
, jer nam je potreban samo kada radimo sa datumima/vremenima. Takođe će nam trebati nycflights13
za podatke na kojima ćemo prikazati rad sa datumima i vremenom.
library(tidyverse)
library(lubridate)
library(nycflights13)
Postoje tri tipa podataka o datumu/vremenu koji se odnose na trenutak u vremenu:
date: Tiblovi ovo ispisuju kao <date>
.
time: Vreme u toku dana, tiblovi ga ispisuju kao <time>
.
date-time: Datum i vreme - jedinstveno identifikuje trenutak u vremenu (obično do najbliže sekunde). Tibl ovo ispisuje kao <dttm>
.
U ovom poglavlju ćemo se fokusirati samo na date
i date-time
, jer R nema osnovnu klasu za čuvanje vremena. Ako nam je tako nešto potrebno, možemo koristiti paket hms
.
Uvek treba koristiti najjednostavniji mogući tip podataka koji odgovara našim potrebama. To znači da ako možemo koristiti date
umesto date-time
, treba to i da radimo. Date-times
su znatno komplikovaniji zbog potrebe da se obrađuju vremenske zone, na koje ćemo se vratiti na kraju poglavlja.
Da bismo dobili trenutni datum ili datum i vreme možemo da koristimo today()
ili now()
:
today()
## [1] "2019-04-18"
now()
## [1] "2019-04-18 11:11:40 CEST"
Inače, postoje tri načina koja se često koriste za izdvajanje datuma/vremena:
Iz stringa.
Iz pojedinačnih date-time komponenti.
Iz postojećeg objekta datuma/vremena.
Podaci o datumu i vremenu često dolaze kao stringovi. Videli smo jedan pristup za raščlanjavanje stringova u date-time objekte ranije u delu o parsiranju. Drugi pristup je korišćenje pomagača koje pruža lubridate
. Oni automatski određuju format kada navedemo redosled komponenata. Da bismo ih koristili, treba da odredimo redosled po kojem se godina, mesec i dan pojavljuju u našim datumima, zatim rasporedimo “y”, “m” i “d” istim redosledom i to nam daje ime lubridate
funkcije koja će analizirati naš datum. Na primer:
ymd("2017-01-31")
## [1] "2017-01-31"
mdy("January 31st, 2017")
## [1] "2017-01-31"
dmy("31-Jan-2017")
## [1] "2017-01-31"
Ove funkcije kao argumente uzimaju i brojeve koji nisu pod navodnicima. Ovo je najsažetiji način da se kreira jedan objekat za datum/vreme, što nam može biti potrebno kada filtriramo podatke o datumu/vremenu. ymd()
je kratko i nedvosmisleno:
ymd(20170131)
## [1] "2017-01-31"
ymd()
i slične funkcije kreiraju datume. Da bismo kreirali datum-vreme, treba da dodamo donju crtu i jedno ili više od slova “h”, “m” i “s” (u zavisnosti od toga šta imamo) imenu funkcije raščlanjavanja:
ymd_hms("2017-01-31 20:11:59")
## [1] "2017-01-31 20:11:59 UTC"
mdy_hm("01/31/2017 08:01")
## [1] "2017-01-31 08:01:00 UTC"
Možemo i da stvorimo date-time od datuma tako što ćemo dodati vremensku zonu:
ymd(20170131, tz = "UTC")
## [1] "2017-01-31 UTC"
Umesto jednog stringa, ponekad ćemo imati pojedinačne komponente datuma i vremena raspoređene na više kolona. To je ono što imamo u bazi flights
:
flights %>%
select(year, month, day, hour, minute)
## # A tibble: 336,776 x 5
## year month day hour minute
## <int> <int> <int> <dbl> <dbl>
## 1 2013 1 1 5 15
## 2 2013 1 1 5 29
## 3 2013 1 1 5 40
## 4 2013 1 1 5 45
## 5 2013 1 1 6 0
## 6 2013 1 1 5 58
## 7 2013 1 1 6 0
## 8 2013 1 1 6 0
## 9 2013 1 1 6 0
## 10 2013 1 1 6 0
## # ... with 336,766 more rows
Da bismo kreirali datum/vreme iz ove vrste unosa, koristimo make_date()
za datume, ili make_datetime()
za date-time:
flights %>%
select(year, month, day, hour, minute) %>%
mutate(departure = make_datetime(year, month, day, hour, minute))
## # A tibble: 336,776 x 6
## year month day hour minute departure
## <int> <int> <int> <dbl> <dbl> <dttm>
## 1 2013 1 1 5 15 2013-01-01 05:15:00
## 2 2013 1 1 5 29 2013-01-01 05:29:00
## 3 2013 1 1 5 40 2013-01-01 05:40:00
## 4 2013 1 1 5 45 2013-01-01 05:45:00
## 5 2013 1 1 6 0 2013-01-01 06:00:00
## 6 2013 1 1 5 58 2013-01-01 05:58:00
## 7 2013 1 1 6 0 2013-01-01 06:00:00
## 8 2013 1 1 6 0 2013-01-01 06:00:00
## 9 2013 1 1 6 0 2013-01-01 06:00:00
## 10 2013 1 1 6 0 2013-01-01 06:00:00
## # ... with 336,766 more rows
Uradimo istu stvar za svaku od četiri vremenske kolone u flights
. Vremena su predstavljena u malo čudnom formatu, tako da koristimo modularnu aritmetiku da izvučemo komponente sati i minuta. Kada smo kreirali date-time promenljive, fokusiramo se na promenljive koje ćemo istraživati u ostatku poglavlja.
make_datetime_100 = function(year, month, day, time) {
make_datetime(year, month, day, time %/% 100, time %% 100)
}
flights_dt = flights %>%
filter(!is.na(dep_time), !is.na(arr_time)) %>%
mutate(
dep_time = make_datetime_100(year, month, day, dep_time),
arr_time = make_datetime_100(year, month, day, arr_time),
sched_dep_time = make_datetime_100(year, month, day, sched_dep_time),
sched_arr_time = make_datetime_100(year, month, day, sched_arr_time)
) %>%
select(origin, dest, ends_with("delay"), ends_with("time"))
flights_dt
## # A tibble: 328,063 x 9
## origin dest dep_delay arr_delay dep_time
## <chr> <chr> <dbl> <dbl> <dttm>
## 1 EWR IAH 2 11 2013-01-01 05:17:00
## 2 LGA IAH 4 20 2013-01-01 05:33:00
## 3 JFK MIA 2 33 2013-01-01 05:42:00
## 4 JFK BQN -1 -18 2013-01-01 05:44:00
## 5 LGA ATL -6 -25 2013-01-01 05:54:00
## 6 EWR ORD -4 12 2013-01-01 05:54:00
## 7 EWR FLL -5 19 2013-01-01 05:55:00
## 8 LGA IAD -3 -14 2013-01-01 05:57:00
## 9 JFK MCO -3 -8 2013-01-01 05:57:00
## 10 LGA ORD -2 8 2013-01-01 05:58:00
## # ... with 328,053 more rows, and 4 more variables: sched_dep_time <dttm>,
## # arr_time <dttm>, sched_arr_time <dttm>, air_time <dbl>
Uz pomoć ovih podataka možemo da vizuelizujemo raspodelu vremena poletanja tokom godine:
flights_dt %>%
ggplot(aes(dep_time)) +
geom_freqpoly(binwidth = 86400) # 86400 sekundi = 1 dan
Ili u jednom danu:
flights_dt %>%
filter(dep_time < ymd(20130102)) %>%
ggplot(aes(dep_time)) +
geom_freqpoly(binwidth = 600) # 600 s = 10 minuta
Primetimo da kada koristimo date-time u numeričkom kontekstu (kao u histogramu), 1 znači 1 sekundu, tako da binwidth od 86400 znači jedan dan. Za datume, 1 znači 1 dan.
Ako želimo da se prebacujemo između datuma i date-time, u tome će nam pomoći as_datetime()
i as_date()
:
as_datetime(today())
## [1] "2019-04-18 UTC"
as_date(now())
## [1] "2019-04-18"
Ponekad ćemo dobiti datum/vreme kao numerička odstupanja od (“Unix Epoch”) 1970-01-01. Ako je pomak u sekundama, koristimo as_datetime()
; ako je u danima, koristimo as_date()
.
as_datetime(60 * 60 * 10)
## [1] "1970-01-01 10:00:00 UTC"
as_date(365 * 10 + 2)
## [1] "1980-01-01"
Hajde sada da istražimo šta možemo da uradimo sa date-time podacima. Ovaj odeljak će se fokusirati na pristupne funkcije koje nam omogućavaju da dobijemo i podesimo pojedinačne komponente.
Možemo izvući pojedinačne delove datuma funkcijama year()
, month()
, mday()
(dan u mesecu), yday()
(dan u godini), wday()
(dan u nedelji), hour()
, minute()
, i second()
.
datetime = ymd_hms("2016-07-08 12:34:56")
year(datetime)
## [1] 2016
month(datetime)
## [1] 7
mday(datetime)
## [1] 8
yday(datetime)
## [1] 190
wday(datetime)
## [1] 6
Za month()
i wday()
možemo postaviti label = TRUE
da bi funkcije vratile skraćeni naziv meseca ili dana u nedelji. Ako podesimo abbr = FALSE
vratiće puno ime.
month(datetime, label = TRUE)
#> [1] Jul
#> 12 Levels: Jan < Feb < Mar < Apr < May < Jun < Jul < Aug < Sep < ... < Dec
wday(datetime, label = TRUE, abbr = FALSE)
#> [1] Friday
#> 7 Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < ... < Saturday
Možemo koristiti wday()
da bismo videli da li više letova odlazi radnim danima ili vikendom:
flights_dt %>%
mutate(wday = wday(dep_time, label = TRUE)) %>%
ggplot(aes(x = wday)) +
geom_bar()
Možemo primetiti interesantan šablon ako pogledamo prosečno kašnjenje pri odlasku po minutima u toku jednog sata. Izgleda da letovi koji poleću u minutima 20-30 i 50-60 imaju mnogo manje kašnjenja od ostalih!
flights_dt %>%
mutate(minute = minute(dep_time)) %>%
group_by(minute) %>%
summarise(
avg_delay = mean(arr_delay, na.rm = TRUE),
n = n()) %>%
ggplot(aes(minute, avg_delay)) +
geom_line()
Zanimljivo je da, ako pogledamo raspored predviđenih vremena polazaka, ne vidimo tako jak šablon:
sched_dep = flights_dt %>%
mutate(minute = minute(sched_dep_time)) %>%
group_by(minute) %>%
summarise(
avg_delay = mean(arr_delay, na.rm = TRUE),
n = n())
ggplot(sched_dep, aes(minute, avg_delay)) +
geom_line()
Zašto onda vidimo taj šablon kod pravih vremena odlazaka? Pa, kao i mnogi podaci koje su prikupili ljudi, postoji jaka pristrasnost prema letovima koji odlaze u “lepom” vremenu. Uvek treba biti oprezan za ovu vrstu uzoraka, kad god radimo sa podacima koji uključuju ljudsku procenu!
ggplot(sched_dep, aes(minute, n)) +
geom_line()
Može se vršiti i zaokruživanje datuma na obližnju jedinicu vremena, sa floor_date()
, round_date()
i ceiling_date()
. Svaka funkcija uzima vektor datuma za podešavanje, a zatim ime jedinice. Ovo nam, na primer, omogućava da nacrtamo broj letova nedeljno:
flights_dt %>%
count(week = floor_date(dep_time, "week")) %>%
ggplot(aes(week, n)) +
geom_line()
Izračunavanje razlike između zaokruženog i nezaokruženog datuma može biti posebno korisno.
Navedene funkcije za izdvajanje datuma/vremena možemo koristiti i za podešavanje pojedinačnih komponenata:
(datetime = ymd_hms("2016-07-08 12:34:56"))
## [1] "2016-07-08 12:34:56 UTC"
year(datetime) = 2020
datetime
## [1] "2020-07-08 12:34:56 UTC"
month(datetime) = 01
datetime
## [1] "2020-01-08 12:34:56 UTC"
hour(datetime) = hour(datetime) + 1
datetime
## [1] "2020-01-08 13:34:56 UTC"
Alternativno, možemo kreirati novi date-time sa update()
. Ovo nam takođe omogućava da postavimo više vrednosti odjednom.
update(datetime, year = 2020, month = 2, mday = 2, hour = 2)
## [1] "2020-02-02 02:34:56 UTC"
Ako su vrednosti prevelike, preći će u sledeću godinu, mesec, dan, sat ili minut:
ymd("2015-02-01") %>%
update(mday = 30)
## [1] "2015-03-02"
ymd("2015-02-01") %>%
update(hour = 400)
## [1] "2015-02-17 16:00:00 UTC"
Možemo koristiti update()
da bismo prikazali raspodelu letova u toku dana za svaki dan u godini:
flights_dt %>%
mutate(dep_hour = update(dep_time, yday = 1)) %>%
ggplot(aes(dep_hour)) +
geom_freqpoly(binwidth = 300)
Postavljanje većih komponenti datuma na konstantu je dobra tehnika koja nam omogućava da istražimo obrasce u manjim komponentama.
Sada ćemo saznati kako aritmetika sa datumima funkcioniše, npr. izdvajanje podskupova, sabiranje, itd. Usput ćemo saznati više o tri važne klase koje predstavljaju vremenski period:
duracije, koje predstavljaju tačan broj sekundi.
periodi, koji predstavljaju ljudske jedinice kao što su nedelje i meseci.
intervali, koji predstavljaju početnu i završnu tačku.
U R, kada oduzmemo dva datuma, dobijamo difftime
objekat:
# Koliko je stara osoba sa datim datumom rodjenja?
h_age = today() - ymd(19960516)
h_age
## Time difference of 8372 days
Difftime objekat beleži vremenski raspon sekundi, minuta, sati, dana ili sedmica. Ovo može da dovede do toga da bude malo komplikovano za rad, pa lubridate
obezbeđuje alternativu koja uvek koristi sekunde: duracije (duration).
as.duration(h_age)
## [1] "723340800s (~22.92 years)"
Duracije dolaze sa gomilom praktičnih konstruktora:
dseconds(15)
## [1] "15s"
dminutes(10)
## [1] "600s (~10 minutes)"
dhours(c(12, 24))
## [1] "43200s (~12 hours)" "86400s (~1 days)"
ddays(0:5)
## [1] "0s" "86400s (~1 days)" "172800s (~2 days)"
## [4] "259200s (~3 days)" "345600s (~4 days)" "432000s (~5 days)"
dweeks(3)
## [1] "1814400s (~3 weeks)"
dyears(1)
## [1] "31536000s (~52.14 weeks)"
Duracija uvek beleži vremenski raspon u sekundama. Veće jedinice se kreiraju pretvaranjem minuta, sati, dana, nedelja i godina u sekunde na standardan način (60 sekundi u minuti, 60 minuta u satu, 24 sata dnevno, 7 dana u nedelji, 365 dana u godini).
Možemo dodavati i množiti duracije:
2 * dyears(1)
## [1] "63072000s (~2 years)"
dyears(1) + dweeks(12) + dhours(15)
## [1] "38847600s (~1.23 years)"
Možemo dodati i oduzeti duracije do i od dana:
tomorrow = today() + ddays(1)
tomorrow
## [1] "2019-04-19"
last_year = today() - dyears(1)
last_year
## [1] "2018-04-18"
Međutim, pošto duracije predstavljaju tačan broj sekundi, ponekad možemo dobiti neočekivani rezultat:
one_pm = ymd_hms("2016-03-12 13:00:00", tz = "America/New_York")
one_pm
## [1] "2016-03-12 13:00:00 EST"
one_pm + ddays(1)
## [1] "2016-03-13 14:00:00 EDT"
Zašto je jedan dan posle 13 časova 12. marta, 14 časova 13. marta ?! Ako pažljivo pogledamo datum, primetićemo da su se vremenske zone promenile. Zbog letnjeg računanja vremena, 12. mart ima samo 23 sata, tako da ako dodamo pune dane izražene u sekundama, završićemo sa drugim vremenom.
Da bi rešio ovaj problem, lubridate
obezbeđuje periode (periods). Periodi su vremenski rasponi, ali nemaju fiksnu dužinu u sekundama, već rade sa “ljudskim” vremenom, kao što su dani i meseci. To im omogućava da rade na intuitivniji način:
one_pm
## [1] "2016-03-12 13:00:00 EST"
one_pm + days(1)
## [1] "2016-03-13 13:00:00 EDT"
Kao i duracije, periodi mogu biti kreirani brojnim funkcijama konstruktora.
seconds(15)
## [1] "15S"
minutes(10)
## [1] "10M 0S"
hours(c(12, 24))
## [1] "12H 0M 0S" "24H 0M 0S"
days(7)
## [1] "7d 0H 0M 0S"
months(1:6)
## [1] "1m 0d 0H 0M 0S" "2m 0d 0H 0M 0S" "3m 0d 0H 0M 0S" "4m 0d 0H 0M 0S"
## [5] "5m 0d 0H 0M 0S" "6m 0d 0H 0M 0S"
weeks(3)
## [1] "21d 0H 0M 0S"
years(1)
## [1] "1y 0m 0d 0H 0M 0S"
Možemo dodavati i množiti periode:
10 * (months(6) + days(1))
## [1] "60m 10d 0H 0M 0S"
days(50) + hours(25) + minutes(2)
## [1] "50d 25H 2M 0S"
I naravno, možemo ih dodati na datume. U poređenju sa duracijama, verovatnije je da će periodi uraditi ono što očekujemo:
# Prestupna godina
ymd("2016-01-01") + dyears(1)
## [1] "2016-12-31"
ymd("2016-01-01") + years(1)
## [1] "2017-01-01"
# Letnje racunanje vremena
one_pm + ddays(1)
## [1] "2016-03-13 14:00:00 EDT"
one_pm + days(1)
## [1] "2016-03-13 13:00:00 EDT"
Hajde da iskoristimo periode da popravimo neobičnost vezanu za naše datume letenja. Izgleda da su neki avioni stigli na svoje odredište pre nego što su napustili Njujork.
flights_dt %>%
filter(arr_time < dep_time)
## # A tibble: 10,633 x 9
## origin dest dep_delay arr_delay dep_time
## <chr> <chr> <dbl> <dbl> <dttm>
## 1 EWR BQN 9 -4 2013-01-01 19:29:00
## 2 JFK DFW 59 NA 2013-01-01 19:39:00
## 3 EWR TPA -2 9 2013-01-01 20:58:00
## 4 EWR SJU -6 -12 2013-01-01 21:02:00
## 5 EWR SFO 11 -14 2013-01-01 21:08:00
## 6 LGA FLL -10 -2 2013-01-01 21:20:00
## 7 EWR MCO 41 43 2013-01-01 21:21:00
## 8 JFK LAX -7 -24 2013-01-01 21:28:00
## 9 EWR FLL 49 28 2013-01-01 21:34:00
## 10 EWR FLL -9 -14 2013-01-01 21:36:00
## # ... with 10,623 more rows, and 4 more variables: sched_dep_time <dttm>,
## # arr_time <dttm>, sched_arr_time <dttm>, air_time <dbl>
Ovo su noćni letovi. Koristili smo istu informaciju o vremenu odlaska i dolaska, ali ovi letovi su stigli narednog dana. To možemo popraviti dodavanjem day(1)
na vreme dolaska svakog leta.
flights_dt = flights_dt %>%
mutate(
overnight = arr_time < dep_time,
arr_time = arr_time + days(overnight * 1),
sched_arr_time = sched_arr_time + days(overnight * 1)
)
Sada svi naši letovi poštuju zakone fizike.
flights_dt %>%
filter(overnight, arr_time < dep_time)
## # A tibble: 0 x 10
## # ... with 10 variables: origin <chr>, dest <chr>, dep_delay <dbl>,
## # arr_delay <dbl>, dep_time <dttm>, sched_dep_time <dttm>,
## # arr_time <dttm>, sched_arr_time <dttm>, air_time <dbl>,
## # overnight <lgl>
Očigledno je šta bi trebalo da vrati dyears(1)/ddays(365)
: jedan, jer su duracije uvek predstavljene brojem sekundi, a duracija od godinu dana je definisana kao 365 dana po sekundama.
Šta bi trebalo da vrati `years(1)/days(1)
? Pa, ako je godina 2015, trebalo bi da vrati 365, ali ako je bila 2016, trebalo bi da vrati 366! Nema dovoljno informacija da lubridate
da jedan jasan odgovor. Umesto toga, daje procenu, uz upozorenje:
years(1)/days(1)
## estimate only: convert to intervals for accuracy
## [1] 365.25
Ako želimo preciznije merenje, moraćemo da koristimo interval. Interval je trajanje sa početnom tačkom: to ga čini preciznim tako da možemo tačno odrediti dužinu:
next_year = today() + years(1)
(today() %--% next_year) / ddays(1)
## [1] 366
Da bismo saznali koliko perioda upada u interval, moramo koristiti celobrojno deljenje:
(today() %--% next_year) %/% days(1)
## Note: method with signature 'Timespan#Timespan' chosen for function '%/%',
## target signature 'Interval#Period'.
## "Interval#ANY", "ANY#Period" would also be valid
## [1] 366
Kako biramo između duracija, perioda i intervala? Kao i uvek, izaberemo najjednostavniju strukturu podataka koja rešava naš problem. Ako nam je samo stalo do fizičkog vremena, koristimo duracije; ako nam treba “ljudsko” vreme, koristimo periode; ako treba shvatiti koliko je dug raspon u ljudskim jedinicama, koristimo intervale.
Sledeća slika prikazuje dozvoljene aritmetičke operacije između različitih tipova podataka.
Vremenske zone su izuzetno komplikovana tema zbog njihove interakcije sa geopolitičkim entitetima. Srećom, ne moramo da se upuštamo u sve detalje jer oni nisu važni za analizu podataka, ali se možemo osvrnuti na nekoliko izazova koje bi trebalo da rešimo.
Prvi izazov je to što su imena vremenskih zona često dvosmislena. Na primer, Amerikanci imaju EST ili istočno standardno vreme. Međutim, i Australija i Kanada imaju EST! Da bi se izbegla konfuzija, R koristi međunarodne standardne IANA vremenske zone. One koriste konzistentnu shemu imenovanja "/"
, obično u obliku "<kontinent>/<grad>"
(postoji nekoliko izuzetaka jer nije svaka zemlja na kontinentu). Primeri su “America/New_York”, “Europe/Paris” i “Pacific/Auckland”.
Možda se pitate zašto vremenska zona koristi grad, kada obično mislite na vremenske zone koje su povezane sa državom ili regionom u zemlji. To je zato što baza podataka IANA mora da snimi decenijska pravila za vremenske zone. Tokom decenija, zemlje često menjaju imena (ili se raspadaju), ali imena gradova uglavnom ostaju ista. Drugi problem je to što ime treba da odražava ne samo sadašnje ponašanje, već i kompletnu istoriju. Na primer, postoje vremenske zone za “America/New_York” i “America/Detroit”. Oba grada trenutno koriste Istočno standardno vreme, ali 1969-1972 Mičigen (država u kojoj se nalazi Detroit), nije pratio letnje računanje vremena, pa mu je potrebno drugo ime.
Možemo saznati za šta R misli da je naša trenutna vremenska zona sa Sys.timezone()
:
Sys.timezone()
## [1] "Europe/Prague"
(Ako R ne zna, dobićemo NA
.)
Možemo pogledati kompletnu listu svih imena vremenskih zona sa OlsonNames()
:
length(OlsonNames())
## [1] 592
head(OlsonNames())
## [1] "Africa/Abidjan" "Africa/Accra" "Africa/Addis_Ababa"
## [4] "Africa/Algiers" "Africa/Asmara" "Africa/Asmera"
U R-u, vremenska zona je atribut date-time objekta, koji samo kontroliše štampanje. Na primer, ova tri objekta predstavljaju isti trenutak u vremenu:
(x1 = ymd_hms("2015-06-01 12:00:00", tz = "America/New_York"))
## [1] "2015-06-01 12:00:00 EDT"
(x2 = ymd_hms("2015-06-01 18:00:00", tz = "Europe/Copenhagen"))
## [1] "2015-06-01 18:00:00 CEST"
(x3 = ymd_hms("2015-06-02 04:00:00", tz = "Pacific/Auckland"))
## [1] "2015-06-02 04:00:00 NZST"
Možemo da potvrdimo da je isto vreme oduzimanjem:
x1 - x2
## Time difference of 0 secs
x1 - x3
## Time difference of 0 secs
Osim ako nije drugačije navedeno, lubridate
uvek koristi UTC. UTC (Coordinated Universal Time) je standardna vremenska zona koju koristi naučna zajednica i koja je približno jednaka svom prethodniku GMT (Greenvich Mean Time). Ona nema letnje računanje vremena, što je čini pogodnom za računanje. Operacije koje kombinuju date-time objekte, poput c()
, često će ispustiti vremensku zonu. U tom slučaju, datum-vreme će se prikazati u našoj lokalnoj vremenskoj zoni:
x4 = c(x1, x2, x3)
x4
## [1] "2015-06-01 12:00:00 EDT" "2015-06-01 12:00:00 EDT"
## [3] "2015-06-01 12:00:00 EDT"
Vremenska zona se može promeniti na dva načina:
x4a = with_tz(x4, tzone = "Australia/Lord_Howe")
x4a
## [1] "2015-06-02 02:30:00 +1030" "2015-06-02 02:30:00 +1030"
## [3] "2015-06-02 02:30:00 +1030"
x4a - x4
## Time differences in secs
## [1] 0 0 0
x4b = force_tz(x4, tzone = "Australia/Lord_Howe")
x4b
## [1] "2015-06-01 12:00:00 +1030" "2015-06-01 12:00:00 +1030"
## [3] "2015-06-01 12:00:00 +1030"
x4b - x4
## Time differences in hours
## [1] -14.5 -14.5 -14.5