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)

Krerianje faktora

Pretpostavimo da želimo promenljivu koja čuva mesece:

x1 = c("Dec", "Apr", "Jan", "Mar")

Korišćenje vektora karaktera za čuvanje ove promenljive ima dva problema:

  1. Postoji samo dvanaest različitih meseci, i ništa nas ne spasava od pogrešnih unosa:
x2 = c("Dec", "Apr", "Jam", "Mar")
x2
## [1] "Dec" "Apr" "Jam" "Mar"
  1. Ne sortira se na odgovarajući način:
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.

Promena redosleda nivoa

Č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:

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()

Promena vrednosti nivoa faktora

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

Datumi i vremena

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:

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)

Kreiranje datuma/vremena

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.

Iz stringova

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"

Od pojedinačnih komponenti

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.

Od drugih tipova

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"

Date-time komponente

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.

Vađenje komponenata

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()

Zaokruživanje

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.

Podešavanje komponenti

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.

Vremenski razmaci

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.

Duracije

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.

Periodi

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>

Intervali

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

Rezime

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

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:

  • Zadržimo trenutak u vremenu istim i promenimo način na koji se prikazuje. Koristimo ovo kada je trenutak tačan, ali želimo prirodniji prikaz.
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
  • Promenimo trenutni trenutak. Koristimo ovo kada imamo trenutak koji je označen pogrešnom vremenskom zonom i moramo ga popraviti.
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