1. 前言
本节我们将介绍 R
中的时间和日期的处理。乍一看,你可能觉得这很简单。
让我们来看看下面三个问题:
- 每年都是
365
天? - 每天都有
24
小时? - 每分钟是
60
秒?
当然,你肯定知道一年不一定都是 365
天,但是你知道哪一年是闰年吗?
世界上很多地方都使用夏令时,所以有些日子是 23
小时,有些日子是 25
小时。
你可能不知道有些分钟有 61
秒,因为地球自转正逐渐减慢,时不时地增加闰秒。
有时候,时间和日期是很困难的。因为需要协调地球的自转和公转与地区的月份、时区、夏令时之间的关系。
下面我们将详细介绍如何处理这些数据
1.1 导入
本节的重点是使用 lubridate
包来处理 R
中的日期和时间。
lubridate
不是核心的 tidyverse
包,所以需要手动导入。并利用 nycflights13
作为例子数据来练习
library(tidyverse)
library(lubridate)
library(nycflights13)
2. 创建时间/日期
总共有 3 种形式的日期/时间数据
date
:tibble
中标题的<date>
time
: 一天内的时间,tibble
中标题的<time>
date-time
: 日期+时间,唯一标识时间点,tibble
中标题的<dttm>
本节我们只关注日期和日期+时间,并尽可能使其简单化,如果能用日期就不用日期+时间
要获取当前的日期和日期+时间,可以
> today()
[1] "2021-01-27"
> now()
[1] "2021-01-27 19:48:46 CST"
此外,有三种方法创建日期/时间
- 根据字符串创建
- 单个日期-时间组件
- 现存的日期-时间
它们的工作方式如下
2.1 字符串
日期/时间通常以字符串的形式出现,一种方式我们已经在前面的 readr
中介绍过了,另一种方法是使用 lubridate
包提供的工具
通过制定字符串中年(y
)、月(m
)、日(d
)的出现顺序,会自动转换为日期格式。
> ymd("2017-01-31")
[1] "2017-01-31"
> mdy("January 31st, 2017")
[1] "2017-03-01"
> dmy("31-Jan-2017")
[1] "2017-01-31"
这些函数也可以接受不带引号的数字
> ymd(20170131)
[1] "2017-01-31"
想要创建日期时间的话,可以在 ymd
(mdy
、dmy
等) 后面添加一个下划线以及一个或多个 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"
还可以添加时区
> ymd(20170131, tz = "UTC")
[1] "2017-01-31 UTC"
2.2 单个组件
有时候我们的数据会像 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
对于这种数据,可以使用 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
让我们对 flights
剩下的其他四列时间进行格式化
> 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 sched_dep_time arr_time
<chr> <chr> <dbl> <dbl> <dttm> <dttm> <dttm>
1 EWR IAH 2 11 2013-01-01 05:17:00 2013-01-01 05:15:00 2013-01-01 08:30:00
2 LGA IAH 4 20 2013-01-01 05:33:00 2013-01-01 05:29:00 2013-01-01 08:50:00
3 JFK MIA 2 33 2013-01-01 05:42:00 2013-01-01 05:40:00 2013-01-01 09:23:00
4 JFK BQN -1 -18 2013-01-01 05:44:00 2013-01-01 05:45:00 2013-01-01 10:04:00
5 LGA ATL -6 -25 2013-01-01 05:54:00 2013-01-01 06:00:00 2013-01-01 08:12:00
6 EWR ORD -4 12 2013-01-01 05:54:00 2013-01-01 05:58:00 2013-01-01 07:40:00
7 EWR FLL -5 19 2013-01-01 05:55:00 2013-01-01 06:00:00 2013-01-01 09:13:00
8 LGA IAD -3 -14 2013-01-01 05:57:00 2013-01-01 06:00:00 2013-01-01 07:09:00
9 JFK MCO -3 -8 2013-01-01 05:57:00 2013-01-01 06:00:00 2013-01-01 08:38:00
10 LGA ORD -2 8 2013-01-01 05:58:00 2013-01-01 06:00:00 2013-01-01 07:53:00
# … with 328,053 more rows, and 2 more variables: sched_arr_time <dttm>, air_time <dbl>
然后,我们可以看看全年的出发时间的分布
> flights_dt %>%
+ ggplot(aes(dep_time)) +
+ geom_freqpoly(binwidth = 86400) # 86400 seconds = 1 day
也可以查看一天内的出发情况
> flights_dt %>%
+ filter(dep_time < ymd(20130102)) %>%
+ ggplot(aes(dep_time)) +
+ geom_freqpoly(binwidth = 600) # 600 s = 10 minutes
注意:在日期中 1
表示一天,在时间中 1
表示一秒,86400
秒为一天
2.3 其他类型
有时你可能需要自日期-时间和日期之间相互切换,可以使用 as_datetime()
和 as_date()
> as_datetime(today())
[1] "2021-01-27 UTC"
> as_date(now())
[1] "2021-01-27"
如果想获取相对于 1970-01-01
的偏移的日期/时间,如果偏移是秒,使用 as_datetime()
,如果是天,使用 as_date()
> as_datetime(60 * 60 * 10)
[1] "1970-01-01 10:00:00 UTC"
> as_date(365 * 10 + 2)
[1] "1980-01-01"
2.4 思考练习
- 如果解析包含无效日期的字符串会怎样?
ymd(c("2010-10-10", "bananas"))
-
tzone
参数对today()
有什么作用? -
使用相应的
lubridate
函数来解析以下每个日期
d1 <- "January 1, 2010"
d2 <- "2015-Mar-07"
d3 <- "06-Jun-2017"
d4 <- c("August 19 (2015)", "July 1 (2015)")
d5 <- "12/30/14" # Dec 30, 2014
3. 日期-时间组件
这一部分的重点是介绍访问器函数,用于获取或设置日期-时间组件。
3.1 获取组件
你可以使用下面的访问器函数来获取日期时间组件:
year()
month()
mday()
月份的天,yday()
一年中的天,wday()
星期,hour()
minute()
second()
> datetime <- ymd_hms("2020-07-08 12:34:56")
> year(datetime)
[1] 2020
> month(datetime)
[1] 7
> mday(datetime)
[1] 8
> yday(datetime)
[1] 190
> wday(datetime)
[1] 4
对于 month()
和 wday()
,您可以设置 label = TRUE
来返回一个月或星期的缩写名称。
设置 abbr = FALSE
返回全名
> month(datetime, label = TRUE)
[1] 7
Levels: 1 < 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < 11 < 12
> wday(datetime, label = TRUE, abbr = FALSE)
[1] 星期三
Levels: 星期日 < 星期一 < 星期二 < 星期三 < 星期四 < 星期五 < 星期六
我们可以使用 wday()
来查看一周的航班
> flights_dt %>%
+ mutate(wday = wday(dep_time, label = TRUE)) %>%
+ ggplot(aes(x = wday)) +
+ geom_bar() +
+ theme(text = element_text(family='Kai')) # 显示中文
如果我们看一小时内每分钟的平均延迟,会看到一个有趣的现象。
> 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()
`summarise()` ungrouping output (override with `.groups` argument)
我们可以看到,在 20-30
分钟和 50-60
分钟起飞的航班延误要比其他时间少得多
有趣的是,如果我们看预计出发时间,则没有这样的模式
> sched_dep <- flights_dt %>%
+ mutate(minute = minute(sched_dep_time)) %>%
+ group_by(minute) %>%
+ summarise(
+ avg_delay = mean(arr_delay, na.rm = TRUE),
+ n = n())
`summarise()` ungrouping output (override with `.groups` argument)
> #> `summarise()` ungrouping output (override with `.groups` argument)
>
> ggplot(sched_dep, aes(minute, avg_delay)) +
+ geom_line()
3.2 四舍五入
类似于浮点数,我们可以使用 floor_date()
,round_date()
和 ceiling_date()
将日期四舍五入到附近的时间单位
> flights_dt %>%
+ count(week = floor_date(dep_time, "week")) %>%
+ ggplot(aes(week, n)) +
+ geom_line()
3.3 设置组件
您还可以使用每个访问器函数来设置日期/时间的组成部分
> (datetime <- ymd_hms("2010-10-08 12:34:56"))
[1] "2010-10-08 12:34:56 UTC"
> year(datetime) <- 2020
> datetime
[1] "2020-10-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"
此外,您可以使用 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_dt %>%
+ mutate(dep_hour = update(dep_time, yday = 1)) %>%
+ ggplot(aes(dep_hour)) +
+ geom_freqpoly(binwidth = 300)
3.4 思考练习
-
一天中的飞行时间分布在一年中如何变化
-
将
air_time
与出发和到达之间的持续时间进行比较,解释您的发现(提示:考虑机场的位置)。 -
平均延迟时间在一天中如何变化?您应该使用
dep_time
还是sched_dep_time
?为什么? -
如果想最大程度地减少飞机晚点的情况,应该在一周中的哪一天离开?
4. 时间跨度
接下来,我们将介绍日期算术运算。包括加法、减法和除法。
在这里,我们要先了解三个代表时间跨度的类:
durations
: 持续时间,表示精确的秒数periods
: 周期,代表有单位的日期,如几周和几个月intervals
: 区间,代表起点和终点的区间
4.1 periods
在 R
中,当两个日期相减时,将得到一个 difftime
对象
> h_age <- today() - ymd(19791014)
> h_age
Time difference of 15082 days
difftime
类对象记录了秒,分钟,小时,天或周的时间跨度
这种模糊性会使 difftime
有点难以处理,因此 lubridate
提供了一种始终用秒来表示的方法:duration
。
> as.duration(h_age)
[1] "1303084800s (~41.29 years)"
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)" "259200s (~3 days)" "345600s (~4 days)"
[6] "432000s (~5 days)"
> dweeks(3)
[1] "1814400s (~3 weeks)"
> dyears(1)
[1] "31557600s (~1 years)"
duration
始终以秒为单位来记录时间,可以通过传入分钟、小时、天、周和年等单位来创建较大的单位。
你可以使用加法或乘法
> 2 * dyears(1)
[1] "63115200s (~2 years)"
> dyears(1) + dweeks(12) + dhours(15)
[1] "38869200s (~1.23 years)"
对天的加法减法
tomorrow <- today() + ddays(1)
last_year <- today() - dyears(1)
但是由于 duration
代表的是精确的时间,因此有时可能会得到意外的结果
> 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"
为什么会变成下午两点呢?你可以注意到了,时区已经变了。
由于 DST
的原因,3
月 12
日只有 23
个小时,因此,如果加上一天的秒数,我们将得到不同的时间。
4.2 Periods
为了解决这个问题,lubridate
提供了 periods
,它是一个时间跨度,但是没有固定的秒数。
而是人为定义的时间单位,如天或月。
> one_pm
[1] "2016-03-12 13:00:00 EST"
> one_pm + days(1)
[1] "2016-03-13 13:00:00 EDT"
类似 duration
,也有许多函数用于创建 periods
> 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" "5m 0d 0H 0M 0S"
[6] "6m 0d 0H 0M 0S"
> weeks(3)
[1] "21d 0H 0M 0S"
> years(1)
[1] "1y 0m 0d 0H 0M 0S"
对 periods
进行加法和乘法
> 10 * (months(6) + days(1))
[1] "60m 10d 0H 0M 0S"
> days(50) + hours(25) + minutes(2)
[1] "50d 25H 2M 0S"
与 durations
相比较,periods
更符合我们的预期
> ymd("2016-01-01") + dyears(1)
[1] "2016-12-31 06:00:00 UTC"
> 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"
来让我用 periods
来解决与航班日期有关的问题。有些飞机在离开纽约市之前似乎已经到达目的地
> flights_dt %>%
+ filter(arr_time < dep_time)
# A tibble: 10,633 x 9
origin dest dep_delay arr_delay dep_time sched_dep_time arr_time
<chr> <chr> <dbl> <dbl> <dttm> <dttm> <dttm>
1 EWR BQN 9 -4 2013-01-01 19:29:00 2013-01-01 19:20:00 2013-01-01 00:03:00
2 JFK DFW 59 NA 2013-01-01 19:39:00 2013-01-01 18:40:00 2013-01-01 00:29:00
3 EWR TPA -2 9 2013-01-01 20:58:00 2013-01-01 21:00:00 2013-01-01 00:08:00
4 EWR SJU -6 -12 2013-01-01 21:02:00 2013-01-01 21:08:00 2013-01-01 01:46:00
5 EWR SFO 11 -14 2013-01-01 21:08:00 2013-01-01 20:57:00 2013-01-01 00:25:00
6 LGA FLL -10 -2 2013-01-01 21:20:00 2013-01-01 21:30:00 2013-01-01 00:16:00
7 EWR MCO 41 43 2013-01-01 21:21:00 2013-01-01 20:40:00 2013-01-01 00:06:00
8 JFK LAX -7 -24 2013-01-01 21:28:00 2013-01-01 21:35:00 2013-01-01 00:26:00
9 EWR FLL 49 28 2013-01-01 21:34:00 2013-01-01 20:45:00 2013-01-01 00:20:00
10 EWR FLL -9 -14 2013-01-01 21:36:00 2013-01-01 21:45:00 2013-01-01 00:25:00
# … with 10,623 more rows, and 2 more variables: sched_arr_time <dttm>, air_time <dbl>
这些是夜间航班。起飞和到达时间使用了相同的日期信息,但这些航班是在第二天到达的。
我们可以通过在每个夜间航班的到达时间上加上 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>
4.3 intervals
dyears(1) / ddays(365)
应该返回的是 1
,因此 durations
总是表示秒数,而一年表示为 365
天的秒数
那 years(1) / days(1)
返回的是什么?如果是 2015
年返回的是 365
,而 2016
返回的是 366
对于 lubridate
来说,当没有足够的信息来给出一个明确的答案。它会给出一个估计值
> years(1) / days(1)
[1] 365.25
如果你想要更精确的值,可以使用 interval
。
interval
是一个有起点的 durations
,因此您可以准确地确定它的持续时间
> next_year <- today() + years(1)
> (today() %--% next_year) / ddays(1)
[1] 365
要找出间隔中有多少个周期,您需要使用整数除法
> (today() %--% next_year) %/% days(1)
[1] 365
4.4 总结
如何在持续时间、周期和间隔之间进行选择?一如既往,选择最简单的数据结构来解决您的问题。
- 如果您只关心物理时间,使用持续时间;
- 如果需要增加人为定义的时间,使用周期;
- 如果需要计算人为定义的单位长度的跨度,使用一个间隔。
4.5 思考练习
-
创建一个向量,存储了
2015
年每月的第一天。创建一个包含本年度每月的第一天的日期向量。 -
编写一个函数,给定您的生日(以日期为单位),返回您的年龄(岁)。
5. 时区
获取当前时区
> Sys.timezone()
[1] "Asia/Shanghai"
使用 OlsonNames()
查看所有时区名称的完整列表:
> length(OlsonNames())
[1] 593
> head(OlsonNames())
[1] "Africa/Abidjan" "Africa/Accra" "Africa/Addis_Ababa" "Africa/Algiers"
[5] "Africa/Asmara" "Africa/Asmera"
在 R
中,时区仅仅是控制打印日期时间的属性。
例如,下面这三个对象表示同一时间点
> (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
(格林威治标准时间)
> x4 <- c(x1, x2, x3)
> x4
[1] "2015-06-01 12:00:00 EDT" "2015-06-01 12:00:00 EDT" "2015-06-01 12:00:00 EDT"
你可以通过两种方式来更改时区
- 保持时间不变,只更改其显示方式。即时间正确但您想要更自然的显示时,可以使用此功能
> x4a <- with_tz(x4, tzone = "Australia/Lord_Howe")
> x4a
[1] "2015-06-02 02:30:00 +1030" "2015-06-02 02:30:00 +1030" "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" "2015-06-01 12:00:00 +1030"
> x4b - x4
Time differences in hours
[1] -14.5 -14.5 -14.5