9 R日期时间

9.1 R日期和日期时间类型

R日期可以保存为Date类型, 一般用整数保存,数值为从1970-1-1经过的天数。

R中用一种叫做POSIXct和POSIXlt的特殊数据类型保存日期和时间, 可以仅包含日期部分,也可以同时有日期和时间。 技术上,POSIXct把日期时间保存为从1970年1月1日零时到该日期时间的时间间隔秒数, 所以数据框中需要保存日期时用POSIXct比较合适, 需要显示时再转换成字符串形式; POSIXlt把日期时间保存为一个包含年、月、日、星期、时、分、秒等成分的列表, 所以求这些成分可以从POSIXlt格式日期的列表变量中获得。 日期时间会涉及到所在时区、夏时制等问题, 比较复杂。

为了获得专用的时间类型, 可以使用hms扩展包。

基础的R用as.Date()as.POSIXct()等函数生成日期型和日期时间型, R扩展包lubridate提供了多个方便函数, 可以更容易地生成、转换、管理日期型和日期时间型数据。

library(lubridate)

9.2 从字符串生成日期数据

函数lubridate::today()返回当前日期:

today()
## [1] "2023-07-27"

函数lubridate::now()返回当前日期时间:

now()
## [1] "2023-07-27 12:04:33 CST"

结果显示中出现的CST是时区, 这里使用了操作系统提供的当前时区。 CST不是一个含义清晰的时区, 在不同国家对应不同的时区, 在中国代表中国标准时间(北京时间)。

lubridate::ymd(), lubridate::mdy(), lubridate::dmy()将字符型向量转换为日期型向量,如:

ymd(c("1998-3-10", "2018-01-17", "18-1-17"))
## [1] "1998-03-10" "2018-01-17" "2018-01-17"
mdy(c("3-10-1998", "01-17-2018"))
## [1] "1998-03-10" "2018-01-17"
dmy(c("10-3-1998", "17-01-2018"))
## [1] "1998-03-10" "2018-01-17"

在年号只有两位数字时,默认对应到1969-2068范围。

lubridate包的ymdmdydmy等函数添加hmshmh等后缀, 可以用于将字符串转换成日期时间。 如

ymd_hms("1998-03-16 13:15:45")
## [1] "1998-03-16 13:15:45 UTC"

9.3 时区和时区转换

上面例子结果显示中UTC是时区, UTC是协调世界时(Universal Time Coordinated)英文缩写, 是由国际无线电咨询委员会规定和推荐, 并由国际时间局(BIH)负责保持的以秒为基础的时间标度。 UTC相当于本初子午线(即经度0度)上的平均太阳时, 过去曾用格林威治平均时(GMT)来表示。 北京时间比UTC时间早8小时, 以1999年1月1日0000UTC为例, UTC时间是零点, 北京时间为1999年1月1日早上8点整。 日期时间字符串中日期部分和时间部分以空格分隔, 也可以用字母T分隔。

Date()as.DateTime()ymd()等函数中, 可以用tz=指定时区, 比如北京时间可指定为tz="Etc/GMT-8"tz="Asia/Shanghai"

为了将某个时间转换到指定的时区, 而不改变真正的时间, 用with_tz()函数,如:

with_tz(ymd_hms(c("1998-03-16 13:15:45", 
  "2023-03-14 10:11:12"),
  tz="Asia/Shanghai"), tzone="UTC")
## [1] "1998-03-16 05:15:45 UTC" "2023-03-14 02:11:12 UTC"

为了保持表面的时间(时钟显示的日期时间)不变, 但将真正的时间修改到另外的时区, 用force_tz()force_tzs(), 其中force_tzs()可以将每个时间单独应用不同的时区。 如:

force_tz(ymd_hms(c("1998-03-16 13:15:45", 
  "2023-03-14 10:11:12"),
  tz="Asia/Shanghai"), tzone="UTC")
## [1] "1998-03-16 13:15:45 UTC" "2023-03-14 10:11:12 UTC"

上例的结果显示中,仅时区改变了, 显示的日期、时间都没有变, 但这样就改变了真实的时间。 输入的日期中的时区"Asia/Shanghai"不起作用, 仅该时区的钟面时间起作用。

又如:

force_tzs(ymd_hms(c("1998-03-16 13:15:45", 
  "2023-03-14 10:11:12"),
  tz="Asia/Shanghai"), 
  tzones=c("Etc/GMT-6", "Etc/GMT-10"))
## [1] "1998-03-16 07:15:45 UTC" "2023-03-14 00:11:12 UTC"

这将北京时间"1998-03-16 13:15:45"改成了东6区时间, 对应到UTC就是钟面时间减6, 所以13点变成7点, 将北京时间"2023-03-14 10:11:12"改成了东10区时间, 对应到UTC就是钟面时间减10,所以10点变成0点。 输入的日期中的时区"Asia/Shanghai"不起作用, 仅该时区的钟面时间起作用。

9.4 从数值生成日期数据

lubridate::make_date(year, month, day)可以从三个数值构成日期向量。 如

make_date(1998, 3, 10)
## [1] "1998-03-10"

lubridate::make_datetime(year, month, day, hour, min, sec) 可以从最多六个数值组成日期时间, 其中时分秒缺省值都是0。 如

make_datetime(1998, 3, 16, 13, 15, 45.2)
## [1] "1998-03-16 13:15:45 UTC"

make_date()make_datetime()都可以用tz选项指定时区, 但仅支持单个时区。 如果多个时间的时区也不同, 应该使用force_tzs()函数。

9.5 日期和日期时间之间的转换

lubridate::as_date()可以将日期时间型转换为日期型,如

as_date(as.POSIXct("1998-03-16 13:15:45"))
## [1] "1998-03-16"

lubridate::as_datetime()可以将日期型数据转换为日期时间型,如

as.Date("1998-03-16") |>
  as_datetime() |> 
  class()
## [1] "POSIXct" "POSIXt"

9.6 日期显示格式

as.character()函数把日期型数据转换为字符型, 如

x <- as.POSIXct(c('1998-03-16', '2015-11-22'))
as.character(x)
## [1] "1998-03-16" "2015-11-22"

as.character()中可以用format选项指定显示格式,如

as.character(x, format='%m/%d/%Y')
## [1] "03/16/1998" "11/22/2015"

格式中“%Y”代表四位的公元年号, “%m”代表两位的月份数字, “%d”代表两位的月内日期号。

这些格式缩写也被用在从字符串转换在日期时, 比如readr::read_csv在指定某一列为col_date()时, 可以指定格式如format="%m/%d/%Y"。 又如:

as.Date(c("12/6/2022", "1/1/2023"), format="%m/%d/%Y")
## [1] "2022-12-06" "2023-01-01"

"15Mar98"这样的日期在英文环境中比较常见, 但是在R中的处理比较复杂。 在下面的例子中,R日期被转换成了类似"Mar98"这样的格式, 在format选项中用了“%b”代表三英文字母月份缩写, 但是因为月份缩写依赖于操作系统默认语言环境, 需要用Sys.setlocale()函数设置语言环境为"C"。示例程序如下

x <- as.POSIXct(c('1998-03-16', '2015-11-22'))
old.lctime <- Sys.getlocale('LC_TIME')
Sys.setlocale('LC_TIME', 'C')
## [1] "C"
as.character(x, format='%b%y')
## [1] "Mar98" "Nov15"
Sys.setlocale('LC_TIME', old.lctime)
## [1] "Chinese (Simplified)_China.utf8"

format选项中的“%y”表示两位数的年份, 应尽量避免使用两位数年份以避免混淆。

包含时间的转换如

x <- as.POSIXct('1998-03-16 13:15:45')
as.character(x)
## [1] "1998-03-16 13:15:45"
as.character(x, format='%H:%M:%S')
## [1] "13:15:45"

这里“%H”代表小时(按24小时制), “%M”代表两位的分钟数字, “%S”代表两位的秒数。

9.7 访问日期时间的组成值

lubridate包的如下函数可以取出日期型或日期时间型数据中的组成部分:

  • year()取出年
  • month()取出月份数值
  • mday()取出日数值
  • yday()取出日期在一年中的序号,元旦为1
  • wday()取出日期在一个星期内的序号, 但是一个星期从星期天开始, 星期天为1,星期一为2,星期六为7。
  • hour()取出小时
  • minute()取出分钟
  • second()取出秒

比如, 2018-1-17是星期三, 则

month(as.POSIXct("2018-1-17 13:15:40"))
## [1] 1
mday(as.POSIXct("2018-1-17 13:15:40"))
## [1] 17
wday(as.POSIXct("2018-1-17 13:15:40"))
## [1] 4

lubridate的这些成分函数还允许被赋值, 结果就修改了相应元素的值,如

x <- as.POSIXct("2018-1-17 13:15:40")
year(x) <- 2000
month(x) <- 1
mday(x) <- 1
x
## [1] "2000-01-01 13:15:40 CST"

update()可以对一个日期或一个日期型向量统一修改其组成部分的值, 如

x <- as.POSIXct("2018-1-17 13:15:40")
y <- update(x, year=2000)
y
## [1] "2000-01-17 13:15:40 CST"

update()函数中可以用year, month, mday, hour, minute, second等参数修改日期的组成部分。

用lubridate包的功能计算周岁如下:

age.int <- function(birth, now){
  age <- year(now) - year(birth)
  sele <- (month(now) * 100 + mday(now)
              < month(birth) * 100 + mday(birth))
  ## sele 是那些没有到生日的人
  age[sele] <- age[sele] - 1

  age
}

9.8 日期舍入计算

lubridate包提供了floor_date(), round_date(), ceiling_date()等函数, 对日期可以用unit=指定一个时间单位进行舍入。 时间单位为字符串, 如seconds, 5 seconds, minutes, 2 minutes, hours, days, weeks, months, years等。

比如,以10 minutes为单位, floor_date()将时间向前归一化到10分钟的整数倍, ceiling_date()将时间向后归一化到10分钟的整数倍, round_date()将时间归一化到最近的10分钟的整数倍, 时间恰好是5分钟倍数时按照类似四舍五入的原则向上取整。 例如

x <- ymd_hms("2018-01-11 08:32:44")
floor_date(x, unit="10 minutes")
## [1] "2018-01-11 08:30:00 UTC"
ceiling_date(x, unit="10 minutes")
## [1] "2018-01-11 08:40:00 UTC"
round_date(x, unit="10 minutes")
## [1] "2018-01-11 08:30:00 UTC"

如果单位是星期, 会涉及到一个星期周期的开始是星期日还是星期一的问题。 用参数week_start=7指定开始是星期日, week_start=1指定开始是星期一。

9.9 日期计算

在lubridate的支持下日期可以相减, 可以进行加法、除法。 lubridate包提供了如下的三种与时间长短有关的数据类型:

  • 时间长度(duration),按整秒计算;
  • 时间周期(period),如日、周;
  • 时间区间(interval),包括一个开始时间和一个结束时间。

9.9.1 时间长度

R的POSIXct日期时间之间可以相减,如

d1 <- ymd_hms("2000-01-01 0:0:0")
d2 <- ymd_hms("2000-01-02 12:0:5")
di <- d2 - d1; di
## Time difference of 1.500058 days

结果显示与日期之间差别大小有关系, 结果是类型是difftime。 为了转换成数值,比如秒数, 可以用as.double()units="secs", 如:

as.double(di, units="days")
## [1] 1.500058
as.double(di, units="hours")
## [1] 36.00139
as.double(di, units="mins")
## [1] 2160.083
as.double(di, units="secs")
## [1] 129605

lubridate包提供了duration类型, 固定以秒作为基本单位, 所以处理更方便:

as.duration(di)
## [1] "129605s (~1.5 days)"

lubridate的dseconds(), dminutes(), dhours(), ddays(), dweeks(), dyears()函数可以直接生成时间长度类型的数据,如

dhours(1)
## [1] "3600s (~1 hours)"

lubridate的时间长度类型总是以秒作为单位, 可以在时间长度之间相加, 也可以对时间长度乘以无量纲数,如

dhours(1) + dseconds(5)
## [1] "3605s (~1 hours)"
dhours(1)*10
## [1] "36000s (~10 hours)"

可以给一个日期加或者减去一个时间长度, 结果严格按推移的秒数计算, 如

d2 <- ymd_hms("2000-01-02 12:0:5")
d2 - dhours(5)
## [1] "2000-01-02 07:00:05 UTC"
d2 + ddays(10)
## [1] "2000-01-12 12:00:05 UTC"

时间的前后推移在涉及到夏时制时有可能出现难以预料到的情况。

9.9.2 时间周期

时间长度的固定单位是秒, 但是像月、年这样的单位, 因为可能有不同的天数, 所以日历中的时间单位往往没有固定的时长。

lubridate包的seconds(), minutes(), hours(), days()weeks(), years()函数可以生成以日历中正常的周期为单位的时间长度, 不需要与秒数相联系, 可以用于时间的前后推移。 这些时间周期的结果可以相加、乘以无量纲整数:

years(2) + 10*days(1)
## [1] "2y 0m 10d 0H 0M 0S"

lubridate的月度周期因为与已有函数名冲突, 所以没有提供, 需要使用lubridate::period(num, units="month")的格式, 其中num是几个月的数值。

为了按照日历进行日期的前后平移, 而不是按照秒数进行日期的前后平移, 应该使用这些时间周期。

例如,因为2016年是闰年, 按秒数给2016-01-01加一年,得到的并不是2017-01-01:

ymd("2016-01-01") + dyears(1)
## [1] "2016-12-31 06:00:00 UTC"

使用时间周期函数则得到预期结果:

ymd("2016-01-01") + years(1)
## [1] "2017-01-01"

9.9.3 时间区间

lubridate提供了%--%运算符构造一个时间期间(time interval)。 时间区间可以求交集、并集等。

构造如:

d1 <- ymd_hms("2000-01-01 0:0:0")
d2 <- ymd_hms("2000-01-02 12:0:5")
din <- (d1 %--% d2); din
## [1] 2000-01-01 UTC--2000-01-02 12:00:05 UTC

对一个时间区间可以用除法计算其时间长度,如

din / ddays(1)
## [1] 1.500058
din / dseconds(1)
## [1] 129605

生成时间区间, 也可以用lubridate::interval(start, end)函数,如

interval(ymd_hms("2000-01-01 0:0:0"), 
  ymd_hms("2000-01-02 12:0:5"))
## [1] 2000-01-01 UTC--2000-01-02 12:00:05 UTC

可以指定时间长度和开始日期生成时间区间, 如

d1 <- ymd("2018-01-15")
din <- as.interval(dweeks(1), start=d1); din
## [1] 2018-01-15 UTC--2018-01-22 UTC

注意这个时间区间表面上涉及到8个日期, 但是实际长度还是只有7天, 因为每一天的具体时间都是按零时计算, 所以区间末尾的那一天实际不含在内。

lubridate::int_start()lubridate::int_end()函数访问时间区间的端点,如:

int_start(din)
## [1] "2018-01-15 UTC"
int_end(din)
## [1] "2018-01-22 UTC"

可以用as.duration()将一个时间区间转换成时间长度, 用as.period()将一个时间区间转换为可变时长的时间周期个数。

lubridate::int_shift()平移一个时间区间,如

din2 <- int_shift(din, by=ddays(3)); din2
## [1] 2018-01-18 UTC--2018-01-25 UTC

lubridate::int_overlaps()判断两个时间区间是否有共同部分,如

int_overlaps(din, din2)
## [1] TRUE

时间区间允许开始时间比结束时间晚, 用lubridate::int_standardize()可以将时间区间标准化成开始时间小于等于结束时间。 lubridate()现在没有提供求交集的功能, 一个自定义求交集的函数如下:

int_intersect <- function(int1, int2){
  n <- length(int1)
  int1 <- lubridate::int_standardize(int1)
  int2 <- lubridate::int_standardize(int2)
  sele <- lubridate::int_overlaps(int1, int2)
  inter <- rep(lubridate::interval(NA, NA), n)
  if(any(sele)){
    inter[sele] <- 
      lubridate::interval(pmax(lubridate::int_start(int1[sele]), 
                               lubridate::int_start(int2[sele])),
                          pmin(lubridate::int_end(int1[sele]), 
                               lubridate::int_end(int2[sele])))
  }
  inter
}

测试如:

d1 <- ymd(c("2018-01-15", "2018-01-18", "2018-01-25"))
d2 <- ymd(c("2018-01-21", "2018-01-23", "2018-01-30"))
din <- interval(d1, d2); din
## [1] 2018-01-15 UTC--2018-01-21 UTC 2018-01-18 UTC--2018-01-23 UTC
## [3] 2018-01-25 UTC--2018-01-30 UTC
int_intersect(rep(din[1], 2), din[2:3])
## [1] 2018-01-18 UTC--2018-01-21 UTC NA--NA

此自定义函数还可以进一步改成允许两个自变量长度不等的情形。

9.10 基本R软件的日期功能

9.10.1 生成日期和日期时间型数据

Sys.date()返回Date类型的当前日期。 Sys.time()返回POSIXct类型的当前日期时间。

yyyy-mm-ddyyyy/mm/dd格式的数据, 可以直接用as.Date()转换为Date类型,如:

x <- as.Date("1970-1-5"); x
## [1] "1970-01-05"
as.numeric(x)
## [1] 4

as.Date()可以将多个日期字符串转换成Date类型,如

as.Date(c("1970-1-5", "2017-9-12"))
## [1] "1970-01-05" "2017-09-12"

对于非标准的格式,在as.Date()中可以增加一个format选项, 其中用%Y表示四位数字的年, %m表示月份数字,%d表示日数字。如

as.Date("1/5/1970", format="%m/%d/%Y")
## [1] "1970-01-05"

as.POSIXct()函数把年月日格式的日期转换为R的标准日期, 没有时间部分就认为时间在午夜。如

as.POSIXct(c('1998-03-16'))
## [1] "1998-03-16 CST"
as.POSIXct(c('1998/03/16'))
## [1] "1998-03-16 CST"

年月日中间的分隔符可以用减号也可以用正斜杠, 但不能同时有减号又有斜杠。

待转换的日期时间字符串,可以是年月日之后隔一个空格以“时:分:秒”格式带有时间。如

as.POSIXct('1998-03-16 13:15:45')
## [1] "1998-03-16 13:15:45 CST"

as.POSIXct()可以同时转换多项日期时间,如

as.POSIXct(c('1998-03-16 13:15:45', '2015-11-22 9:45:3'))
## [1] "1998-03-16 13:15:45 CST" "2015-11-22 09:45:03 CST"

转换后的日期变量有class属性,取值为POSIXct与POSIXt, 并带有一个tzone(时区)属性。

x <- as.POSIXct(c('1998-03-16 13:15:45', '2015-11-22 9:45:3'))
attributes(x)
## $class
## [1] "POSIXct" "POSIXt" 
## 
## $tzone
## [1] ""

as.POSIXct()函数中用format参数指定一个日期格式。如

as.POSIXct('3/13/15', format='%m/%d/%y')
## [1] "2015-03-13 CST"

如果日期仅有年和月,必须添加日(添加01为日即可)才能读入。 比如用’1991-12’表示1991年12月,则如下程序将其读入为’1991-12-01’:

as.POSIXct(paste('1991-12', '-01', sep=''), format='%Y-%m-%d')
## [1] "1991-12-01 CST"

又如

old.lctime <- Sys.getlocale('LC_TIME')
Sys.setlocale('LC_TIME', 'C')
## [1] "C"
as.POSIXct(paste('01', 'DEC91', sep=''), format='%d%b%y')
## [1] "1991-12-01 CST"
Sys.setlocale('LC_TIME', old.lctime)
## [1] "Chinese (Simplified)_China.utf8"

'DEC91'转换成了’1991-12-01’。

如果明确地知道时区, 在as.POSIXct()as.POSIXlt()中可以加选项tz=字符串。 选项tz的缺省值为空字符串, 这一般对应于当前操作系统的默认时区。 但是,有些操作系统和R版本不能使用默认值, 这时可以为tz指定时区, 比如北京时间可指定为tz='Etc/GMT+8'。如

as.POSIXct('1949-10-01', tz='Etc/GMT+8')
## [1] "1949-10-01 -08"

9.10.2 取出日期时间的组成值

把一个R日期时间值用as.POSIXlt()转换为POSIXlt类型, 就可以用列表元素方法取出其组成的年、月、日、时、分、秒等数值。 如

x <- as.POSIXct('1998-03-16 13:15:45')
y <- as.POSIXlt(x)
cat(1900+y$year, y$mon+1, y$mday, y$hour, y$min, y$sec, '\n')
## 1998 3 16 13 15 45

注意year要加1900,mon要加1。 另外,列表元素wday取值1-6时表示星期一到星期六, 取值0时表示星期天。

对多个日期,as.POSIXlt()会把它们转换成一个列表(列表类型稍后讲述), 这时可以用列表元素year, mon, mday等取出日期成分。如

x <- as.POSIXct(c('1998-03-16', '2015-11-22'))
as.POSIXlt(x)$year + 1900
## [1] 1998 2015

9.10.3 日期计算

因为Date类型是用数值保存的,所以可以给日期加减一个整数,如:

x <- as.Date("1970-1-5")
x1 <- x + 10; x1
## [1] "1970-01-15"
x2 <- x - 5; x2
## [1] "1969-12-31"

所有的比较运算都适用于日期类型。

可以给一个日期加减一定的秒数,如

as.POSIXct(c('1998-03-16 13:15:45')) - 30
## [1] "1998-03-16 13:15:15 CST"
as.POSIXct(c('1998-03-16 13:15:45')) + 10
## [1] "1998-03-16 13:15:55 CST"

但是两个日期不能相加。

给一个日期加减一定天数, 可以通过加减秒数实现,如

as.POSIXct(c('1998-03-16 13:15:45')) + 3600*24*2
## [1] "1998-03-18 13:15:45 CST"

这个例子把日期推后了两天。

difftime(time1, time2, units='days')计算time1减去time2的天数, 如

x <- as.POSIXct(c('1998-03-16', '2015-11-22'))
c(difftime(x[2], x[1], units='days'))
## Time difference of 6460 days

函数结果用c()包裹以转换为数值, 否则会带有单位。

调用difftime()时如果前两个自变量中含有时间部分, 则间隔天数也会带有小数部分。如

x <- as.POSIXct(c('1998-03-16 13:15:45', '2015-11-22 9:45:3'))
c(difftime(x[2], x[1], units='days'))
## Time difference of 6459.854 days

difftime()units选项还可以取为 'secs', 'mins', 'hours'等。

9.11 练习

设文件dates.csv中包含如下内容:

"出生日期","发病日期"
"1941/3/8","2007/1/1"
"1972/1/24","2007/1/1"
"1932/6/1","2007/1/1"
"1947/5/17","2007/1/1"
"1943/3/10","2007/1/1"
"1940/1/8","2007/1/1"
"1947/8/5","2007/1/1"
"2005/4/14","2007/1/1"
"1961/6/23","2007/1/2"
"1949/1/10","2007/1/2"

把这个文件读入为R数据框dates.tab, 运行如下程序定义date1date2变量:

date1 <- dates.tab[,'出生日期']
date2 <- dates.tab[,'发病日期']
  1. 把date1、date2转换为R的POSIXct日期型。

  2. 求date1中的各个出生年。

  3. 计算发病时的年龄,以周岁论(过生日才算)。

  4. 把date2中发病年月转换为’monyy’格式,这里mon是如FEB这样英文三字母缩写, yy是两数字的年份。

  5. 对诸如’FEB91’, ’OCT15’这样的年月数据, 假设00—20表示21世纪年份,21—99表示20实际年份。 编写R函数,输入这样的字符型向量, 返回相应的POSIXct格式日期, 具体日期都取为相应月份的1号。 这个习题和后两个习题可以预习函数部分来做。

  6. 对R的POSIXct日期,写函数转换成’FEB91’, ’OCT15’这样的年月表示, 假设00—20表示21世纪年份,21—99表示20实际年份。

  7. 给定两个POSIXct日期向量birth和work, birth为生日,work是入职日期, 编写R函数, 返回相应的入职周岁整数值(不到生日时周岁值要减一)。