2017年12月14日

日期和时间

在本章中,我们将介绍lubridate包,它提供了方便的处理日期和时间的函数。

library(lubridate)
library(dplyr)
library(ggplot2)
library(nycflights13)

在tibble中有到3类日期时间变量,分别为:

  • <date> 日期
  • <time> 时间
  • <dttm> 日期+时间

如果需要得到当前的日期和时间:

today()
[1] "2017-12-11"
now()
[1] "2017-12-11 15:17:51 CST"

CST:(美)中央时区(Central Standard Time)。

三种创建日期时间变量的方法:

  • 由单个字符串或数值创建日期时间变量
  • 由多个数值或字符串创建日期时间变量
  • 从其他数据类型转换为日期时间变量

由单个字符串或数值创建日期时间变量

ymd家族函数是最常用的创建日期时间变量的方法。根据字符串中年月日的排序,以同样的顺序安排y、m、d作为函数名。

ymd("2017-01-31")
[1] "2017-01-31"
ymd("17-01-31")
[1] "2017-01-31"
ymd("17 01 31")
[1] "2017-01-31"

ymd("170131")
[1] "2017-01-31"
mdy("01-31-17")
[1] "2017-01-31"
mdy("01/31/17")
[1] "2017-01-31"
dmy("31-Jan-2017")
[1] "2017-01-31"

ymd家族函数能够猜测一些不规则分隔的字符串。

ymd("201002-01", "201002-1", "20102-1")
[1] "2010-02-01" "2010-02-01" "2010-02-01"
dmy("0312-2010", "312-2010")
Warning: 1 failed to parse.
[1] "2010-12-03" NA          

使用数值也可以创建日期时间变量。

ymd(20170131)
[1] "2017-01-31"

ymd家族函数与h、m、s合并使用可以创建日期-时间变量。

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"

UTC:世界调整时间(Universal Time Coordinated)。
通过设置tz函数可以人为设置时区。

ymd(20170131, tz = "UTC")
[1] "2017-01-31 UTC"

由多个数值或字符串创建日期时间变量

使用make_date()可以创建日期变量,make_datetime()可以创建日期-时间变量。

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

创建另外4个日期-时间变量。

flights_dt <- flights %>% 
  filter(!is.na(dep_time), !is.na(arr_time)) %>% 
  mutate(
    dep_time = make_datetime(year, month, day, dep_time %/% 100, 
                             dep_time %% 100),
    arr_time = make_datetime(year, month, day, arr_time %/% 100, 
                             arr_time %% 100),
    sched_dep_time = make_datetime(year, month, day, sched_dep_time %/% 100, 
                                   sched_dep_time %% 100),
    sched_arr_time = make_datetime(year, month, day, sched_arr_time %/% 100, 
                                   sched_arr_time %% 100)
  ) %>% 
  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>

使用创建的日期-时间变量,可视化dep_time在年分布。

# 86400 s = 1 day
qplot(dep_time, data = flights_dt, geom = "freqpoly", binwidth = 86400)

可视化dep_time在2013年1月2日的分布。

flights_plot <- flights_dt %>% filter(dep_time < ymd(20130102))
# 600 s = 10 minutes
qplot(dep_time, data = flights_plot, geom = "freqpoly", binwidth = 600)

从其他数据类型创建日期时间变量

使用as_datetime()as_date()做日期和时间日期的转换。

as_datetime(today())
[1] "2017-12-11 UTC"
as_date(now())
[1] "2017-12-11"

练习

  1. 尝试转换无效日期时会发生什么?

    ymd(c("2010-10-10", "bananas"))
  2. 使用合适的函数转换下面的日期。

    d1 <- "1 1, 2010"
    d2 <- "2015-05-07"
    d3 <- "06-06-2017"
    d4 <- c("8 19 (2015)", "7 1 (2015)")
    d5 <- "12/30/14" # Dec 30, 2014

从日期时间变量中获取元素

year()month()hour()minute()second()
wday()(一星期中的第几天)、mday()(一月中的第几天)、 yday()(一年中的第几天)。

datetime <- ymd_hms("2016-07-08 12:34:56")
year(datetime)
[1] 2016
month(datetime)
[1] 7

yday(datetime)
[1] 190
wday(datetime)
[1] 6
mday(datetime)
[1] 8

month()wday()可以设置参数label = TRUE返回几月份和星期几的缩写,设置参数abbr = FALSE返回全称。

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

使用wday()可以看出工作日比休息日航班数量更多。

flights_plot <- flights_dt %>% mutate(wday = wday(dep_time, label = TRUE))
qplot(wday, data = flights_plot, geom = "bar")

观察不同分钟实际起飞航班的平均延迟,在20-30和50-60分钟起飞航班的平均延误比其他航班低很多。

dep <- flights_dt %>% mutate(minute = minute(dep_time)) %>% 
    group_by(minute) %>% 
    summarise(avg_delay = mean(arr_delay, na.rm = TRUE), n = n())
qplot(minute, avg_delay, data = dep, geom = "line")

有趣的是,如果观察不同分钟预计起飞航班的平均延误,并没有之前的模式。

sched_dep <- flights_dt %>% mutate(minute = minute(sched_dep_time)) %>% 
    group_by(minute) %>% summarise(avg_delay = mean(arr_delay, na.rm = TRUE), n = n())
qplot(minute, avg_delay, data = sched_dep, geom = "line")

为什么只有实际起飞时间的平均延误才能观察这个模式呢?与很多人为采集的数据一样,人为使飞机在“好”的时间起飞的倾向会造成偏差。当使用人为采集的数据时,需要注意这种模式。

qplot(minute, n, data = sched_dep, geom = "line")

近似

另一种从日期时间变量中获取元素的方法是使用floor_date()round_date()ceiling_date()将日期近似到临近时间。每个函数包含两个参数——日期时间向量和单位。
绘制每周的航班数量。

flights_plot <- flights_dt %>% count(week = floor_date(dep_time, "week")) 
qplot(week, n, data = flights_plot, geom = "line")

设置日期时间变量中元素

通过赋值可以设置时间日期变量中的元素。

datetime <- ymd_hms("2016-07-08 12:34:56")
(year(datetime) <- 2020)
[1] 2020
(month(datetime) <- 01)
[1] 1
(hour(datetime) <- hour(datetime) + 1)
[1] 13

也可以使用update()同时设置多个元素。

update(datetime, year = 2020, month = 2, mday = 2, hour = 2)
[1] "2020-02-02 02:34:56 UTC"

如果设置的元素值太大,会“滚动”。

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"

使用update()将日期设置为常数,观察起飞时间的日分布。

flights_plot <- flights_dt %>% mutate(dep_hour = update(dep_time, yday = 1)) 
qplot(dep_hour, data = flights_plot, geom = "freqpoly", binwidth = 300)

练习

  1. 在一年中每天的飞行时间分布是如何变化的?
  2. 比较dep_time - sched_dep_timedep_delay的关系,可以一一对应吗,解释你的发现。
  3. 比较air_time和到达时间与出发时间的间隔,解释你的发现。(提示:考虑机场位置)
  4. 平均延误时间在一天中是如何变化的,可以使用dep_timesched_dep_time吗?为什么?
  5. 如果想最小化延误,应该选择在星期几搭乘航班?

时间跨度

日期时间变量可以进行算数运算,包括-+/。有三个表示时间跨度的类。

  • duration 用精确的秒数表示。
  • period 用日常的单位表示,如周和月。
  • interval 用起点和终点表示。

duration

duration可以由一系列简单的构造函数产生。

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

duration以精确的秒数记录时间跨度。分钟、小时、天、周、年等更大的单位均转换为秒。
duration可以进行+、x运算。

2 * dyears(1)
[1] "63072000s (~2 years)"
dyears(1) + dweeks(12) + dhours(15)
[1] "38847600s (~1.23 years)"

可以从日期中加和减duration。

tomorrow <- today() + ddays(1)
last_year <- today() - dyears(1)

因为duration由精确的秒数表示,有时会有意想不到的结果。

(one_pm <- ymd_hms("2016-03-12 13:00:00", tz = "America/New_York"))
[1] "2016-03-12 13:00:00 EST"
one_pm + ddays(1)
[1] "2016-03-13 14:00:00 EDT"

为什么是2016年3月12日下午1点后的一天,是2016年3月13日下午2点?如果仔细观察日期,会注意到时区发生改变。由于夏时制的原因,2016年3月12日只有23个小时。

period

为了解决上述问题,lubridate包提供了period。period以日常的时间对时间跨度进行计算,更符合日常认知。

one_pm
[1] "2016-03-12 13:00:00 EST"
one_pm + days(1)
[1] "2016-03-13 13:00:00 EDT"

与duration相同,可以通过构造函数生成period。

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"

period可以进行+、x运算。

10 * (months(6) + days(1))
[1] "60m 10d 0H 0M 0S"
days(50) + hours(25) + minutes(2)
[1] "50d 25H 2M 0S"

将period与日期相加,与duration相比,更符合日常认知(避免闰年、夏令时的问题)。

ymd("2016-01-01") + dyears(1)
[1] "2016-12-31"
ymd("2016-01-01") + years(1)
[1] "2017-01-01"
one_pm + ddays(1)
[1] "2016-03-13 14:00:00 EDT"
one_pm + days(1)
[1] "2016-03-13 13:00:00 EDT"

使用period修正某些奇怪的航班时间,即一些飞机的到达时间要比起飞的时间早。

flights_dt %>% filter(arr_time < dep_time) %>% select(arr_time, dep_time)
# A tibble: 10,633 x 2
              arr_time            dep_time
                <dttm>              <dttm>
 1 2013-01-01 00:03:00 2013-01-01 19:29:00
 2 2013-01-01 00:29:00 2013-01-01 19:39:00
 3 2013-01-01 00:08:00 2013-01-01 20:58:00
 4 2013-01-01 01:46:00 2013-01-01 21:02:00
 5 2013-01-01 00:25:00 2013-01-01 21:08:00
 6 2013-01-01 00:16:00 2013-01-01 21:20:00
 7 2013-01-01 00:06:00 2013-01-01 21:21:00
 8 2013-01-01 00:26:00 2013-01-01 21:28:00
 9 2013-01-01 00:20:00 2013-01-01 21:34:00
10 2013-01-01 00:25:00 2013-01-01 21:36:00
# ... with 10,623 more rows

由于生成起飞和到达时间时使用了相同的日期,而这些飞机实际是后一天到达的,需要在到达时间中加days(1)

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

现在所有的飞机的航班时间均符合常识了。

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>

interval

dyears(1) / ddays(1)的结果是365。因为duration是用秒数表示的,一年的duration等于365天的duration。
years(1) / days(1)的结果如果是2015年,为365;如果是2016年,为366。由于没有年份信息,给出估计值并警告(warning)。

years(1) / days(1)
estimate only: convert to intervals for accuracy
[1] 365.25

如果要更精确计算,需要用interval。interval是记录了起始时间的时间跨度。

next_year <- today() + years(1)
today() %--% next_year
[1] 2017-12-12 UTC--2018-12-12 UTC
(today() %--% next_year) / ddays(1)
[1] 365

使用整数除法计算interval包含多少period。

(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] 365

总结

如何选择duration、period、interval?选择能够解决问题的最简单的类。如果只关心实际时间,使用duration; 如果需要日常认知的时间,使用period;如果需要知道时间跨度有多少个日常单位(月、日),使用interval。 数据类型类型与允许使用的算数运算。

练习

  1. 为什么只有months()而没有dmonths()?
  2. 创建一个包含2015年每个月第一天日期的向量。创建一个包含今年每个月第一天日期的向量。

时区

Sys.timezone()可以查看R中的当前时区。

Sys.timezone()
[1] "Asia/Taipei"

时区是日期-时间变量是一个属性。以下三个对象表示相同的时间。

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

可以验证。

x1 - x2
Time difference of 0 secs
x1 - x3
Time difference of 0 secs

除非另外指定,lubridate包总是使用UTC。UTC(协调世界时)是科学界中使用的标准时区,大致相当于其前身GMT(格林尼治标准时间)。UTC没有夏时制,因此计算也更加方便。
如果使用c()等,会丢掉对象原先的时区,而显示在本地时区。

x4 <- c(x1, x2, x3)
x4
[1] "2015-06-02 CST" "2015-06-02 CST" "2015-06-02 CST"