9 R日期时间

9.1 R日期和日期时间类型

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

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

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

library(lubridate)
## Warning: 程辑包'lubridate'是用R版本4.0.3 来建造的
## 
## 载入程辑包:'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union

9.2 从字符串生成日期数据

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

today()
## [1] "2020-12-28"

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

now()
## [1] "2020-12-28 08:25:04 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::make_date(year, month, day)可以从三个数值构成日期向量。 如

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

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

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

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

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

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"

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

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

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

as_datetime(as.Date("1998-03-16"))
## [1] "1998-03-16 UTC"

9.3 日期显示格式

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”代表两位的月内日期号。

"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.936"

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.4 访问日期时间的组成值

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.5 日期舍入计算

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.6 日期计算

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

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

9.6.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。

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"

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

unclass()函数将时间长度数据的类型转换为普通数值, 如:

unclass(dhours(1))
## [1] 3600

9.6.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"

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

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

9.6.3 时间区间

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

构造如:

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.7 基本R软件的日期功能

9.7.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.936"

'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.7.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.7.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.8 练习

设文件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函数, 返回相应的入职周岁整数值(不到生日时周岁值要减一)。