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創造一種方法,可以對重復發生的日期模式進行處理。

致謝.略

參考.略

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容