17  Dates and times

17.1 Introduction

本章将向你展示如何在 R 中处理日期和时间。 乍看之下,日期和时间似乎很简单。 你在日常生活中经常使用它们,而且它们似乎不会引起太多困惑。 然而,你对日期和时间了解得越多,就越会发现它们的复杂之处!

先来思考一下:一年有多少天,一天有多少小时。 你或许记得大多数年份有 365 天,但闰年有 366 天。 你知道判断闰年的完整规则吗1? 一天的小时数则稍不明显:大多数日子有 24 小时,但在使用夏令时(DST)的地区,每年会有一天只有 23 小时,另一天则有 25 小时。

日期和时间之所以复杂,是因为它们必须协调两种自然现象(地球自转和绕太阳公转)与一系列地缘政治现象(包括月份、时区和夏令时)。 本章不会涵盖日期和时间的每一个细节,但会为你打下扎实的实践技能基础,帮助你应对常见的数据分析挑战。

我们将首先展示如何从不同输入创建日期时间,然后介绍在获得日期时间后如何提取年、月、日等组成部分。 接着,我们将深入探讨处理时间跨度的棘手主题,根据不同的使用目标,时间跨度有多种表现形式。 最后,我们将简要讨论时区带来的额外挑战。

17.1.1 Prerequisites

本章将重点介绍 lubridate 包,它能让你在 R 中更轻松地处理日期和时间。 根据 tidyverse 的最新版本,lubridate 已成为核心 tidyverse 的一部分。 我们还需要 nycflights13 包来提供练习数据。

17.2 Creating date/times

有三种类型的日期/时间数据用于指代特定时刻:

  • A date。 Tibbles 会将其显示为 <date>

  • A time within a day。 Tibbles 会将其显示为 <time>

  • A date-time 是日期加上时间: 它能唯一标识某个时刻(通常精确到秒)。 Tibbles 会将其显示为 <dttm>。 Base R 将其称为 POSIXct, 但这个名称并不直观.

本章我们将重点关注日期和日期时间,因为 R 没有原生类来存储时间。 如果你需要处理时间,可以使用 hms 包。

你应始终使用能满足需求的最简单数据类型。 这意味着如果可以用 date 代替 date-time,就应该使用 date。 Date-times 要复杂得多,因为需要处理时区问题,我们将在本章末尾讨论这一点。

要获取当前 date 或 date-time,可以使用 today()now()

today()
#> [1] "2026-01-22"
now()
#> [1] "2026-01-22 23:21:08 UTC"

除此之外,以下部分将介绍四种常见的创建 date/time 的方法:

  • 使用 readr 读取文件时。
  • 从字符串解析。
  • 从独立的 date-time 组件组合。
  • 从已有的 date/time 对象转换。

17.2.1 During import

如果你的 CSV 文件包含 ISO8601 格式的 date 或 date-time,你无需进行额外操作;readr 会自动识别它:

csv <- "
  date,datetime
  2022-01-02,2022-01-02 05:12
"
read_csv(csv)
#> # A tibble: 1 × 2
#>   date       datetime           
#>   <date>     <dttm>             
#> 1 2022-01-02 2022-01-02 05:12:00

如果你之前没听说过 ISO8601,它是一种国际标准2,用于书写日期,其日期组成部分按从大到小的顺序排列,并用 - 分隔。 例如,在 ISO8601 标准中,2022 年 5 月 3 日写作 2022-05-03。ISO8601 日期也可以包含时间,其中时、分、秒用 : 分隔,而日期和时间部分则用 T 或空格分隔。 例如,2022 年 5 月 3 日下午 4:26 可以写作 2022-05-03 16:262022-05-03T16:26

对于其他日期时间格式,你需要使用 col_types 以及 col_date()col_datetime(),并指定日期时间格式。 readr 使用的日期时间格式是多种编程语言通用的标准,它用 % 后跟一个字符来描述日期组成部分。 例如,%Y-%m-%d 指定了一个日期格式为年、-、月(数字形式)、-、日。 表 Table 17.1 列出了所有选项。

Table 17.1: readr 能理解的所有日期格式
Type Code Meaning Example
Year %Y 4 digit year 2021
%y 2 digit year 21
Month %m Number 2
%b Abbreviated name Feb
%B Full name February
Day %d Two digits 02
%e One or two digits 2
Time %H 24-hour hour 13
%I 12-hour hour 1
%p AM/PM pm
%M Minutes 35
%S Seconds 45
%OS Seconds with decimal component 45.35
%Z Time zone name America/Chicago
%z Offset from UTC +0800
Other %. Skip one non-digit :
%* Skip any number of non-digits

这段代码显示了一些应用于一个非常模糊的日期的选项:

csv <- "
  date
  01/02/15
"

read_csv(csv, col_types = cols(date = col_date("%m/%d/%y")))
#> # A tibble: 1 × 1
#>   date      
#>   <date>    
#> 1 2015-01-02

read_csv(csv, col_types = cols(date = col_date("%d/%m/%y")))
#> # A tibble: 1 × 1
#>   date      
#>   <date>    
#> 1 2015-02-01

read_csv(csv, col_types = cols(date = col_date("%y/%m/%d")))
#> # A tibble: 1 × 1
#>   date      
#>   <date>    
#> 1 2001-02-15

请注意,无论你如何指定日期格式,一旦将其导入 R,它总是以相同的方式显示。

如果你使用 %b%B 并处理非英语日期,还需要提供 locale() 参数。 可查看 date_names_langs() 中内置的语言列表,或使用 date_names() 创建自定义语言设置。

17.2.2 From strings

日期时间规范语言功能强大,但需要仔细分析日期格式。 另一种方法是使用 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() 等创建 data。 要创建 date-time,请在解析函数的名称中添加下划线和一个或多个 “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"

你也可以通过提供时区来强制从 date 创建 date-time:

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

这里我使用了 UTC3 时区,你可能也知道它被称为 GMT(格林威治标准时间),即经度 0° 的时间4 。该时区不采用夏令时,这使得计算更为简便 。

17.2.3 From individual components

有时,你会遇到 date-time 的各个组成部分分散在多个列中,而不是单个字符串。 flights 数据集中的情况正是如此:

flights |> 
  select(year, month, day, hour, minute)
#> # A tibble: 336,776 × 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
#> # ℹ 336,770 more rows

要从这类输入创建 date/time,请对 dates 使用 make_date(),对 date-times 使用 make_datetime()

flights |> 
  select(year, month, day, hour, minute) |> 
  mutate(departure = make_datetime(year, month, day, hour, minute))
#> # A tibble: 336,776 × 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
#> # ℹ 336,770 more rows

让我们对 flights 中的四个时间列进行同样的处理。 时间的表示格式有些特殊,因此我们使用模运算来提取小时和分钟部分。 创建 date-time 变量后,我们将重点关注本章后续部分要探索的变量。

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 × 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
#> # ℹ 328,057 more rows
#> # ℹ 3 more variables: arr_time <dttm>, sched_arr_time <dttm>, …

利用这些数据,我们可以可视化一年中起飞时间的分布情况:

flights_dt |> 
  ggplot(aes(x = dep_time)) + 
  geom_freqpoly(binwidth = 86400) # 86400 seconds = 1 day

A frequency polyon with departure time (Jan-Dec 2013) on the x-axis and number of flights on the y-axis (0-1000). The frequency polygon is binned by day so you see a time series of flights by day. The pattern is dominated by a weekly pattern; there are fewer flights  on weekends. The are few days that stand out as having a surprisingly few flights in early February, early July, late November, and late December.

或者在一天之内:

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

A frequency polygon with departure time (6am - midnight Jan 1) on the x-axis, number of flights on the y-axis (0-17), binned into 10 minute increments. It's hard to see much pattern because of high variability, but most bins have 8-12 flights, and there are markedly fewer flights  before 6am and after 8pm.

请注意,当您在数字上下文中(如直方图中)使用 date-times 时,1 表示 1 秒,因此 binwidth 为 86400 表示一天。 对于 dates,1 表示 1 天。

17.2.4 From other types

你可能需要在 date-time 和 date 之间进行转换。 这可以通过 as_datetime()as_date() 函数来实现:

as_datetime(today())
#> [1] "2026-01-22 UTC"
as_date(now())
#> [1] "2026-01-22"

有时你会遇到以数字偏移量形式表示的 date/times,其基准是 “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"

17.2.5 Exercises

  1. 如果解析包含无效日期的字符串会发生什么?

    ymd(c("2010-10-10", "bananas"))
  2. today() 函数中的 tzone 参数有什么作用? 为什么它很重要?

  3. 针对以下每个日期时间,请分别说明如何使用 readr 的列规范解析以及 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
    t1 <- "1705"
    t2 <- "11:15:10.12 PM"

17.3 Date-time components

现在你已经知道如何将 date-time 数据导入 R 的 date-time 数据结构,接下来让我们探索可以对它们进行哪些操作。 本节将重点介绍用于获取和设置单个组件的访问器函数。 下一节将探讨 date-times 的算术运算。

17.3.1 Getting components

你可以使用访问器函数 year(), month(), mday() (day of the month), yday() (day of the year), wday() (day of the week), hour(), minute(), 和 second() 来提取日期的各个组成部分。 这些函数实际上是 make_datetime() 的反向操作。

datetime <- ymd_hms("2026-07-08 12:34:56")

year(datetime)
#> [1] 2026
month(datetime)
#> [1] 7
mday(datetime)
#> [1] 8

yday(datetime)
#> [1] 189
wday(datetime)
#> [1] 4

对于 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] Wednesday
#> 7 Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < ... < Saturday

我们可以使用 wday() 来观察工作日出发的航班数量多于周末:

flights_dt |> 
  mutate(wday = wday(dep_time, label = TRUE)) |> 
  ggplot(aes(x = wday)) +
  geom_bar()

A bar chart with days of the week on the x-axis and number of  flights on the y-axis. Monday-Friday have roughly the same number of flights, ~48,0000, decreasingly slightly over the course of the week. Sunday is a little lower (~45,000), and Saturday is much lower  (~38,000).

我们还可以观察每小时之内分钟粒度的平均起飞延误情况。 这里有一个有趣的规律:在 20-30 分钟和 50-60 分钟起飞的航班,其延误程度远低于该小时内其他时段的航班!

flights_dt |> 
  mutate(minute = minute(dep_time)) |> 
  group_by(minute) |> 
  summarize(
    avg_delay = mean(dep_delay, na.rm = TRUE),
    n = n()
  ) |> 
  ggplot(aes(x = minute, y = avg_delay)) +
  geom_line()

A line chart with minute of actual departure (0-60) on the x-axis and average delay (4-20) on the y-axis. Average delay starts at (0, 12), steadily increases to (18, 20), then sharply drops, hitting at minimum at ~23 minute past the hour and 9 minutes of delay. It then increases again to (17, 35), and sharply decreases to (55, 4). It finishes off with an increase to (60, 9).

有趣的是,如果我们查看计划起飞时间,并没有观察到如此明显的规律:

sched_dep <- flights_dt |> 
  mutate(minute = minute(sched_dep_time)) |> 
  group_by(minute) |> 
  summarize(
    avg_delay = mean(arr_delay, na.rm = TRUE),
    n = n()
  )

ggplot(sched_dep, aes(x = minute, y = avg_delay)) +
  geom_line()

A line chart with minute of scheduled departure (0-60) on the x-axis and average delay (4-16). There is relatively little pattern, just a small suggestion that the average delay decreases from maybe 10 minutes to 8 minutes over the course of the hour.

那么,为什么在实际起飞时间中会出现这种规律呢? 事实上,如同许多由人工收集的数据一样,航班倾向于在“整点”时间起飞,如 Figure 17.1 所示,这导致了明显的偏差。 在处理涉及人为判断的数据时,务必警惕这类规律的存在!

A line plot with departure minute (0-60) on the x-axis and number of flights (0-60000) on the y-axis. Most flights are scheduled to depart on either the hour (~60,000) or the half hour (~35,000). Otherwise, all most all flights are scheduled to depart on multiples of five,  with a few extra at 15, 45, and 55 minutes.
Figure 17.1: A frequency polygon showing the number of flights scheduled to depart each hour. You can see a strong preference for round numbers like 0 and 30 and generally for numbers that are a multiple of five.

17.3.2 Rounding

另一种绘制单个日期组成部分的方法是使用 floor_date()round_date()ceiling_date() 函数,将日期四舍五入到附近的时间单位。 每个函数都接受一个待调整的日期向量,以及要向下取整(floor)、向上取整(ceiling)或四舍五入(round)的时间单位名称。 例如,这使我们能够绘制每周的航班数量:

flights_dt |> 
  count(week = floor_date(dep_time, "week")) |> 
  ggplot(aes(x = week, y = n)) +
  geom_line() + 
  geom_point()

A line plot with week (Jan-Dec 2013) on the x-axis and number of flights (2,000-7,000) on the y-axis. The pattern is fairly flat from February to November with around 7,000 flights per week. There are far fewer flights on the first (approximately 4,500 flights) and last weeks of the year (approximately 2,500 flights).

通过计算 dep_time 与当天最早时刻之间的差值,你可以使用四舍五入来展示航班在一天内的分布情况:

flights_dt |> 
  mutate(dep_hour = dep_time - floor_date(dep_time, "day")) |> 
  ggplot(aes(x = dep_hour)) +
  geom_freqpoly(binwidth = 60 * 30)
#> Don't know how to automatically pick scale for object of type <difftime>.
#> Defaulting to continuous.

A line plot with depature time on the x-axis. This is units of seconds since midnight so it's hard to interpret.

计算两个日期时间之间的差值会得到一个 difftime 对象(更多内容见 Section 17.4.3 节)。 我们可以将其转换为 hms 对象,以获得更有用的 x 轴:

flights_dt |> 
  mutate(dep_hour = hms::as_hms(dep_time - floor_date(dep_time, "day"))) |> 
  ggplot(aes(x = dep_hour)) +
  geom_freqpoly(binwidth = 60 * 30)

A line plot with depature time (midnight to midnight) on the x-axis and number of flights on the y-axis (0 to 15,000). There are very few (<100) flights before 5am. The number of flights then rises rapidly  to 12,000 / hour, peaking at 15,000 at 9am, before falling to around 8,000 / hour for 10am to 2pm. Number of flights then increases to around 12,000 per hour until 8pm, when they rapidly drop again.

17.3.3 Modifying components

你也可以使用每个访问器函数来修改 date/time 的组成部分。 这在数据分析中不常用,但在清理日期明显错误的数据时可能很有用。

(datetime <- ymd_hms("2026-07-08 12:34:56"))
#> [1] "2026-07-08 12:34:56 UTC"

year(datetime) <- 2030
datetime
#> [1] "2030-07-08 12:34:56 UTC"
month(datetime) <- 01
datetime
#> [1] "2030-01-08 12:34:56 UTC"
hour(datetime) <- hour(datetime) + 1
datetime
#> [1] "2030-01-08 13:34:56 UTC"

或者,你无需修改现有变量,而是可以使用 update() 创建一个新的 date-time。 这还允许你一步设置多个值:

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

如果设置的值过大,它们会自动向前进位:

update(ymd("2023-02-01"), mday = 30)
#> [1] "2023-03-02"
update(ymd("2023-02-01"), hour = 400)
#> [1] "2023-02-17 16:00:00 UTC"

17.3.4 Exercises

  1. 一天内航班时间的分布如何随一年中的时间变化?

  2. 比较 dep_time, sched_dep_timedep_delay。 它们是否一致? 请解释你的发现。

  3. 比较 air_time 与起飞和到达之间的时间间隔。 解释你的发现。 (提示:考虑机场的位置。)

  4. 一天中平均延误时间如何变化? 应该使用 dep_time 还是 sched_dep_time? 为什么?

  5. 如果希望最小化延误的可能性,你应该在一周中的哪一天出发?

  6. diamonds$caratflights$sched_dep_time 的分布有何相似之处?

  7. 验证我们的假设:20-30 分钟和 50-60 分钟起飞的航班较早出发是由于计划航班提前起飞所致。 提示:创建一个二元变量来指示航班是否延误。

17.4 Time spans

接下来,你将学习日期算术运算的原理,包括减法、加法和除法。 在此过程中,你会了解到三种表示时间跨度的重要类别:

  • Durations, 表示精确的秒数。
  • Periods, 表示人类可理解的单位,如周 (weeks) 和月 (months)。
  • Intervals, 表示一个起始点和结束点。

如何在 duration、periods 和 intervals 之间做出选择? 一如既往,选择能解决你问题的最简单数据结构。 如果你只关心物理时间,使用 duration;如果需要添加人类可理解的时间,使用 period;如果需要计算某个跨度在人类单位中的长度,使用 interval。

17.4.1 Durations

在 R 中,当你对两个日期进行相减时,会得到一个 difftime 对象:

# How old is Hadley?
h_age <- today() - ymd("1979-10-14")
h_age
#> Time difference of 16902 days

difftime 类对象以秒、分钟、小时、天或周为单位记录时间跨度。 这种模糊性可能使得 difftimes 对象处理起来有些棘手,因此 lubridate 提供了另一种始终以秒为单位的时间跨度表示方式:duration

as.duration(h_age)
#> [1] "1460332800s (~46.28 years)"

Durations 附带了一系列便捷的构造函数:

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

Durations 始终以秒为单位记录时间跨度。 更大的单位通过将分钟、小时、天、周和年转换为秒来创建:一分钟 60 秒,一小时 60 分钟,一天 24 小时,一周 7 天。 更大的时间单位则更具问题性。 一年使用“平均”年天数,即 365.25 天。 由于月份的变化太大,无法将其转换为 duration。

你可以对 durations 进行加减和乘法运算:

2 * dyears(1)
#> [1] "63115200s (~2 years)"
dyears(1) + dweeks(12) + dhours(15)
#> [1] "38869200s (~1.23 years)"

你可以对日期进行 durations 的加减运算:

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

然而,由于 durations 表示的是精确的秒数,有时你可能会得到意想不到的结果:

one_am <- ymd_hms("2026-03-08 01:00:00", tz = "America/New_York")

one_am
#> [1] "2026-03-08 01:00:00 EST"
one_am + ddays(1)
#> [1] "2026-03-09 02:00:00 EDT"

为什么 3 月 8 日凌晨 1 点加上一天后是 3 月 9 日凌晨 2 点? 如果仔细观察日期,你可能还会注意到时区发生了变化。 3 月 8 日只有 23 小时,因为这是夏令时开始的时间,所以如果我们加上一整天的秒数,最终会得到一个不同的时间。

17.4.2 Periods

为了解决这个问题,lubridate 提供了 periods。 Periods 表示时间跨度,但其长度不以固定的秒数为单位,而是以“人类”时间(如 days 和 months)为单位。 这使得它们能以更直观的方式工作:

one_am
#> [1] "2026-03-08 01:00:00 EST"
one_am + days(1)
#> [1] "2026-03-09 01:00:00 EDT"

与 durations 类似,periods 也可以通过一系列便捷的构造函数来创建。

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"

你可以对 periods 进行加法和乘法运算:

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

当然,也可以将它们与 dates 相加。 与 durations 相比,periods 更可能符合你的预期:

# A leap year
ymd("2024-01-01") + dyears(1)
#> [1] "2024-12-31 06:00:00 UTC"
ymd("2024-01-01") + years(1)
#> [1] "2025-01-01"

# Daylight saving time
one_am + ddays(1)
#> [1] "2026-03-09 02:00:00 EDT"
one_am + days(1)
#> [1] "2026-03-09 01:00:00 EDT"

让我们使用 periods 来解决与航班日期相关的一个异常情况。 一些飞机似乎在其从纽约市起飞之前就已到达目的地。

flights_dt |> 
  filter(arr_time < dep_time) 
#> # A tibble: 10,633 × 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
#> # ℹ 10,627 more rows
#> # ℹ 3 more variables: arr_time <dttm>, sched_arr_time <dttm>, …

这些是过夜航班。 我们在起飞和到达时间中使用了相同的日期信息,但这些航班实际上是在第二天抵达的。 我们可以通过为每个过夜航班的到达时间加上 days(1) 来修正这个问题。

flights_dt <- flights_dt |> 
  mutate(
    overnight = arr_time < dep_time,
    arr_time = arr_time + days(overnight),
    sched_arr_time = sched_arr_time + days(overnight)
  )

现在,所有航班都符合物理定律了。

flights_dt |> 
  filter(arr_time < dep_time) 
#> # A tibble: 0 × 10
#> # ℹ 10 variables: origin <chr>, dest <chr>, dep_delay <dbl>,
#> #   arr_delay <dbl>, dep_time <dttm>, sched_dep_time <dttm>, …

17.4.3 Intervals

dyears(1) / ddays(365) 会返回什么结果? 它并不完全等于 1,因为 dyears() 被定义为每个平均年份的秒数,即 365.25 天。

那么 years(1) / days(1) 会返回什么? 如果年份是 2015,它应该返回 365;但如果是 2016,它应该返回 366! lubridate 没有足够的信息来给出一个唯一明确的答案。 它实际做的是给出一个估算值:

years(1) / days(1)
#> [1] 365.25

如果你想要更精确的测量,就必须使用 interval。 interval 是一对起始和结束的日期时间,或者你可以将其视为带有起始点的时长。

你可以通过 start %--% end 的语法创建一个区间:

y2023 <- ymd("2023-01-01") %--% ymd("2024-01-01")
y2024 <- ymd("2024-01-01") %--% ymd("2025-01-01")

y2023
#> [1] 2023-01-01 UTC--2024-01-01 UTC
y2024
#> [1] 2024-01-01 UTC--2025-01-01 UTC

然后你可以将其除以 days() 来找出该年份包含多少天:

y2023 / days(1)
#> [1] 365
y2024 / days(1)
#> [1] 366

17.4.4 Exercises

  1. 向刚学习 R 的人解释 days(!overnight)days(overnight)。 你需要了解的关键事实是什么?

  2. 创建一个日期向量,表示 2015 年每个月的第一天。 再创建一个日期向量,表示当前年份每个月的第一天。

  3. 编写一个函数,根据你的生日(作为日期输入),返回你的年龄(以年为单位)。

  4. 为什么 (today() %--% (today() + years(1))) / months(1) 无法运行?

17.5 Time zones

时区是一个极其复杂的主题,因为它们与地缘政治实体相互作用。 幸运的是,我们不需要深入探究所有细节,因为并非所有细节对数据分析都至关重要,但有一些挑战我们确实需要直面应对。

第一个挑战是日常使用的时区名称往往具有歧义性。 例如,如果你是美国人,可能很熟悉 EST(东部标准时间)。 然而,澳大利亚和加拿大也都有 EST! 为了避免混淆,R 采用国际标准的 IANA 时区。 这些时区使用一致的命名规则 {area}/{location},通常形式为 {continent}/{city}{ocean}/{city}。 例如:“America/New_York”、“Europe/Paris” 和 “Pacific/Auckland”。

你可能会好奇,为什么时区使用城市名称,而通常我们认为时区与国家或国家内的区域相关联。 这是因为 IANA 数据库必须记录数十年的时区规则。 在几十年间,国家名称变更(或分裂)相当频繁,但城市名称往往保持不变。 另一个问题是,名称不仅需要反映当前行为,还需体现完整的历史。 例如,同时存在 “America/New_York” 和 “America/Detroit” 两个时区。 这两个城市目前都使用东部标准时间,但在 1969-1972 年间,密歇根州(底特律所在州)未遵循夏令时,因此需要一个不同的名称。 值得一读原始时区数据库(可在 https://www.iana.org/time-zones 获取),仅是为了了解其中的一些历史故事!

你可以通过 Sys.timezone() 查看 R 认为你当前的时区是什么:

Sys.timezone()
#> [1] "UTC"

(如果 R 无法识别,则会返回 NA。)

可以通过 OlsonNames() 查看完整的时区名称列表:

length(OlsonNames())
#> [1] 598
head(OlsonNames())
#> [1] "Africa/Abidjan"     "Africa/Accra"       "Africa/Addis_Ababa"
#> [4] "Africa/Algiers"     "Africa/Asmara"      "Africa/Asmera"

在 R 中,时区是 date-time 的一个属性,仅控制显示方式。 例如,以下三个对象表示同一时刻:

x1 <- ymd_hms("2024-06-01 12:00:00", tz = "America/New_York")
x1
#> [1] "2024-06-01 12:00:00 EDT"

x2 <- ymd_hms("2024-06-01 18:00:00", tz = "Europe/Copenhagen")
x2
#> [1] "2024-06-01 18:00:00 CEST"

x3 <- ymd_hms("2024-06-02 04:00:00", tz = "Pacific/Auckland")
x3
#> [1] "2024-06-02 04:00:00 NZST"

你可以通过减法运算验证它们是同一时间:

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

除非另有说明,lubridate 始终使用 UTC。 UTC(协调世界时)是科学界使用的标准时区,大致等同于 GMT(格林威治标准时间)。 它不采用夏令时,因此便于计算。 组合 date-times 的操作(如 c())通常会丢弃时区信息。 这种情况下,date-times 将按第一个元素的时区显示:

x4 <- c(x1, x2, x3)
x4
#> [1] "2024-06-01 12:00:00 EDT" "2024-06-01 12:00:00 EDT"
#> [3] "2024-06-01 12:00:00 EDT"

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

  • 保持时间点不变,仅改变其显示方式。 当时间点正确但希望以更自然的方式显示时使用此方法。

    x4a <- with_tz(x4, tzone = "Australia/Lord_Howe")
    x4a
    #> [1] "2024-06-02 02:30:00 +1030" "2024-06-02 02:30:00 +1030"
    #> [3] "2024-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] "2024-06-01 12:00:00 +1030" "2024-06-01 12:00:00 +1030"
    #> [3] "2024-06-01 12:00:00 +1030"
    x4b - x4
    #> Time differences in hours
    #> [1] -14.5 -14.5 -14.5

17.6 Summary

本章向你介绍了 lubridate 提供的工具,帮助你处理日期时间数据。 处理日期和时间看似比实际需要的更复杂,但希望本章能让你理解其原因——日期时间比乍看之下更为复杂,而处理各种可能的情况增加了其复杂性。 即使你的数据从未涉及夏令时变更或闰年,相关函数也必须能够应对这些情况。

下一章将系统总结缺失值的处理方法。 你已经在多个场景中遇到过缺失值,在自己的分析中也无疑会碰到它们。现 在,是时候提供一个实用技巧合集来应对这些情况了。


  1. 如果能被 4 整除,那就是闰年,除非它能被 100 整除,除非它能被 400 整除。 换句话说,在每一组 400 年里,有 97 个闰年。↩︎

  2. https://xkcd.com/1179/↩︎

  3. 你可能会好奇 UTC 代表什么。 它是英文 “Coordinated Universal Time” 与法文 “Temps Universel Coordonné” 之间的折中简称。↩︎

  4. 无需猜测是哪个国家提出了这个经度系统。↩︎