11.日期和时间

2022-01-07  本文已影响0人  denghb001

1 简介

如何在 R 中使用日期和时间呢?你可能觉得日期和时间似乎很简单。我们在日常生活中一直在使用它们,它们似乎不会引起太多混乱。但当你对日期和时间了解得越多,它们似乎就越复杂。下面我们来看看这三个简单的问题:

我们都知道不是每年都有 365 天,但是你知道如何确定闰年的规则吗?(它分为三个部分)。您也许还记得世界上很多地方都使用夏令时 (DST),因此有些日子有 23 小时,有些日子有 25 小时。您可能不知道有些分钟有 61 秒,因为地球的自转正在逐渐减慢,所以不时添加闰秒。

日期和时间很难计算,因为它们必须将两种物理现象(地球自转及其绕太阳公转)与月份、时区和夏令时在内的大量现象协调起来。下面关于日期和时间的处理,会帮助你解决常见的数据分析所面临的问题。

1.1 加载包

下面将介绍lubridate包,它可以更轻松地在 R 中处理日期和时间。 lubridate 不是 tidyverse 的核心部分,因为只有在处理日期/时间时才需要它。我们还需要 nycflights13 来获取练习的数据。

library(tidyverse)

library(lubridate)
library(nycflights13)

2 日期/时间的创建

有三种类型的日期/时间数据表示时间:

接下来我们将学习日期和日期-时间。由于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
image

在一天内出发时间的分布:

flights_dt %>% 
  filter(dep_time < ymd(20130102)) %>% 
  ggplot(aes(dep_time)) + 
  geom_freqpoly(binwidth = 600) # 600 s = 10 minutes
image

请注意,在数字上下文中(如在直方图中)使用日期时间时,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()
image

如果我们查看一小时内按分钟计算出发的平均延迟,看起来在 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)
image
如果我们查看预定的出发时间,我们并没有看到如此强烈的模式:
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()
image

那么为什么我们会在实际出发时间中看到这种模式呢?好吧,就像人类收集的许多数据一样,对在“合适”起飞时间起飞的航班存在强烈偏见。每当您处理涉及人类判断的数据时,请始终警惕这种模式!

ggplot(sched_dep, aes(minute, n)) +
  geom_line()
image

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

计算四舍五入日期和非四舍五入日期之间的差异可能特别有用。

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

4 时间跨度

下面将了解日期算术的工作原理,包括减法、加法和除法。明确时间跨度的三个重要类:

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"

可以通过两种方式更改时区:

上一篇下一篇

猜你喜欢

热点阅读