第12章 时间

    我看到了我的爱恋 我飞到她的身边
    我捧出给她的礼物 那是一小块凝固的时间
    时间上有美丽的条纹 摸出来像浅海的泥一样柔软
    她把时间涂满全身 然后拉起我飞向存在的边缘

—刘慈欣《三体3:死神永生》

科幻小说《三体》三部曲,描绘了时间这个物理量的奥妙。在第6章对复活节的计算里,我们已经领教了时间数据的厉害。时间数据非常特殊又十分有趣,主要是因为时间单位不是十进制的:满60秒才进1分钟,满24小时进1日,满7日进1周,而考虑到闰年和闰秒等因素,日、周、月、年之间的进位就更加复杂,再加上时区和夏时制,处理起时间数据来花费的精力,真是一寸光阴一寸金。

举个例子就能切身体会到时间的复杂:你一定知道自己的生日几月几日,但大概并不知道自己出生那天是星期几吧?从出生那一刻到今天为止,已经过去了多少天?这辈子能遇见几次在星期天过生日吗?

回答这些问题,我们的R语言可以轻松办到。R 对时间数据的复杂性有充分的准备,让这件工作变得不是那么困难。

为了不引起歧义,本书中,我们将时间点(比如“现在几点几分了”)称为“时刻”,而时间长度(比如“还差几分钟下班”)称为“时间”。

12.1 时刻数据的获取

假定你出生的时刻是1994年9月22日20时30分0秒。我们先把你的生日告诉R,让R听得懂。

一般来说,我们从数据文件中读取数据表时,其中的时刻数据列往往是 “1994-09-22 20:30:00”这样的格式,读入之后,这一列自动按照字符格式存储。

bd <- '1994-09-22 20:30:00'

我们知道,字符串是不可以做数学的加减法的,比如我们想把这一列时刻都加20分钟,字符串是无法做到的,这就需要先把字符串转换成R能识别的时刻格式。

bdtime <- strptime(x = bd, format = '%Y-%m-%d %H:%M:%S', 
                   tz = "Asia/Shanghai")
bdtime
## [1] "1994-09-22 20:30:00 CST"

为什么strptime()函数里需要个formattz参数呢?因为如果不说一声的话,R就不知道这个字符串里哪个是年哪个是月,也不知道是哪个时区。

我们发现,bdtime的值依然带有引号,除了多了个时区代码外,看上去跟bd一模一样。这难道不是跟bd一样的字符串吗?不是的。

class(bd)
## [1] "character"
class(bdtime)
## [1] "POSIXlt" "POSIXt"

他们属于不同的类。bd属于字符串(character),而bdtime属于一种称为POSIXct的类。

除了从外界数据文件读取时刻数据外,R还可以获取计算机时钟的时刻。下面三个函数,都可以获得R运行环境中的当前时刻或日期。

t1 <- Sys.time()#返回当前时刻
t2 <- date()#返回当前时刻
t3 <- Sys.Date()#返回当前日期
t1
## [1] "2017-09-15 12:06:42 CEST"
t2
## [1] "Fri Sep 15 12:06:42 2017"
t3
## [1] "2017-09-15"

如果查看一下这三个变量的类,会发现各有不同:

class(t1)
## [1] "POSIXct" "POSIXt"
class(t2)
## [1] "character"
class(t3)
## [1] "Date"

这些类有什么区别和联系?请自行放狗去搜。

一切准备工作就绪,R现在知道了你的生日,就可以进行后续计算了。

12.2 时刻数据的格式

    女孩和男孩陷入爱河。后来战争即将爆发,兵荒马乱之中,男孩把写有日期的纸条塞进女孩手心,约定在战争结束那一年的那一天老地方见。如果谁没有赴约,就说明已遭遇不幸。
    战争结束后,女孩拿着纸条如约到达,苦苦等了一个月,却不见恋人踪影,悲愤之下自尽身亡。不久男孩赶到,看到的却是昔日恋人的坟墓,悲痛之下,也殉情而死。
    纸条上的日期是“12.10”。女孩是德国人,以为是10月12日;男孩来自美国,以为是12月10日。
    这个爱情悲剧告诉我们:处理时间数据,格式很重要。

— 我听来的故事

先看看你出生那天是星期几。很简单,只需这样操作:

bdtime$wday
## [1] 4

是个星期四,对吗?

$wday 表示从时刻数据中提取星期几。类似的,$hour, $min, $sec 分别用来提取时分秒,$year, $mon, $mday提取年月日,$yday提取一年内的第几天。

Example 12.1 今天是今年的第几天?

Example 12.2 算算你出生那天是当年的第几天。

时刻数据的全部组成信息,可以用unclass()函数来查询:

unlist(unclass(bdtime))
##    sec    min   hour   mday    mon   year   wday   yday 
##    "0"   "30"   "20"   "22"    "8"   "94"    "4"  "264" 
##  isdst   zone gmtoff 
##    "0"  "CST"     NA

为了方便显示,我们调用了unlist()函数。

由于世界各地风俗习惯不同,采取的时刻格式也各种各样。比如在表述日期时,放在最前面的,我们中国习惯先说年,美国习惯先说月,而德国习惯先说日。年月日的分隔符也有不同。R提供了 format()函数,来对这些格式进行随意转换。

format(bdtime, format = '%d.%m.%Y')
## [1] "22.09.1994"
小贴士 12.1 常用时刻格式的详细说明。

X 中文示例 英文示例 解释
%a 周日 Sun 星期几的缩写
%A 星期日 Sunday 星期几的全拼
%b 十月 Oct 月份缩写
%B 十月 October 月份全称
%c 周日 十月 1 Sun Oct 1 日期时刻全称
10:30:59 2017 10:30:59 2017
%C 20 20 世纪
%d 1 1 日(0补位)
%D 10/1/2017 10/1/2017 日/月/年
%F 10/1/2017 10/1/2017 年-月-日
%H 10 10 时(00-23)
%I 10 10 时(00-12)
%j 274 274 一年的第几天
%m 10 10
%M 30 30
%p 上午 AM 上下午
%r 10:30:59 上午 10:30:59 AM 12小时制时刻
%R 10:30 10:30 24小时制时刻
%S 59 59
%T 10:30:59 10:30:59 时刻
%u 7 7 星期几(周日为7)
%U 40 40 年中第几周
%V 39 39 年中第几周(ISO8601)
%w 0 0 星期几(周日为0)
%W 39 39 年内第几周(英制)
%x 10/1/2017 10/1/2017 日期,格式取决于系统设置
%X 10:30:59 10:30:59 AM 时刻,格式取决于系统设置
%y 17 17 年份后两位
%Y 2017 2017 年份四位
%Z GMT GMT 时区

百分号用来提示后面的格式是什么:%d表示日,%m表示月,%Y表示四位年份。他们用圆点.分隔。小贴士12.1列出了更多格式代表的含义。有了他们,就可以对时刻数据进行任意转换了。

Example 12.3 请把你的生日格式转换成包含日期时刻星期几的全称。

12.3 时刻数据的计算

时刻数据是可以参加加减法计算和逻辑运算的。看看你的生日加上1是什么:

bdtime + 1
## [1] "1994-09-22 20:30:01 CST"

bdtime + 1得到的是bdtime之后1秒的时刻。可以预料, + 60 就是1分钟后, + 3600 就是1小时后,而 + 86400 就是1天后。

bdtime + 60
## [1] "1994-09-22 20:31:00 CST"
bdtime + 3600
## [1] "1994-09-22 21:30:00 CST"
t3 <- bdtime + 86400
t3
## [1] "1994-09-23 20:30:00 CST"

果然如此。R把我们加上的秒数按照复杂的时间进位方式自动处理了。当然,时刻也可以减去一个数,让时光倒流。

两个时刻之间可以相减,得到两个时刻之间的时间长度。假如我的生日是1995年9月1日7时30分,那么你我生日之间相差的时间就是

bdtime2 <- strptime(
  '1995-09-01 7:30', format = '%Y-%m-%d %H:%M', 
  tz = 'Asia/Shanghai')
bdtime2 - bdtime
## Time difference of 343.4583 days

还记得我们有个diff()函数来计算两者之差么?类似地,时刻数据处理中有个difftime()函数,效果跟上面的减法大体相同,不同之处是可以指定输出数值的单位:

difftime(time1 = bdtime2, time2 = bdtime, units = 'secs')
## Time difference of 29674800 secs

我们还可以计算距离你我生日相等长度的那一天:

mean(c(bdtime, bdtime2))
## [1] "1995-03-13 07:00:00 CET"
思考 12.1 既然两个时刻可以相减,那么两个时刻能否相加?为什么?

除了本文介绍的函数外,我们还可以根据需要选用扩展包,例如timeDate (Team et al. 2015),可以让我们对时间数据的处理能力如虎添翼。

Example 12.4 算算到今天为止,你已经来到这个世界多少天?多少小时?多少秒?
Example 12.5 算算你从0岁到100岁,每年的生日都是星期几。作图展示,横轴是年份或你的年龄,纵轴是星期几。数数有几个生日是在星期天。

12.4 课外活动:夏令时

如果你只处理中国的时刻数据,那么是比较幸运的。如果遇到欧美国家的时刻数据,就不得不考虑一个特殊的问题:夏令时。

夏令时,或称夏时制,正规的名称是“日光节约时制”。欧洲和美国很多地区,夏季天亮得太早,于是就硬生生把时钟调快一小时,大家早起早睡,据说可以充分利用白天的光照而省电。但是,夏令时的实际收效至今仍有争议,很多人认为靠照明省下那点电微不足道,而粗暴地调时钟把大家的作息时间弄得很乱,尤其对老人和孩子影响很大,列车、航班等统统要做调整。我国其实在1986年到1991年实行了六年夏时制,实在得不偿失,就取消了。

12.1用 R 做出的德国拜罗伊特市日出日落时刻图,虚线和实线分别显示的是不使用和使用夏时制的日出日落时刻。很容易看出夏时制的好处:夏天把一小时的时间往后挪了之后,不至于天亮之后过很久才需要上班,并且下班后距离天黑还有很长时间可以让人去泡吧,泡网,泡……不挪到下午的话,这一个小时在早上只够泡个澡。

德国拜罗伊特日出日落时刻.

图 12.1: 德国拜罗伊特日出日落时刻.

R 在处理时刻数据的时候充分考虑了夏时制。时刻数据变量名,后面接上$isdst 就表示“操作中是否是光节约时制”(逻辑值,1 表示TRUE,0 表示FALSE

我学习R语言时正生活在德国,遇到了一件趣事,至今记忆犹新。德国的夏令时是这样调整时钟的:每年3月的最后一个星期天,凌晨2点钟把时钟拨到3点。夏令时结束时,在10月的最后一个星期天凌晨再拨回来。那么,在R里是怎么处理这种复杂的情况呢?

我们以2018年为例,3月最后一个星期天是25日,由于凌晨2点后会直接跳至3点,我们看看R的世界里这段时间发生了什么:

a <- strptime("2018-03-25 03:00:00", 
              format = "%Y-%m-%d %H:%M:%S", tz = "CET") 
a
## [1] "2018-03-25 03:00:00 CEST"

注意,我们明明设定的时区是CET中欧时间(Central European Time),但R自动转换成了CEST中欧夏季时间。

照理说,3点钟的前一秒应该是2:59:59,我们来试试:

a - 1
## [1] "2018-03-25 01:59:59 CET"

竟然是1:59:59!2点钟就这么凭空消失了么?

我不甘心,于是试了试另外一个函数,同样可以给时刻变量赋值:

a <- as.POSIXlt("2018-03-25 03:00:00", tz = "CET")
a
## [1] "2018-03-25 03:00:00 CEST"
a - 1
## [1] "2018-03-25 01:59:59 CET"

时区符号仍然自动改变了。

如果我硬来,非要找回 2 点钟呢?

a <- as.POSIXlt("2018-03-25 02:00:00", tz = "CET")
a
## [1] "2018-03-25 CET"

R索性把时刻给丢了,仅保留了日期。这就是对我任性的回答么?

换回strptime()函数试试?

a <- strptime("2018-03-25 02:00:00", 
              format = "%Y-%m-%d %H:%M:%S", tz = "CET") #
a
## [1] "2018-03-25 02:00:00"

欧叶!但是……

a - 1
## [1] NA

啊哦……

接受这个现实吧:在中欧,2018年3月25日2:00:00 到2:59:59这段时间是不存在的!若要在数据处理中避免这种情况发生,还是强行把时区都设置成 GMT 保险。为了保险起见,我每次从别人那里拿到数据都会问:你这数据里的时间,用没用夏令时?

然而,这也暗示男孩女孩们可以用一种新方法对别人的表白说no:那我们就在中欧时间2018年3月25日凌晨2:30见吧……

References

Team, Rmetrics Core, Diethelm Wuertz, Tobias Setz, Yohan Chalabi, Martin Maechler, and Joe W. Byers. 2015. TimeDate: Rmetrics - Chronological and Calendar Objects. https://CRAN.R-project.org/package=timeDate.