R日期和日期时间类型

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

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

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

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

从字符串生成日期数据

函数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"

时区和时区转换

上面例子结果显示中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"不起作用,
仅该时区的钟面时间起作用。

从数值生成日期数据

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()函数。

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

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"

日期显示格式

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”代表两位的秒数。

访问日期时间的组成值

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
}

日期舍入计算

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指定开始是星期一。

日期计算

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

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

时间长度

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()函数可以直接生成时间长度类型的数据,如

## [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"

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

时间周期

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

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"

时间区间

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

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

基本R软件的日期功能

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

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"

取出日期时间的组成值

把一个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

日期计算

因为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'等。

练习

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