日期时间处理包含两个彼此关联的问题:数据应如何表示,以及 R 应如何解析和运算。ISO 8601 提供统一的表示方式,lubridate 则提供一组直观的日期时间工具。
ISO 8601 日期时间
ISO 8601 使用从大到小的顺序表示日期和时间,可以避免 04/05/2025 究竟是 4 月 5 日还是 5 月 4 日的歧义。
| 类型 | 示例 | 含义 |
|---|---|---|
| 日期 | 2025-04-25 |
年、月、日 |
| 本地日期时间 | 2025-04-25T14:30:00 |
T 分隔日期与时间 |
| UTC 时间戳 | 2025-04-25T14:30:00Z |
Z 表示 UTC |
| 带偏移的时间戳 | 2025-04-25T14:30:00+08:00 |
时间相对 UTC 的偏移量 |
存储和交换数据时,应明确保留时区信息。缺少 Z 或偏移量的日期时间只表达墙上时间,无法独立确定全球时间线上的具体时刻。
R 中的两种基本类型
只包含年月日的信息使用 Date;需要时、分、秒及时区的信息使用 POSIXct。
library(lubridate)
date <- ymd("2025-04-25")
datetime <- ymd_hms("2025-04-25T14:30:00Z", tz = "UTC")
class(date)
# [1] "Date"
class(datetime)
# [1] "POSIXct" "POSIXt"
生日、采样日期和自然日通常用 Date;日志时间、事件发生时刻和跨时区数据通常用 POSIXct。
解析已有文本
lubridate 的解析函数名直接表达年月日的排列顺序:
ymd("2024-04-25")
mdy("04/25/2024")
dmy("25-04-2024")
ymd_hms("2024-04-25 18:00:00")
parse_date_time("25-Apr-2024", orders = "d-b-Y")
当输入是 ISO 8601 时间戳时,显式提供时区:
ymd_hms("2025-04-25T14:30:00Z", tz = "UTC")
readr 也可以按照格式字符串解析日期时间:
library(readr)
parse_date("2025-04-25", format = "%Y-%m-%d")
parse_datetime("2025-04-25T14:30:00+08:00")
常用格式代码如下:
| 代码 | 含义 | 示例 |
|---|---|---|
%Y / %y |
四位年 / 两位年 | 2025 / 25 |
%m |
月 | 04 |
%b / %B |
月份英文缩写 / 全称 | Apr / April |
%d |
日 | 25 |
%H / %I |
24 小时制 / 12 小时制小时 | 14 / 02 |
%p |
上午或下午 | PM |
%M |
分钟 | 30 |
%S / %OS |
秒 / 带小数的秒 | 45 / 45.35 |
%Z |
时区名称 | UTC |
%z |
UTC 偏移 | +0800 |
从分列字段构造日期
数据库导出、传感器日志和整理后的数据框经常将年、月、日、小时等存成独立列。make_date() 用这些字段构造 Date:
make_date(2024, 4, 25)
# [1] "2024-04-25"
make_datetime() 构造 POSIXct。未提供的小时、分钟和秒默认为零:
make_datetime(2024, 4, 25, 14, 30, tz = "UTC")
# [1] "2024-04-25 14:30:00 UTC"
在数据框中,可以一次处理整列:
library(dplyr)
library(lubridate)
df <- tibble(
year = c(2024, 2024),
month = c(4, 5),
day = c(25, 10),
hour = c(14, 9),
minute = c(30, 15)
)
df <- df |>
mutate(
date = make_date(year, month, day),
datetime = make_datetime(
year, month, day, hour, minute, tz = "UTC"
)
)
只关心自然日时使用 make_date();只有确实需要一天内的时刻时才构造日期时间。
获取当前日期与时间
today()
now()
today() 返回 Date,now() 返回带时区的当前日期时间。需要可重复的分析时,不要让关键结果隐式依赖当前时间,应将分析日期作为明确输入保存下来。
提取时间组件
x <- ymd_hms("2024-04-25 18:30:45", tz = "UTC")
year(x)
month(x)
day(x)
hour(x)
minute(x)
second(x)
wday(x)
yday(x)
week(x)
quarter(x)
month(x, label = TRUE) 和 wday(x, label = TRUE) 可以返回月份或星期标签。提取后的组件常用于分组汇总和绘图,但原始时间列仍应保留。
日期时间运算
days()、hours() 和 minutes() 等函数使固定长度的时间偏移更容易阅读:
ymd("2024-01-01") + days(3)
now() - hours(2) + minutes(30)
月份和年份并非固定秒数。涉及月末、闰年或日历规则时,应先明确想表达的是日历周期还是精确持续时间。
对齐到统计周期
按日、周或月聚合前,可以先将时间对齐:
floor_date(x, "month")
ceiling_date(x, "week")
round_date(x, "day")
floor_date()向当前周期的起点取整;ceiling_date()向下一个周期边界取整;round_date()取最近的周期边界。
例如按月汇总:
events |>
mutate(month = floor_date(event_time, "month")) |>
count(month)
计算时间差与区间
start <- ymd_hms("2024-04-25 08:00:00", tz = "UTC")
end <- ymd_hms("2024-04-26 14:30:00", tz = "UTC")
difftime(end, start, units = "hours")
interval(start, end)
duration(end - start)
difftime() 适合直接取得指定单位的差值;interval() 保留起点和终点,适合判断包含关系和处理日历运算;duration() 表达以秒为基础的精确时长。
时区与夏令时
时区不是显示格式,而是日期时间含义的一部分。跨系统存储时,可以使用 UTC;展示给用户时,再转换到需要的本地时区。
本地时区还可能受夏令时影响:某些墙上时间会重复出现或根本不存在。因此日志和事件数据应在进入分析流程时明确时区,不能只在导出结果时补一个时区标签。
把日期表示、解析顺序、时区和数据类型在入口处确定下来,后续的分组、连接和时间差计算才会保持一致。