lubridate—輕鬆處理日期時間

weixin_33670713發表於2017-11-08

本文嘗試翻譯了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方法。

8895558-9611625c27c987eb.jpg

現在更進一步,我們將日期提前一天,並用格林尼治子午線時區(格林威治時間GMT)顯示新日期。同樣,Base R方法在左邊,右邊是lubridate方法。

8895558-9bc4b3c24497df59.jpg

lubridate對基本日期時間的操作是簡單直觀的。此外,lubridate這種簡單的操作方法適用於大多數流行的日期時間類(Date,POSIXt,chron,等等)。表1更完整的對lubridate和Base R方法進行了比較。它顯示了lubridate如何簡化R中常見的日期時間,還對lubridate的使用方法進行了總結。

8895558-a4db7160780bed16.jpg

3.解析日期與時間

我們可以用lubridate提供的ymd() 系列函式來讀取日期資料。字母y,m和d分別對應年、月和日。讀取日期時,根據日期時間的元素順序,選擇相應的函式。例如,在下面的日期中,月份在首,其次是日,然後是年。所以我們會用mdy()函式:

8895558-84572a8289002c96.jpg

這些函式將字串形式的日期轉換為POSIXct型別。另外,這些函式會自動識別日期中常用的分隔符,包括:“-”,“/”,“.” 和 “”(即,無分隔符)。當ymd()函式應用於向量形式的日期資料時,lubridate將假定所有的日期都具有一樣的順序和相同的分隔符。ymd()型函式同樣適用於用小時、分鐘、秒記錄的時間。這些函式使解析任何可轉變為字串的日期時間物件變得簡單。完整的ymd()函式清單見表2:

8895558-4d958fc0f8ba252a.jpg

4.操作日期與時間

每個日期時間都是不同元素的組合,每個元素都有自己的值。例如,大多數日期時間包括年、月、日的值等等。這些元素的組合指定了確切時刻。我們可以用表3中的存取函式輕易地獲取每個日期時間中的元素。

8895558-f6536573bd0769fd.jpg

例如,當前系統時間為:

8895558-428c8bf53aecb610.jpg

我們可以獲取它的每一個元素。

8895558-8ebf6b07116f7aef.jpg

對於month()和wday()這樣的函式,我們還可以通過label指定是要顯示數值,還是顯示名稱(縮寫或全稱)。例如,

8895558-5965c42851e0f160.jpg
8895558-83c9854fd46d9a23.jpg

我們也可以使用任意的提取函式來設定一個元素的值。這將會改變日期時間確定的具體時刻。例如,把日期改為該月的第五天。

8895558-ac860447ef40b6dd.jpg

我們還可以將元素設定為更復雜的值。例如,

8895558-14b3e985771fb9ba.jpg

注意,如果我們將一個元素設定的值超過它所支援的範圍,那麼差值將自動延續到下一個更高的元素。例如,

8895558-1977902db6d3d4a2.jpg

我們可以利用此特性去尋找一個月的最後一天:

8895558-114e5c7339ec7b74.jpg

lubridate還提供更新日期時間的方法。當你想批量更改多個屬性,或者希望建立一個改良的副本,這將非常有用。

8895558-bfa3d7dd70b9a8b5.jpg

最後,我們還可以通過加上或減去對應的時間單位來更改日期。例如,下面的方法產生相同的結果。

8895558-befa70c11f848017.jpg

注意,hours()(複數)和hour()(單數)不是一個函式。hours()將建立一個新的物件,可以和日期時間進行加減操作。這些物件在下一節中會討論。

5.日期時間的數學運算

我們可以用lubridate完成複雜的日期時間運算。時鐘時間週期性地重新校準以反映天文條件,例如白晝時間,或者地球相對於太陽的軸傾斜。我們知道這些重新校準會產生夏時制,閏年和閏秒。這些校準造成的時間偏差會使一個可能很簡單的數學運算複雜化。如果今天是2010年1月1日,我們希望知道從現在算起一年後是哪一天,我們可以簡單地在日期的年元素上加1。

8895558-d2e5882ce7f842ce.jpg

或者,因為一年相當於365天,我們也可以將365新增到日期元素中。

8895558-d263573449802f46.jpg

如果我們在2012年1月1日嘗試同樣的方法,就會出現麻煩。2012年是閏年,這意味著它有額外的一天。我們的兩種方法給我們提供了不同的答案,因為年的長度改變了。

8895558-4066191b64b6e2d3.jpg

在時間的不同時刻,月、周、日、小時、甚至分鐘的長度也會有所不同。我們可以認為它們是時間的相對單位,它們的長度取決於開始的時間。相比之下,秒總是有一個一致的長度。因此,秒是精確的時間單位。

研究人員可能對絕對長度、相對長度或兩者都感興趣。例如,物理物體的速度適合用絕對長度衡量。股票市場的開盤時間比較容易用相對長度來模擬。

Lubridate引入了四個與時間物件,從而使相對和絕對單位能用數學運算。這四個時間物件借用了Joda Time專案的術語,分別是instants,intervals,durations和periods。

5.1.Instants 時點

時點Instant是一個特定的時刻,比如2012年1月1日。我們每次將日期解析進R時都會建立一個時點。

8895558-563666250eb1c277.jpg

lubridate不建立時點類物件。相反,它識別出的任何一個指向具體時間的日期時間物件,都是時點。我們可以用instant()測試一個物件是否是時點。例如,

8895558-3127a231b8594060.jpg

我們可以用floor_date(),ceiling_date(),和round_date()進行模糊取整,即將日期時間取整到不同的單位,如分,時,月,等。例如,

8895558-58b90f15fbc67fe4.jpg

我們可以用now()獲取當前的時點:年,月,日,時,分,秒,用today()獲取當前時點:年,月,日。

5.2. Intervals時間間隔

intervals、durations、periods都是記錄時間跨度的方法。其中, interval是最簡單的,是特定的時間跨度。一個interval是兩個特定時刻之間的時間。時間間隔的長度從不模稜兩可,因為我們知道它的發生時間,任何時間都可以計算確切長度。

我們可以通過兩時點相減或使用命令new_interval()來建立interval間隔物件。

8895558-9f44f8cbad71415c.jpg

由於interval必須繫結開始和結束日期,所以它對日期時間的數學運算作用不大。它只在需要在開始日期上加一個間隔,或者從結束日期減去一個間隔時有意義。

8895558-c91bda9f46117028.jpg

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-,下面例子沒有修改),

8895558-33cbb803a84e874b.jpg

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

8895558-07bd1a88dee747d9.jpg

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

8895558-b8f3fb713855b6ba.jpg

我們也可以用as.duration()將interval間隔轉換為durations。

8895558-391aa8c288b08207.jpg

5.4. Periods時間跨度

period以大於秒的單位記錄時間跨度,如年、月、周、日、小時和分鐘。為了方便起見,我們還是可以建立使用秒的period,但這樣的period與duration有相同的屬性。我們通過years(),months(),weeks(),days(),hours(),minutes(),和seconds()這些函式建立period。

8895558-9ae3dc0728cf69c7.jpg
8895558-0683edf685faf3c9.jpg

這些函式名稱中不包含字母e/d,因為他們不是近似值。例如,month(2)總是表示兩個月的長度,即便2個月代表的總時間會跟隨具體開始時間變動。因此,我們無法精確計算一個period的秒數,直到我們知道它的開始時間。但是,我們仍然可以用period進行日期時間計算。當我們為instant時點加上或減去一個period,這個period就繫結在了instant上。這個instant告訴我們這個period的開始時間,這使得我們能夠以秒計算出精確長度。

換句話說,我們可以用period來精確地表述時鐘時間,而不用知道閏秒、閏天以及夏時制等是否發生。

8895558-9c1d801f562bc232.jpg

我們也可以用period()函式將interval轉換為period物件。

8895558-0d5464c050a6dbf5.jpg

period可以從 instant, interval, 和其它period中加上或減去,但不適用於duration。

總之,可以對四種型別的時間物件進行數學運算:instants, intervals, durations和periods。表4描述了哪些物件可以相互新增,以及產生的結果屬於什麼型別。

8895558-eabf8ce45c7b40c3.jpg

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

像數字一樣,日期時間也是按順序發生的。這代表日期時間是可以被近似的,lubridate提供了3種方法實現這個功能:round_date(),floor_date(),和ceiling_date()。每個函式的第一個引數代表要近似的物件,第二個引數代表要近似到的單位。例如,我們可以將2010年4月20日近似到最近的一天,或者最近的一個月。

8895558-0891648ccda659ff.jpg

注意,對日期時間物件進行近似時,近似的單位就成為原物件的最小單位,例如round_date(apri120,“day”)就會把天后面的小時,分鐘和秒等通通設定為00。

ceiling_date()提供了另外一種更簡單的辦法找到某月的最後一天。把日期定在它的下個月,然後減去一天。

8895558-a4b4cf8e025511f9.jpg

7.時區

不同的時區使時點有多個不同的名稱。例如,“2010-03-26 11:53:24 CDT”和“2010-03-26 12:53:24 EDT”都描述了相同的時點。前者是美國中央時區(CDT),後者是美國東部時區(EDT)。時區使日期時間資料複雜化,但對於將時鐘時間轉換為夏時制時非常有用。

instant的時間是世界協調時區(UTC),它與標準時鍾時間是一致的,這就節省了計算量,但如果你的計算機堅持將時間轉換為你當前的時區,可能會很煩人,並且在討論時間時也不方便。

lubridate用兩種方式減輕時區造成的差異。我們可以用with_tz()去改變時區,顯示同一個時間點在不同時區的時間,例如,

8895558-4e428b350d5a9b52.jpg

force_tz()與with_tz()相反:它改變了時點,但是時間的數值保持不變。例如下面,同樣的時間數值,由於時區不同,時間相差了6個小時。

8895558-a88385dd8073f773.jpg
8895558-1bf8a83010a6bf83.jpg

8.夏時制

在世界上許多地方,官方時間在春季撥快一小時,秋季撥慢一小時。例如,伊利諾斯州的芝加哥,在2010年3月14日凌晨2:00施行夏時制,改變發生前的時間是“2010-03-14 01:59:59 CST”,1秒鐘後時間就變為夏時制的凌晨3點(夏時制比標準時間快一小時)。

8895558-cbc32bbeb558106a.jpg

通過一秒的變化,我們似乎得到了額外的一小時,這就是夏時制的工作原理。我們可以用period代替duration來避免夏時制造成的時間變化。例如,

8895558-b7c3896cf4c77a1a.jpg

當我們使用period時,我們不必在意夏令制的變化,因為它不會影響我們的計算。新增一個duration則會顯示時鐘上的確切時間。

8895558-9275357513f7fead.jpg

如果我們嘗試在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的第一天開始計算。

8895558-d11700efe3327f11.jpg

我們可以在月份上加上10,或者直接將月份設定為11月。

8895558-4329286155aaa9a5.jpg

我們檢視一下11月1日是星期幾。

8895558-f1ce2910fbfdc0d8.jpg

這意味著11月4日將是十一月的第一個星期四。

8895558-fb33f7c412d1e8ec.jpg

接下來,我們增加三個星期到十一月的第四個星期四。

8895558-95310aeae79b07a3.jpg

9.2. Memorial Day

陣亡將士紀念日按照慣例,是在五月的最後一個星期一,為了計算陣亡將士紀念日的日期,我們可以從2010年的第一天開始。

8895558-1c56308e6c97816d.jpg

接下來,我們將月份設定為5月。

8895558-242372bd0523fed7.jpg

我們要計算的假期發生在月末而不是月初,我們通過使用ceiling_date()將月份近似到下一個月,再減去一天得到這個月的最後一天。

8895558-44abc39777946863.jpg

然後我們可以檢視5月31日是星期幾。正巧是星期一,所以我們得到結果了。如果5月31日非星期一,我們可以減去適當的天數來獲得五月的最後一個星期一。

8895558-feeab20685735ea4.jpg

10.範例2(不清楚籃球比賽的專業術語,翻譯可能有誤)

湖人資料集包含了洛杉磯湖人隊在2008-2009賽季,各大聯盟比賽的統計資料。此資料來自http://www.basketballgeek.com/downloads/ (Parker 2010),另lubridate包已內含該資料集。我們將探索湖人全年的比賽分佈以及湖人隊在比賽中的戰術分配情況。我們使用ggplot2。

湖人資料集用date來記錄每場比賽的日期。使用str()命令,我們看到R將日期識別為整數型。

8895558-f0cc982284c5373d.jpg

在使用date資料之前,我們用ymd()將整數型的日期轉化為R認可的型別。

8895558-050f1d4aded16723.jpg

現在date資料變為R中的POSIXct型別。我們可以用處理POSIXct型別資料的方式處理日期了。例如,如果我們繪製整個賽季中主場和客場比賽的次數,X軸為date。

8895558-ae29a7a99cb21e89.jpg

圖1顯示了整個賽季比賽不斷,但比賽之間會有短時相隔。賽季開始時,比賽的頻率看起來較低,而且比賽似乎被均勻分為主場和客場。X軸上的刻度和刻度間隔是由lubridate包中pretty.dates()自動生成的。

接下來我們看看湖人比賽次數的周分佈情況。我們用wday()命令獲取每個日期具體是星期幾。

8895558-4221a59125032d4d.jpg

籃球比賽次數週變化,如圖2。令人驚訝的是,星期二比賽次數最多。

8895558-360644d8d95e274c.jpg

現在讓我們看看每場比賽,特別是整個賽季的投球情況分佈。湖人隊資料集中的time列出了每場比賽中投籃、籃板、罰球等技術時,距離單節比賽結束的時間。比賽單節時長12分鐘,從12:00倒數計時至00:00,分號之前的數字代表比賽剩下的分鐘,後兩個數字代表剩下的秒數。

time僅包含分鐘和秒,無法確定唯一日期-時間,因此它不是R中的標準日期時間型別。我們用ms()函式將分鐘和秒儲存為5.4節中定義的period物件。

8895558-8425b6c6b5a33b0b.jpg

由於period僅有相對長度,不方便進行長度比較。所以我們下一步應該將period轉換為有確切長度的duration。

8895558-9dab7359c92f3d6a.jpg

現在我們可以直接比較不同duration了。由於沒有比賽的具體開始時間,我們不能通過開始時間加duration,來確定每場比賽中投籃、籃板、罰球等的確切時間。然而,我們仍然可以計算每場比賽中每個投籃、籃板、罰球等何時發生。每節比賽時間12分鐘,比賽開始時從12:00開始倒數計時。所以為了計算每個投籃、籃板、罰球等耗的時間,我們從12, 24, 36,或48分鐘(取決於是哪一節)扣除time上的時間,這將建立一個新的duration,記錄了每個投籃、籃板、罰球等距離開場的時間差。

8895558-0daa4ac00a64e0a2.jpg

資料集中觀察到某些比賽有加時,為了保持簡化,我們將忽略加時賽。

8895558-15443742b839fec0.jpg

ggplot2不支援duration採用的difftime型別資料,為了繪製我們的資料,我們可以從duration中提取整數數值,提取的數值與duration代表的時間差相等(圖3)。

8895558-ddefa659dd8f2ea4.jpg

或者也可以為duration加上一個相同的instant,建立一個ggplot2支援的日期時間型別。軸刻度由pretty.date()自動生成。pretty.date()可以建立出最直觀的日期時間資料標記,進一步增強了我們的圖(圖4)。

8895558-686ab3266711265a.jpg

投籃、籃板、罰球等的耗時在每節比賽中間比較多,在下節比賽的開始時耗時較少,如圖4。

8895558-bca21d1ba4ae3cf8.jpg

現在讓我們更仔細地看一場籃球賽:本賽季的第一場比賽。這場比賽是在2008年10月28日進行的。對於這場比賽,我們可以很容易地模擬每次投籃之間的時間。

8895558-5e051469106bc1c9.jpg

投籃之間的時間差是每次投籃之間的時間跨度,因為我們記錄了每次投籃的duration,通過兩個duration相減來記錄差異。這將自動建立一個新的duration,其長度等於前兩個duration時間之間的差異。

8895558-fbb5b4359c815646.jpg

我們在圖5中繪製此資訊。我們看到至少30秒內會有一次嘗試投籃,但有時60秒就都沒有嘗試。

8895558-c9d241b8536c6f25.jpg

我們也可以檢查比賽中比分的變化。如圖6所示,這表明本賽季的第一場比賽很順利:湖人隊整場比賽保持領先。注:plyr包中的下ddply()函式會使計算更簡單。

8895558-725f0e4676decb41.jpg
8895558-bb4ed5b501c362e4.jpg

11.結論

日期時間會造成其他資料型別不存在的技術困難。日期時間型別過多,具體地識別它們是很困難的,而且我們也很難訪問和操作過多的日期時間型別。我們經常會對日期時間型別進行數學運算,但我們必須小心,因此比起其他普通數值型資料,我們要遵循很多規則。最後,與日期相關的很多慣例,如夏時制和時區,也讓我們很難比較和識別不同的時間。Base R可以處理其中很多困難,但無法全部處理。而且,Base R中處理不同日期時間型別有不同的方法,而且會讓人感到非常複雜和混亂。

lubridate使得我們更容易處理R中的日期時間資料。Lubridate包提供了一系列處理常用日期時間的標準方法。這些方法使得解析、操作和計算日期時間物件變得簡單。通過引進基於java的Joda Time專案推出的時間概念,lubridate有助於研究人員進行精確的計算以及構建與時間有關的複雜過程模型。lubridate也方便進行時區切換,以及更方便利用或者忽略夏時制造成的時間差異。

未來,通過lubridate包我們可以處理不完整的日期,可以對重複發生事件進行建模,如股票市場開放時間,營業時間,或街道保潔時間。特別是,我們希望為R創造一種方法,可以對重複發生的日期模式進行處理。

致謝.略

參考.略

相關文章