lubridate—轻松处理日期时间

本文尝试翻译了Garrett Grolemund(《R语言入门与实践》作者)和Hadley Wickham两位大神发表的关于lubridate包的文章,该包专注于对日期时间数据的处理。

本人英文水平有限,翻译难免有纰漏,如果有朋友发现了问题,欢迎指正~

另,原文在此:http://vita.had.co.nz/papers/lubridate.pdf

摘要

本文介绍了R中的lubridate包,该包有利于灵活处理日期和时间数据。日期时间数据为数据分析家们造成了各种各样的技术问题。本文列举了这些问题,并提供了解决问题方法。

本文还介绍了R中日期时间算法的概念框架。

关键词:日期 时间 时区 夏时制 R

1.简介

日期有许多不同的格式,识别和分析它们通常比较麻烦。即便我们能识别出不同格式的日期,仍然面临特定日期时间的问题。我们怎么才能轻易提取出日期时间的元素,例如年、月或秒?怎么才能在时区之间进行切换,或者比较夏时制地区和非夏时制地区的日期时间呢?当我们试着用它们做算术时,日期时间数据会产生更复杂的问题。像闰年和夏时制这样的惯例,使所谓的“一天之后”或“确切的两年”变得不那么清晰,甚至闰秒也会破坏看似简单的计算。这种复杂性也会影响其他任务,例如为绘制日期时间数据构建合理的刻度。

虽然Base R能够处理其中一些问题,但使用的语法会根据日期时间的类型发生变化,可能会令人感到混乱和难以记住。lubridate包重视这些问题,以新颖但有用的方式来处理R中的日期时间。使用lubridate包将增强使用者任何数据分析,包括日期时间数据分析方面的体验。特别是,lubridate将帮助用户:

  • 1.识别和解析日期时间数据,见第3节。
  • 2.提取和修改日期时间数据的成分,如年、月、日、小时、分钟和秒,见第4节。
  • 3.对日期时间和时间间隔进行精确的计算,参见第5和6节。
  • 4.处理时区和夏令制,见第7和8节。
    lubridate兼容多种常见的日期和时间序列对象,包括字符串,POSIXct,POSIXlt,Date,chron,timeDate,zoo,xts,its,tis,timeSeries,fts,以及tseries对象。

lubridate能覆盖Base R中对POSIXt, Date, 以及difftime对象使用的加减运算。这保证了用户能够用lubridate中的timespan(处理时段数据的方法)对日期时间执行简单的运算,但它并不改变R对非lubridate对象实行加减运算。

lubridate引进了Joda-Time项目介绍的四种时间对象,以及对四种时间对象进行了测量的概念模型。第5节描述了这一模型,并解释了lubridate 如何使用它对R数据进行简单而精确的运算。

本文展示了lubridate包提供的实用工具,并以一个应用范例结尾。本文的lubridate 0.2版本可从“the Comprehensive R Archive ”下载,网址: http://CRAN.R-project.org/package=lubridate 。开发版本在此:https://github.com/hadley/lubridate%E3%80%82

2.研究动机

为了体现lubridate的简单之处,我们考虑一个常见的场景:给定一个字符串,我们希望把它作为日期时间对象提取出月份,并将其更改为二月。左边是用Base R的方法完成这三个任务,右边是lubridate方法。

现在更进一步,我们将日期提前一天,并用格林尼治子午线时区(格林威治时间GMT)显示新日期。同样,Base R方法在左边,右边是lubridate方法。

lubridate对基本日期时间的操作是简单直观的。此外,lubridate这种简单的操作方法适用于大多数流行的日期时间类(Date,POSIXt,chron,等等)。表1更完整的对lubridate和Base R方法进行了比较。它显示了lubridate如何简化R中常见的日期时间,还对lubridate的使用方法进行了总结。

3.解析日期与时间

我们可以用lubridate提供的ymd() 系列函数来读取日期数据。字母y,m和d分别对应年、月和日。读取日期时,根据日期时间的元素顺序,选择相应的函数。例如,在下面的日期中,月份在首,其次是日,然后是年。所以我们会用mdy()函数:

这些函数将字符串形式的日期转换为POSIXct类型。另外,这些函数会自动识别日期中常用的分隔符,包括:“-”,“/”,“.” 和 “”(即,无分隔符)。当ymd()函数应用于向量形式的日期数据时,lubridate将假定所有的日期都具有一样的顺序和相同的分隔符。ymd()型函数同样适用于用小时、分钟、秒记录的时间。这些函数使解析任何可转变为字符串的日期时间对象变得简单。完整的ymd()函数清单见表2:

4.操作日期与时间

每个日期时间都是不同元素的组合,每个元素都有自己的值。例如,大多数日期时间包括年、月、日的值等等。这些元素的组合指定了确切时刻。我们可以用表3中的存取函数轻易地获取每个日期时间中的元素。

例如,当前系统时间为:

我们可以获取它的每一个元素。

对于month()和wday()这样的函数,我们还可以通过label指定是要显示数值,还是显示名称(缩写或全称)。例如,

我们也可以使用任意的提取函数来设置一个元素的值。这将会改变日期时间确定的具体时刻。例如,把日期改为该月的第五天。

我们还可以将元素设置为更复杂的值。例如,

注意,如果我们将一个元素设置的值超过它所支持的范围,那么差值将自动延续到下一个更高的元素。例如,

我们可以利用此特性去寻找一个月的最后一天:

lubridate还提供更新日期时间的方法。当你想批量更改多个属性,或者希望创建一个改良的副本,这将非常有用。

最后,我们还可以通过加上或减去对应的时间单位来更改日期。例如,下面的方法产生相同的结果。

注意,hours()(复数)和hour()(单数)不是一个函数。hours()将创建一个新的对象,可以和日期时间进行加减操作。这些对象在下一节中会讨论。

5.日期时间的数学运算

我们可以用lubridate完成复杂的日期时间运算。时钟时间周期性地重新校准以反映天文条件,例如白昼时间,或者地球相对于太阳的轴倾斜。我们知道这些重新校准会产生夏时制,闰年和闰秒。这些校准造成的时间偏差会使一个可能很简单的数学运算复杂化。如果今天是2010年1月1日,我们希望知道从现在算起一年后是哪一天,我们可以简单地在日期的年元素上加1。

或者,因为一年相当于365天,我们也可以将365添加到日期元素中。

如果我们在2012年1月1日尝试同样的方法,就会出现麻烦。2012年是闰年,这意味着它有额外的一天。我们的两种方法给我们提供了不同的答案,因为年的长度改变了。

在时间的不同时刻,月、周、日、小时、甚至分钟的长度也会有所不同。我们可以认为它们是时间的相对单位,它们的长度取决于开始的时间。相比之下,秒总是有一个一致的长度。因此,秒是精确的时间单位。

研究人员可能对绝对长度、相对长度或两者都感兴趣。例如,物理物体的速度适合用绝对长度衡量。股票市场的开盘时间比较容易用相对长度来模拟。

Lubridate引入了四个与时间对象,从而使相对和绝对单位能用数学运算。这四个时间对象借用了Joda Time项目的术语,分别是instants,intervals,durations和periods。

5.1.Instants 时点

时点Instant是一个特定的时刻,比如2012年1月1日。我们每次将日期解析进R时都会创建一个时点。

lubridate不创建时点类对象。相反,它识别出的任何一个指向具体时间的日期时间对象,都是时点。我们可以用instant()测试一个对象是否是时点。例如,

我们可以用floor_date(),ceiling_date(),和round_date()进行模糊取整,即将日期时间取整到不同的单位,如分,时,月,等。例如,

我们可以用now()获取当前的时点:年,月,日,时,分,秒,用today()获取当前时点:年,月,日。

5.2. Intervals时间间隔

intervals、durations、periods都是记录时间跨度的方法。其中, interval是最简单的,是特定的时间跨度。一个interval是两个特定时刻之间的时间。时间间隔的长度从不模棱两可,因为我们知道它的发生时间,任何时间都可以计算确切长度。

我们可以通过两时点相减或使用命令new_interval()来创建interval间隔对象。

由于interval必须绑定开始和结束日期,所以它对日期时间的数学运算作用不大。它只在需要在开始日期上加一个间隔,或者从结束日期减去一个间隔时有意义。

5.3. Durations时间跨度

如果我们从一个interval中删除开始和结束日期,我们将得到一个可以添加到任何日期上的通用的时间跨度。但我们要如何衡量这段时间呢?如果我们以绝对长度的秒为单位记录,它将有精确的长度,这种时间跨度为duration。如果我们用更大的单位记录,比如分或年,由于这些单位的长度随时间而变化,时间跨度的确切长度将取决于开始时间,这些非精确的时间跨度称为period,我们将在下一节讨论。

duration的长度与闰年、闰秒和夏时制无关,因为它是以秒为单位计算的。因此,duration具有一致的长度,durations之间可以互相比较。duration是比较基于时间的属性(如速度、速率和寿命)的合适对象。

lubridate兼容Base R 中difftime类型对象,并且difftime帮助进行duration计算。

对于比较大的时间跨度,用秒来描述长度是不方便的。例如,没有多少人会认为31536000秒是标准年的长度。因此,lubridate也使用其它标准的时间单位来显示duration 。然而,这些单位只是为了方便起见而给出的近似数。duration底层对象总是以秒记录。近似的单位适用于以下关系:一分钟是60秒,一小时是3600秒,一天是86400秒,一个星期是604800秒,一年是31536000秒。月单位不适用,因为它很多变。

通过函数dyears(),dweeks(),ddays(),dhours(),dminutes(),和dseconds(),可以轻松的创建duration时间对象。开头的d代表duration,就与第5.4节中讨论的period对象区分开了。

用上面给的近似关系可以为每个对象创建以秒为单位的duration。例如(目前已经用d-代替e-,下面例子没有修改),

duration可以被任何instant加和减。例如,

duration也可以从interval和其他duration上加或减去。例如,

我们也可以用as.duration()将interval间隔转换为durations。

5.4. Periods时间跨度

period以大于秒的单位记录时间跨度,如年、月、周、日、小时和分钟。为了方便起见,我们还是可以创建使用秒的period,但这样的period与duration有相同的属性。我们通过years(),months(),weeks(),days(),hours(),minutes(),和seconds()这些函数创建period。

这些函数名称中不包含字母e/d,因为他们不是近似值。例如,month(2)总是表示两个月的长度,即便2个月代表的总时间会跟随具体开始时间变动。因此,我们无法精确计算一个period的秒数,直到我们知道它的开始时间。但是,我们仍然可以用period进行日期时间计算。当我们为instant时点加上或减去一个period,这个period就绑定在了instant上。这个instant告诉我们这个period的开始时间,这使得我们能够以秒计算出精确长度。

换句话说,我们可以用period来精确地表述时钟时间,而不用知道闰秒、闰天以及夏时制等是否发生。

我们也可以用period()函数将interval转换为period对象。

period可以从 instant, interval, 和其它period中加上或减去,但不适用于duration。

总之,可以对四种类型的时间对象进行数学运算:instants, intervals, durations和periods。表4描述了哪些对象可以相互添加,以及产生的结果属于什么类型。

6.近似日期(模糊取整)

像数字一样,日期时间也是按顺序发生的。这代表日期时间是可以被近似的,lubridate提供了3种方法实现这个功能:round_date(),floor_date(),和ceiling_date()。每个函数的第一个参数代表要近似的对象,第二个参数代表要近似到的单位。例如,我们可以将2010年4月20日近似到最近的一天,或者最近的一个月。

注意,对日期时间对象进行近似时,近似的单位就成为原对象的最小单位,例如round_date(apri120,“day”)就会把天后面的小时,分钟和秒等通通设置为00。

ceiling_date()提供了另外一种更简单的办法找到某月的最后一天。把日期定在它的下个月,然后减去一天。

7.时区

不同的时区使时点有多个不同的名称。例如,“2010-03-26 11:53:24 CDT”和“2010-03-26 12:53:24 EDT”都描述了相同的时点。前者是美国中央时区(CDT),后者是美国东部时区(EDT)。时区使日期时间数据复杂化,但对于将时钟时间转换为夏时制时非常有用。

instant的时间是世界协调时区(UTC),它与标准时钟时间是一致的,这就节省了计算量,但如果你的计算机坚持将时间转换为你当前的时区,可能会很烦人,并且在讨论时间时也不方便。

lubridate用两种方式减轻时区造成的差异。我们可以用with_tz()去改变时区,显示同一个时间点在不同时区的时间,例如,

force_tz()与with_tz()相反:它改变了时点,但是时间的数值保持不变。例如下面,同样的时间数值,由于时区不同,时间相差了6个小时。

8.夏时制

在世界上许多地方,官方时间在春季拨快一小时,秋季拨慢一小时。例如,伊利诺斯州的芝加哥,在2010年3月14日凌晨2:00施行夏时制,改变发生前的时间是“2010-03-14 01:59:59 CST”,1秒钟后时间就变为夏时制的凌晨3点(夏时制比标准时间快一小时)。

通过一秒的变化,我们似乎得到了额外的一小时,这就是夏时制的工作原理。我们可以用period代替duration来避免夏时制造成的时间变化。例如,

当我们使用period时,我们不必在意夏令制的变化,因为它不会影响我们的计算。添加一个duration则会显示时钟上的确切时间。

如果我们尝试在2010-03-14 01:59:59 CST和2010-03-14 03:00:00 CDT之间创造一个instant时点,lubridate会返回NA,因为这样的时间是无效的,两个时间是相等的。

我们也可以通过固定时区来避免夏时制造成的时间偏差,例如将时区固定在不采用夏时制的“UTC”。

9.范例1

接下来的两节讲解lubridate的使用技巧。首先,我们将使用lubridate计算节日的具体日期。然后我们会用lubridate探索实例数据集(湖人)。

9.1. Thanksgiving

有些节日,如感恩节(美国)和阵亡将士纪念日(美国)并不发生在固定的日期。相反,他们是按照一个规则来庆祝的。例如,感恩节是在十一月的第四个星期四庆祝的。为了得到感恩节是在2010年什么时间举行的,我们可以从2010的第一天开始计算。

我们可以在月份上加上10,或者直接将月份设定为11月。

我们查看一下11月1日是星期几。

这意味着11月4日将是十一月的第一个星期四。

接下来,我们增加三个星期到十一月的第四个星期四。

9.2. Memorial Day

阵亡将士纪念日按照惯例,是在五月的最后一个星期一,为了计算阵亡将士纪念日的日期,我们可以从2010年的第一天开始。

接下来,我们将月份设定为5月。

我们要计算的假期发生在月末而不是月初,我们通过使用ceiling_date()将月份近似到下一个月,再减去一天得到这个月的最后一天。

然后我们可以查看5月31日是星期几。正巧是星期一,所以我们得到结果了。如果5月31日非星期一,我们可以减去适当的天数来获得五月的最后一个星期一。

10.范例2(不清楚篮球比赛的专业术语,翻译可能有误)

湖人数据集包含了洛杉矶湖人队在2008-2009赛季,各大联盟比赛的统计数据。此数据来自http://www.basketballgeek.com/downloads/ (Parker 2010),另lubridate包已内含该数据集。我们将探索湖人全年的比赛分布以及湖人队在比赛中的战术分配情况。我们使用ggplot2。

湖人数据集用date来记录每场比赛的日期。使用str()命令,我们看到R将日期识别为整数型。

在使用date数据之前,我们用ymd()将整数型的日期转化为R认可的类型。

现在date数据变为R中的POSIXct类型。我们可以用处理POSIXct类型数据的方式处理日期了。例如,如果我们绘制整个赛季中主场和客场比赛的次数,X轴为date。

图1显示了整个赛季比赛不断,但比赛之间会有短时相隔。赛季开始时,比赛的频率看起来较低,而且比赛似乎被均匀分为主场和客场。X轴上的刻度和刻度间隔是由lubridate包中pretty.dates()自动生成的。

接下来我们看看湖人比赛次数的周分布情况。我们用wday()命令获取每个日期具体是星期几。

篮球比赛次数周变化,如图2。令人惊讶的是,星期二比赛次数最多。

现在让我们看看每场比赛,特别是整个赛季的投球情况分布。湖人队数据集中的time列出了每场比赛中投篮、篮板、罚球等技术时,距离单节比赛结束的时间。比赛单节时长12分钟,从12:00倒计时至00:00,分号之前的数字代表比赛剩下的分钟,后两个数字代表剩下的秒数。

time仅包含分钟和秒,无法确定唯一日期-时间,因此它不是R中的标准日期时间类型。我们用ms()函数将分钟和秒存储为5.4节中定义的period对象。

由于period仅有相对长度,不方便进行长度比较。所以我们下一步应该将period转换为有确切长度的duration。

现在我们可以直接比较不同duration了。由于没有比赛的具体开始时间,我们不能通过开始时间加duration,来确定每场比赛中投篮、篮板、罚球等的确切时间。然而,我们仍然可以计算每场比赛中每个投篮、篮板、罚球等何时发生。每节比赛时间12分钟,比赛开始时从12:00开始倒计时。所以为了计算每个投篮、篮板、罚球等耗的时间,我们从12, 24, 36,或48分钟(取决于是哪一节)扣除time上的时间,这将创建一个新的duration,记录了每个投篮、篮板、罚球等距离开场的时间差。

数据集中观察到某些比赛有加时,为了保持简化,我们将忽略加时赛。

ggplot2不支持duration采用的difftime类型数据,为了绘制我们的数据,我们可以从duration中提取整数数值,提取的数值与duration代表的时间差相等(图3)。

或者也可以为duration加上一个相同的instant,创建一个ggplot2支持的日期时间类型。轴刻度由pretty.date()自动生成。pretty.date()可以创建出最直观的日期时间数据标记,进一步增强了我们的图(图4)。

投篮、篮板、罚球等的耗时在每节比赛中间比较多,在下节比赛的开始时耗时较少,如图4。

现在让我们更仔细地看一场篮球赛:本赛季的第一场比赛。这场比赛是在2008年10月28日进行的。对于这场比赛,我们可以很容易地模拟每次投篮之间的时间。

投篮之间的时间差是每次投篮之间的时间跨度,因为我们记录了每次投篮的duration,通过两个duration相减来记录差异。这将自动创建一个新的duration,其长度等于前两个duration时间之间的差异。

我们在图5中绘制此信息。我们看到至少30秒内会有一次尝试投篮,但有时60秒就都没有尝试。

我们也可以检查比赛中比分的变化。如图6所示,这表明本赛季的第一场比赛很顺利:湖人队整场比赛保持领先。注:plyr包中的下ddply()函数会使计算更简单。

11.结论

日期时间会造成其他数据类型不存在的技术困难。日期时间类型过多,具体地识别它们是很困难的,而且我们也很难访问和操作过多的日期时间类型。我们经常会对日期时间类型进行数学运算,但我们必须小心,因此比起其他普通数值型数据,我们要遵循很多规则。最后,与日期相关的很多惯例,如夏时制和时区,也让我们很难比较和识别不同的时间。Base R可以处理其中很多困难,但无法全部处理。而且,Base R中处理不同日期时间类型有不同的方法,而且会让人感到非常复杂和混乱。

lubridate使得我们更容易处理R中的日期时间数据。Lubridate包提供了一系列处理常用日期时间的标准方法。这些方法使得解析、操作和计算日期时间对象变得简单。通过引进基于java的Joda Time项目推出的时间概念,lubridate有助于研究人员进行精确的计算以及构建与时间有关的复杂过程模型。lubridate也方便进行时区切换,以及更方便利用或者忽略夏时制造成的时间差异。

未来,通过lubridate包我们可以处理不完整的日期,可以对重复发生事件进行建模,如股票市场开放时间,营业时间,或街道保洁时间。特别是,我们希望为R创造一种方法,可以对重复发生的日期模式进行处理。

致谢.略

参考.略

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容