第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()
函数里需要个format
和tz
参数呢?因为如果不说一声的话,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
提取一年内的第几天。
时刻数据的全部组成信息,可以用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"
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列出了更多格式代表的含义。有了他们,就可以对时刻数据进行任意转换了。
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"
除了本文介绍的函数外,我们还可以根据需要选用扩展包,例如timeDate (Team et al. 2015),可以让我们对时间数据的处理能力如虎添翼。
12.4 课外活动:夏令时
如果你只处理中国的时刻数据,那么是比较幸运的。如果遇到欧美国家的时刻数据,就不得不考虑一个特殊的问题:夏令时。
夏令时,或称夏时制,正规的名称是“日光节约时制”。欧洲和美国很多地区,夏季天亮得太早,于是就硬生生把时钟调快一小时,大家早起早睡,据说可以充分利用白天的光照而省电。但是,夏令时的实际收效至今仍有争议,很多人认为靠照明省下那点电微不足道,而粗暴地调时钟把大家的作息时间弄得很乱,尤其对老人和孩子影响很大,列车、航班等统统要做调整。我国其实在1986年到1991年实行了六年夏时制,实在得不偿失,就取消了。
图12.1用 R 做出的德国拜罗伊特市日出日落时刻图,虚线和实线分别显示的是不使用和使用夏时制的日出日落时刻。很容易看出夏时制的好处:夏天把一小时的时间往后挪了之后,不至于天亮之后过很久才需要上班,并且下班后距离天黑还有很长时间可以让人去泡吧,泡网,泡……不挪到下午的话,这一个小时在早上只够泡个澡。
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.