《利用Python進行數據分析》第10章 時間序列、日期和時間數據類型筆記

時間序列

日期和時間數據類型及工具

Python標準庫包含用於日期(date)和時間(time)數據的數據類型,還有日曆方面的功能。主要會用到datetime、time以及calendar模塊。datetime.datetime(也可以簡寫爲datetime)是用得最多的數據類型

from pandas import  Series,DataFrame
import pandas as pd
import numpy as np
from datetime import datetime
now=datetime.now()
now
datetime.datetime(2017, 12, 27, 10, 25, 45, 574261)
now.year,now.month,now.day
(2017, 12, 26)

datetime以毫秒形式存儲日期和時間。

datetime.timedelta表示兩個datetime對象之間的時間差

delta=datetime(2017,12,1)-datetime(2012,9,8,8,15)
delta
datetime.timedelta(1909, 56700)
delta.days    #日的時間差
1909
delta.seconds  #秒的時間差
56700

可以給datetime對象加上(或減去)一個或多個timedelta,這樣會產生一個新對象

from datetime import timedelta
start=datetime(2017,6,13)
start+timedelta(12)
datetime.datetime(2017, 6, 25, 0, 0)
start-2*timedelta(12)
datetime.datetime(2017, 5, 20, 0, 0)

datetime模塊中的數據類型參見表10-1

字符串和datetime的相互轉換

利用str或strftime方法(傳入一個格式化字符串),datetime對象和pandas的Timestamp對象可以被格式化爲字符串

stamp=datetime(2017,12,26)
str(stamp)
'2017-12-26 00:00:00'
stamp.strftime('%Y-%m-%d')
'2017-12-26'

datetime.strptime可以用表中的這些格式化編碼將字符串轉換爲日期

value='2017-12-26'
datetime.strptime(value,'%Y-%m-%d')
datetime.datetime(2017, 12, 26, 0, 0)
datestrs=['11/12/2017','12/12/2017']
[datetime.strptime(x,'%m/%d/%Y') for x in datestrs]
[datetime.datetime(2017, 11, 12, 0, 0), datetime.datetime(2017, 12, 12, 0, 0)]

datetime.strptime是通過已知格式進行日期解析的最佳方式。d但是對於其他常見的日期格式,需要使用dateutil這個第三方包中的parser.parse方法

from dateutil.parser import parse
parse('2017-12-06')
datetime.datetime(2017, 12, 6, 0, 0)

dateutil可以解析幾乎所有人類能夠理解的日期表示形式

parse('Jan 31,2017 9:35 PM')
datetime.datetime(2017, 1, 31, 21, 35)

在國際通用的格式中,日通常出現在月的前面,傳入dayfirst=True即可解決

parse('10/12/2017',dayfirst=True)
datetime.datetime(2017, 12, 10, 0, 0)

pandas通常是用於處理成組日期的,不管這些日期是DataFrame的軸索引還是列。to_datetime方法可以解析多種不同的日期表示形式。對標準日期格式(如ISO8601)的解析非常快。可以處理缺失值(None、空字符串等)

datestrs
['11/12/2017', '12/12/2017']
pd.to_datetime(datestrs)
DatetimeIndex(['2017-11-12', '2017-12-12'], dtype='datetime64[ns]', freq=None)
idx=pd.to_datetime(datestrs+[None])
idx
DatetimeIndex(['2017-11-12', '2017-12-12', 'NaT'], dtype='datetime64[ns]', freq=None)
idx[2]
NaT
idx[1]
Timestamp('2017-12-12 00:00:00')
pd.isnull(idx)
array([False, False,  True], dtype=bool)

NaT(Not a Time)是pandas中時間戳數據的NA值

警告: dateutil.parser是一個實用但不完美的工具。比如說,它會把一些原本不是日期的字符串認作是日期(比如”42”會被解析爲2042年的今天)

時間序列基礎

pandas最基本的時間序列類型就是以時間戳(通常以Python字符串或datatime對象表示)爲索引的Series

from datetime import datetime
dates=[datetime(2017,1,6),datetime(2017,1,10),datetime(2017,1,13),
      datetime(2017,1,15),datetime(2017,1,18),datetime(2017,1,20)]
ts=Series(np.random.randn(6),index=dates)
ts
2017-01-06    0.005613
2017-01-10    1.222545
2017-01-13    0.000879
2017-01-15    0.172794
2017-01-18   -0.530976
2017-01-20   -0.808555
dtype: float64

可以看出datetime對象是被放在一個DatetimeIndex中,變量ts就成爲一個TimeSeries

type(ts)
pandas.core.series.Series
ts.index
DatetimeIndex(['2017-01-06', '2017-01-10', '2017-01-13', '2017-01-15',
               '2017-01-18', '2017-01-20'],
              dtype='datetime64[ns]', freq=None)

注意: 沒必要顯式使用TimeSeries的構造函數。當創建一個帶有DatetimeIndex的Series時,pandas就會知道該對象是一個時間序列。

ts[::2]
2017-01-06    0.170305
2017-01-13   -0.798955
2017-01-18   -0.367438
dtype: float64

跟其他Series一樣,不同索引的時間序列之間的算術運算會自動按日期對齊

ts+ts[::2]
2017-01-06    0.340610
2017-01-10         NaN
2017-01-13   -1.597911
2017-01-15         NaN
2017-01-18   -0.734876
2017-01-20         NaN
dtype: float64

pandas用NumPy的datetime64數據類型以納秒形式存儲時間戳

ts.index.dtype
dtype('<M8[ns]')

DatetimeIndex中的各個標量值是pandas的Timestamp對象

stamp=ts.index[0]
stamp
Timestamp('2017-01-06 00:00:00')
ts.index[3]
Timestamp('2017-01-15 00:00:00')

如果有需要,TimeStamp可以隨時自動轉換爲datetime對象

索引、選取、子集構造

由於TimeSeries是Series的一個子類,所以在索引以及數據選取方面它們的行爲是一樣的

stamp=ts.index[2]
stamp
Timestamp('2017-01-13 00:00:00')
ts[stamp]
-0.79895542662575525

可以傳入一個可以被解釋爲日期的字符串

ts['1/10/2017']
1.3762403458229933
ts['20170118']
-0.36743784149263731

對於較長的時間序列,只需傳入“年”或“年月”即可輕鬆選取數據的切片

longer_ts=Series(np.random.randn(1000),
                 index=pd.date_range('11/25/2017',periods=1000))
longer_ts
2017-11-25   -0.659067
2017-11-26   -1.539291
2017-11-27   -1.731498
2017-11-28   -0.560947
      ...      ...   
2020-08-17   -0.499714
2020-08-18   -1.978626
2020-08-19    1.757046
2020-08-20   -0.401384
Freq: D, Length: 1000, dtype: float64
longer_ts['2017']
2017-11-25   -0.659067
2017-11-26   -1.539291
2017-11-27   -1.731498
2017-11-28   -0.560947
2017-11-29   -2.086909
        ...      ...   
2017-12-28    0.123651
2017-12-29   -0.188641
2017-12-30    1.206445
2017-12-31   -1.734814
Freq: D, dtype: float64
longer_ts['2017-11']
2017-11-25   -0.659067
2017-11-26   -1.539291
2017-11-27   -1.731498
2017-11-28   -0.560947
2017-11-29   -2.086909
2017-11-30   -0.673677
Freq: D, dtype: float64

通過日期進行切片的方式只對規則Series有效。

ts
2017-01-06    0.170305
2017-01-10    1.376240
2017-01-13   -0.798955
2017-01-15   -0.496169
2017-01-18   -0.367438
2017-01-20    0.059816
dtype: float64
ts[datetime(2017,1,13):]
2017-01-13   -0.798955
2017-01-15   -0.496169
2017-01-18   -0.367438
2017-01-20    0.059816
dtype: float64

由於大部分時間序列數據都是按照時間先後排序的,因此你也可以用不存在於該時間序列中的時間戳對其進行切片(即範圍查詢

ts['1/12/2017':'1/17/2017']
2017-01-13   -0.798955
2017-01-15   -0.496169
dtype: float64

這裏可以傳入字符串日期、datetime或Timestamp。注意,這樣切片所產生的是源時間序列的視圖,跟NumPy數組的切片運算是一樣的。此外,還有一個等價的實例方法也可以截取兩個日期之間TimeSeries

ts.truncate(after='1/14/2017')
2017-01-06    0.170305
2017-01-10    1.376240
2017-01-13   -0.798955
dtype: float64

這些操作對DataFrame也有效

dates=pd.date_range('1/1/2017',periods=100,freq='W-WED')
long_df=DataFrame(np.random.randn(100,4),index=dates,
                 columns=['Colorado','Texas','New York','Ohio'])
long_df.ix['5-2017']

帶有重複索引的時間序列

在某些應用場景中,可能會存在多個觀測數據落在同一個時間點上的情況

dates=pd.DatetimeIndex(['1/1/2017','1/2/2017','1/2/2017','1/2/2017','1/3/2017'])
dup_ts=Series(np.arange(5),index=dates)
dup_ts
2017-01-01    0
2017-01-02    1
2017-01-02    2
2017-01-02    3
2017-01-03    4
dtype: int32

通過檢查索引的is_unique屬性,我們就可以知道它是不是唯一的

dup_ts.index.is_unique
False

對這個時間序列進行索引,要麼產生標量值,要麼產生切片,具體要看所選的時間點是否重複

dup_ts['1/3/2017'] #不重複
4
dup_ts['1/2/2017']  #重複
2017-01-02    1
2017-01-02    2
2017-01-02    3
dtype: int32

假設你想要對具有非唯一時間戳的數據進行聚合。一個辦法是使用groupby,並傳入level=0(索引的唯一一層!)

grouped=dup_ts.groupby(level=0)
grouped.mean()
2017-01-01    0
2017-01-02    2
2017-01-03    4
dtype: int32
grouped.count()
2017-01-01    1
2017-01-02    3
2017-01-03    1
dtype: int64

日期的範圍、頻率以及移動

pandas有一整套標準時間序列頻率以及用於重採樣、頻率推斷、生成固定頻率日期範圍的工具。例如,我們可以將之前那個時間序列轉換爲一個具有固定頻率(每日)的時間序列,只需調用resample

ts
2017-01-06    0.005613
2017-01-10    1.222545
2017-01-13    0.000879
2017-01-15    0.172794
2017-01-18   -0.530976
2017-01-20   -0.808555
dtype: float64
ts.resample('D')
DatetimeIndexResampler [freq=<Day>, axis=0, closed=left, label=left, convention=start, base=0]

生成日期範圍

pandas.date_range可用於生成指定長度的DatetimeIndex

index=pd.date_range('6/10/2017','7/1/2017')
index
DatetimeIndex(['2017-06-10', '2017-06-11', '2017-06-12', '2017-06-13',
               '2017-06-14', '2017-06-15', '2017-06-16', '2017-06-17',
               '2017-06-18', '2017-06-19', '2017-06-20', '2017-06-21',
               '2017-06-22', '2017-06-23', '2017-06-24', '2017-06-25',
               '2017-06-26', '2017-06-27', '2017-06-28', '2017-06-29',
               '2017-06-30', '2017-07-01'],
              dtype='datetime64[ns]', freq='D')

默認情況下,date_range會產生按天計算的時間點。如果只傳入起始或結束日期,那就還得傳入一個表示一段時間的數字

pd.date_range(start='12/8/2017',periods=20)
DatetimeIndex(['2017-12-08', '2017-12-09', '2017-12-10', '2017-12-11',
               '2017-12-12', '2017-12-13', '2017-12-14', '2017-12-15',
               '2017-12-16', '2017-12-17', '2017-12-18', '2017-12-19',
               '2017-12-20', '2017-12-21', '2017-12-22', '2017-12-23',
               '2017-12-24', '2017-12-25', '2017-12-26', '2017-12-27'],
              dtype='datetime64[ns]', freq='D')

起始和結束日期定義了日期索引的嚴格邊界。例如,如果你想要生成一個由每月最後一個工作日組成的日期索引,可以傳入”BM”頻率(表示business end of month),這樣就只會包含時間間隔內(或剛好在邊界上的)符合頻率要求的日期

pd.date_range('1/1/2017','12/1/2017',freq='BM')
DatetimeIndex(['2017-01-31', '2017-02-28', '2017-03-31', '2017-04-28',
               '2017-05-31', '2017-06-30', '2017-07-31', '2017-08-31',
               '2017-09-29', '2017-10-31', '2017-11-30'],
              dtype='datetime64[ns]', freq='BM')

date_range默認會保留起始和結束時間戳的時間信息(如果有的話)

pd.date_range('5/6/2017 12:30:20',periods=5)
DatetimeIndex(['2017-05-06 12:30:20', '2017-05-07 12:30:20',
               '2017-05-08 12:30:20', '2017-05-09 12:30:20',
               '2017-05-10 12:30:20'],
              dtype='datetime64[ns]', freq='D')

有時,雖然起始和結束日期帶有時間信息,但你希望產生一組被規範化(normalize)到午夜的時間戳。normalize選項即可實現該功能

pd.date_range('12/15/2017 12:50:33',periods=5,normalize=True)
DatetimeIndex(['2017-12-15', '2017-12-16', '2017-12-17', '2017-12-18',
               '2017-12-19'],
              dtype='datetime64[ns]', freq='D')

頻率和日期偏移量

pandas中的頻率是由一個基礎頻率(base frequency)和一個乘數組成的。基礎頻率通常以一個字符串別名表示,比如”M”表示每月,”H”表示每小時。對於每個基礎頻率,都有一個被稱爲日期偏移量(date offset)的對象與之對應。例如,按小時計算的頻率可以用Hour類表示

from pandas.tseries.offsets import Hour,Minute
hour=Hour()
hour
<Hour>

傳入一個整數即可定義偏移量的倍數

four_fours=Hour(4)
four_fours
<4 * Hours>

無需顯式創建這樣的對象,只需使用諸如”H”或”4H”這樣的字符串別名即可。在基礎頻率前面放上一個整數即可創建倍數

pd.date_range('1/1/2017','1/3/2017 23:59',freq='4H')
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 04:00:00',
               '2017-01-01 08:00:00', '2017-01-01 12:00:00',
               '2017-01-01 16:00:00', '2017-01-01 20:00:00',
               '2017-01-02 00:00:00', '2017-01-02 04:00:00',
               '2017-01-02 08:00:00', '2017-01-02 12:00:00',
               '2017-01-02 16:00:00', '2017-01-02 20:00:00',
               '2017-01-03 00:00:00', '2017-01-03 04:00:00',
               '2017-01-03 08:00:00', '2017-01-03 12:00:00',
               '2017-01-03 16:00:00', '2017-01-03 20:00:00'],
              dtype='datetime64[ns]', freq='4H')

大部分偏移量對象都可通過加法進行連接;你也可以傳入頻率字符串(如”2h30min”),這種字符串可以被高效地解析爲等效的表達式

Hour(3)+Minute(30)
<210 * Minutes>
pd.date_range('1/1/2017',periods=10,freq='1h30min')
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:30:00',
               '2017-01-01 03:00:00', '2017-01-01 04:30:00',
               '2017-01-01 06:00:00', '2017-01-01 07:30:00',
               '2017-01-01 09:00:00', '2017-01-01 10:30:00',
               '2017-01-01 12:00:00', '2017-01-01 13:30:00'],
              dtype='datetime64[ns]', freq='90T')

有些頻率所描述的時間點並不是均勻分隔的。例如,”M”(日曆月末)和”BM”(每月最後一個工作日)就取決於每月的天數,對於後者,還要考慮月末是不是週末。由於沒有更好的術語,我將這些稱爲錨點偏移量(anchored offset)。

表10-4列出了pandas中的頻率代碼和日期偏移量類。

WOM日期

WOM(Week Of Month)是一種非常實用的頻率類,它以WOM開頭。它使你能獲得諸如“每月第3個星期五”之類的日期

rng=pd.date_range('1/1/2017','9/1/2017',freq='WOM-3FRI')
list(rng)
[Timestamp('2017-01-20 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-02-17 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-03-17 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-04-21 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-05-19 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-06-16 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-07-21 00:00:00', freq='WOM-3FRI'),
 Timestamp('2017-08-18 00:00:00', freq='WOM-3FRI')]

美國的股票期權交易人會意識到這些日子就是標準的月度到期日

移動(超前和滯後)數據

移動(shifting)指的是沿着時間軸將數據前移或後移。Series和DataFrame都有一個shift方法用於執行單純的前移或後移操作,保持索引不變

ts=Series(np.random.randn(4),
         index=pd.date_range('1/1/2017',periods=4,freq='M'))
ts
2017-01-31   -1.555270
2017-02-28    1.325946
2017-03-31   -1.458090
2017-04-30    1.064776
Freq: M, dtype: float64
ts.shift(2)
2017-01-31         NaN
2017-02-28         NaN
2017-03-31   -1.555270
2017-04-30    1.325946
Freq: M, dtype: float64
ts.shift(-2)
2017-01-31   -1.458090
2017-02-28    1.064776
2017-03-31         NaN
2017-04-30         NaN
Freq: M, dtype: float64

shift通常用於計算一個時間序列或多個時間序列(如DataFrame的列)中的百分比變化。可以這樣表達:

ts / ts.shift(1) - 1

由於單純的移位操作不會修改索引,所以部分數據會被丟棄。因此,如果頻率已知,則可以將其傳給shift以便實現對時間戳進行位移而不是對數據進行簡單位移

ts.shift(2,freq='M')
2017-03-31   -1.555270
2017-04-30    1.325946
2017-05-31   -1.458090
2017-06-30    1.064776
Freq: M, dtype: float64

還可以使用其他頻率,於是你就能非常靈活地對數據進行超前和滯後處理了

ts.shift(3,freq='D')
2017-02-03   -1.555270
2017-03-03    1.325946
2017-04-03   -1.458090
2017-05-03    1.064776
dtype: float64
ts.shift(1,freq='3D')
2017-02-03   -1.555270
2017-03-03    1.325946
2017-04-03   -1.458090
2017-05-03    1.064776
dtype: float64
ts.shift(-3,freq='D')
2017-01-28   -1.555270
2017-02-25    1.325946
2017-03-28   -1.458090
2017-04-27    1.064776
dtype: float64

通過偏移量對日期進行位移

pandas的日期偏移量還可以用在datetime或Timestamp對象上

from pandas.tseries.offsets import Day,MonthEnd
now=datetime(2017,12,27)
now+3*Day()
Timestamp('2017-12-30 00:00:00')

如果加的是錨點偏移量(比如MonthEnd),第一次增量會將原日期向前滾動到符合頻率規則的下一個日期

now+MonthEnd()
Timestamp('2017-12-31 00:00:00')
now+MonthEnd(3)
Timestamp('2018-02-28 00:00:00')

通過錨點偏移量的rollforward和rollback方法,可顯式地將日期向前或向後“滾動”

offset=MonthEnd()
offset.rollforward(now)
Timestamp('2017-12-31 00:00:00')
offset.rollback(now)
Timestamp('2017-11-30 00:00:00')

日期偏移量還有一個巧妙的用法,即結合groupby使用這兩個“滾動”方法

ts=Series(np.random.randn(10),
         index=pd.date_range('1/15/2017',periods=10,freq='4d'))
ts
2017-01-15    1.060753
2017-01-19    0.410493
2017-01-23    2.188356
2017-01-27   -0.335335
2017-01-31   -1.976843
2017-02-04    1.644975
2017-02-08    0.243292
2017-02-12   -0.444232
2017-02-16   -0.665907
2017-02-20   -0.771520
Freq: 4D, dtype: float64
ts.groupby(offset.rollforward).mean()
2017-01-31    0.269485
2017-02-28    0.001322
dtype: float64

更簡單、更快速地實現該功能的辦法是使用resample

ts.resample('M',how='mean')
2017-01-31    0.269485
2017-02-28    0.001322
Freq: M, dtype: float64

本章的時間序列知識點比較多,需要分階段練習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章