1 简介
如何在 R 中使用日期和时间呢?你可能觉得日期和时间似乎很简单。我们在日常生活中一直在使用它们,它们似乎不会引起太多混乱。但当你对日期和时间了解得越多,它们似乎就越复杂。下面我们来看看这三个简单的问题:
- 每年有365天吗?
- 每天有24小时吗?
- 每分钟有60秒吗?
我们都知道不是每年都有 365 天,但是你知道如何确定闰年的规则吗?(它分为三个部分)。您也许还记得世界上很多地方都使用夏令时 (DST),因此有些日子有 23 小时,有些日子有 25 小时。您可能不知道有些分钟有 61 秒,因为地球的自转正在逐渐减慢,所以不时添加闰秒。
日期和时间很难计算,因为它们必须将两种物理现象(地球自转及其绕太阳公转)与月份、时区和夏令时在内的大量现象协调起来。下面关于日期和时间的处理,会帮助你解决常见的数据分析所面临的问题。
1.1 加载包
下面将介绍lubridate包,它可以更轻松地在 R 中处理日期和时间。 lubridate 不是 tidyverse 的核心部分,因为只有在处理日期/时间时才需要它。我们还需要 nycflights13 来获取练习的数据。
library(tidyverse)
library(lubridate)
library(nycflights13)
2 日期/时间的创建
有三种类型的日期/时间数据表示时间:
data:Tibbles 将此打印为
<date>
.time一天中:Tibbles 将此打印为
<time>
.data-time是一个日期加上时间:它唯一地标识一个瞬间(通常是最近的秒)。Tibbles 将此打印为
<dttm>
。在 R 的其他地方,这些被称为 POSIXct,但这个名字不一定有用。
接下来我们将学习日期和日期-时间。由于R没有一个本地类来存储时间,如果需要可以使用hms包。我们应该时刻想到用最简单的数据类型解决我们的问题,如果可以使用日期就绝不会使用日期时间。在实际应用中,由于存在时区问题,日期时间往往就有很多的问题。
获取当前日期或日期时间,您可以使用today()
或now()
:
today()
#> [1] "2022-01-05"
now()
#> [1] "2022-01-05 09:54:52 CST"
你可以通过三种方式创建日期/时间:
- 字符串。
- 日期时间组件。
- 现有的日期/时间对象。
2.1 字符串
日期/时间数据通常以字符串形式出现。你已经在 date-times 中看到了一种将字符串解析为日期时间的方法。另一种方法是使用 lubridate 提供的助手。一旦指定了组件的顺序,它们就会自动计算出格式。要使用它们,需要确定日期中出现的年、月和日的顺序,然后按相同顺序排列“y”、“m”和“d”。下面提供了解析日期的 lubridate 函数的名称。例如:
ymd("2017-01-31")
#> [1] "2017-01-31"
mdy("January 31st, 2017")
#> [1] "2017-01-31"
dmy("31-Jan-2017")
#> [1] "2017-01-31"
这些函数可以解析不带引号的数字。这是创建单个日期/时间对象的最简洁的方法,在过滤日期/时间数据时需要ymd()
函数明确:
ymd(20170131)
#> [1] "2017-01-31"
要创建日期时间,请在解析函数的名称中添加下划线以及“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 日期时间组件
有时日期时间的各个组件分布在多个列中,而不是单个字符串。这是在filghts数据中的内容:
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
#> # … with 336,770 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
#> # … with 336,770 more rows
让我们对flights
中的四个时间列中的每一个都做同样的事情。以稍微奇怪的格式表示时间,因此我们使用模算法来提取小时和分钟。
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 sched_dep_time
#> <chr> <chr> <dbl> <dbl> <dttm> <dttm>
#> 1 EWR IAH 2 11 2013-01-01 05:17:00 2013-01-01 05:15:00
#> 2 LGA IAH 4 20 2013-01-01 05:33:00 2013-01-01 05:29:00
#> 3 JFK MIA 2 33 2013-01-01 05:42:00 2013-01-01 05:40:00
#> 4 JFK BQN -1 -18 2013-01-01 05:44:00 2013-01-01 05:45:00
#> 5 LGA ATL -6 -25 2013-01-01 05:54:00 2013-01-01 06:00:00
#> 6 EWR ORD -4 12 2013-01-01 05:54:00 2013-01-01 05:58:00
#> # … with 328,057 more rows, and 3 more variables: arr_time <dttm>,
#> # 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 的 binwidth 表示一天。对于日期,1 表示 1 天。
2.3 其它类型
你可能想要在日期时间和日期之间切换。可以使用as_datetime()
和as_date()
:
as_datetime(today())
#> [1] "2022-01-05 UTC"
as_date(now())
#> [1] "2022-01-05"
有时你会得到日期/时间作为“Unix Epoch”,以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"
3 日期时间组件
前面我们已经知道如何将日期时间数据放入 R 的日期时间数据结构中,那这些日期时间可以做什么呢?下面将介绍获取和设置单个组件的访问器函数。
3.1 组件获取
可以使用访问器函数来提取日期的各个部分:year()
、month()
、mday()
(月份中的某天)、yday()
(一年中的某天)、wday()
(一周中的某天)hour()
、minute()
、 和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
对于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_dt %>%
mutate(wday = wday(dep_time, label = TRUE)) %>%
ggplot(aes(x = wday)) +
geom_bar()
如果我们查看一小时内按分钟计算出发的平均延迟,看起来在 20-30 分钟和 50-60 分钟后起飞的航班的延误比其他时间少得多!
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)
如果我们查看预定的出发时间,我们并没有看到如此强烈的模式:
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)
ggplot(sched_dep, aes(minute, avg_delay)) +
geom_line()
那么为什么我们会在实际出发时间中看到这种模式呢?好吧,就像人类收集的许多数据一样,对在“合适”起飞时间起飞的航班存在强烈偏见。每当您处理涉及人类判断的数据时,请始终警惕这种模式!
ggplot(sched_dep, aes(minute, n)) +
geom_line()
3.2 凑整
的另一种方法绘制各个部件是日期舍入到的时间附近的一个单元,与[floor_date()](http://lubridate.tidyverse.org/reference/round_date.html)
,[round_date()](http://lubridate.tidyverse.org/reference/round_date.html)
,和[ceiling_date()](http://lubridate.tidyverse.org/reference/round_date.html)
。每个函数都需要一个日期向量来调整,然后是单位的名称向下舍入(地板)、向上舍入(天花板)或舍入到。例如,这允许我们绘制每周的航班数量:
绘制单个组件的另一种方法是使用floor_date()
、round_date()
和ceiling_date()
将日期四舍零入到附近的时间单位。每个函数接受一个日期向量来调整,然后单位的名称向下四舍五入(floor)、向上四舍五入(ceiling)或四舍五入。例如,这可以让我们绘制每周的航班数量:
flights_dt %>%
count(week = floor_date(dep_time, "week")) %>%
ggplot(aes(week, n)) +
geom_line()
计算四舍五入日期和非四舍五入日期之间的差异可能特别有用。
3.3 组件设置
可以使用每个访问器函数来重新设置日期/时间的组成部分:
(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"
可以使用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)
4 时间跨度
下面将了解日期算术的工作原理,包括减法、加法和除法。明确时间跨度的三个重要类:
- durations,代表精确的秒数。
- period,代表人类单位,如周和月。
- interval,代表起点和终点。
4.1 持续时间
在 R 中,当两个日期相减时,你会得到一个 difftime 对象:
# How old is Hadley?
h_age <- today() - ymd(19791014)
h_age
#> Time difference of 15424 days
difftime 类对象记录秒、分钟、小时、天或周的时间跨度。这种歧义会使 difftimes 使用起来有点痛苦,所以 lubridate 提供了一个使用秒的替代方法:duration。
as.duration(h_age)
#> [1]"1332633600s (~42.23 years)"
持续时间还有其他的构造函数:
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] "31557600s (~1 years)"
持续时间总是以秒为单位记录时间跨度。更大的单位是通过标准速率将分钟、小时、天、周和年转换为秒来创建的(一分钟 60 秒,一小时 60 分钟,一天 24 小时,一周 7 天,一年 365 天)。
可以乘以持续时间:
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)
但是,由于持续时间代表精确的秒数,有时您可能会得到意外的结果:
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"
为什么3 月 12 日下午 1 点之后的一天,是 3 月 13 日下午 2 点这一天?!如果您仔细查看日期,您可能还会注意到时区已更改。由于 DST,3 月 12 日只有 23 小时,所以如果我们加上一整天的秒数,我们就会得到一个不同的时间。
4.2 周期
为了解决这个问题,lubridate 提供了period。周期是时间跨度,但没有以秒为单位的固定长度,而是使用“人类”时间,例如天和月。这使他们能够以更直观的方式工作:
one_pm
#> [1] "2016-03-12 13:00:00 EST"
one_pm + days(1)
#> [1] "2016-03-13 13:00:00 EDT"
像持续时间一样,可以使用许多友好的构造函数来创建周期。
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"
可以添加和乘:
10 * (months(6) + days(1))
#> [1] "60m 10d 0H 0M 0S"
days(50) + hours(25) + minutes(2)
#> [1] "50d 25H 2M 0S"
当然,将它们添加到日期。与持续时间相比,周期可能是你想要的结果:
# A leap year
ymd("2016-01-01") + dyears(1)
#> [1] "2016-12-31 06:00:00 UTC"
ymd("2016-01-01") + years(1)
#> [1] "2017-01-01"
# Daylight Savings Time
one_pm + ddays(1)
#> [1] "2016-03-13 14:00:00 EDT"
one_pm + days(1)
#> [1] "2016-03-13 13:00:00 EDT"
让我们使用周期来解决与我们的航班日期相关的奇怪问题。一些飞机似乎在离开纽约市之前就已经到达目的地。
flights_dt %>%
filter(arr_time < dep_time)
#> # A tibble: 10,633 x 9
#> origin dest dep_delay arr_delay dep_time sched_dep_time
#> <chr> <chr> <dbl> <dbl> <dttm> <dttm>
#> 1 EWR BQN 9 -4 2013-01-01 19:29:00 2013-01-01 19:20:00
#> 2 JFK DFW 59 NA 2013-01-01 19:39:00 2013-01-01 18:40:00
#> 3 EWR TPA -2 9 2013-01-01 20:58:00 2013-01-01 21:00:00
#> 4 EWR SJU -6 -12 2013-01-01 21:02:00 2013-01-01 21:08:00
#> 5 EWR SFO 11 -14 2013-01-01 21:08:00 2013-01-01 20:57:00
#> 6 LGA FLL -10 -2 2013-01-01 21:20:00 2013-01-01 21:30:00
#> # … with 10,627 more rows, and 3 more variables: arr_time <dttm>,
#> # 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 间隔
dyears(1) / ddays(365)
应该返回什么:很明显是一,因为持续时间总是用秒数表示,一年的持续时间被定义为 365 天的秒数。
years(1)
应该返回什么?如果年份是 2015 年,它应该返回 365,但如果是 2016 年,它应该返回 366!lubridate 没有足够的信息来给出一个明确的答案。它的作用是给出一个估计值,并给出警告:
years(1) / days(1)
#> [1] 365.25
如果您想要更准确的测量,则必须使用interval。间隔是具有起点的持续时间:这使其精确,因此可以确切地确定它是多长时间:
next_year <- today() + years(1)
(today() %--% next_year) / ddays(1)
#> [1] 365
要找出一个区间内有多少个周期,您需要使用整数除法:
(today() %--% next_year) %/% days(1)
#> [1] 365
4.4 总结
如何在持续时间、周期和间隔之间进行选择?与往常一样,选择最简单的数据结构来解决问题。如果只关心物理时间,请使用持续时间;如果需要添加人工时间,请使用周期;如果您需要计算以人为单位的跨度有多长,请使用间隔。
下图总结了不同数据类型之间允许的算术运算。
5 时区
在 R 中查看当前的时区:Sys.timezone()
:
Sys.timezone()
#> [1] "Asia/Taipei"
查看所有时区名称:OlsonNames()
:
length(OlsonNames())
#> [1] 593
head(OlsonNames())
#> [1] "Africa/Abidjan" "Africa/Accra" "Africa/Addis_Ababa"
#> [4] "Africa/Algiers" "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"
#> [3] "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" #> [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