Python 時間序列分析

weixin_34233421發表於2018-07-08

時間序列簡介
時間序列分析是資料分析過程中,尤其是在金融資料分析過程中會經常遇到的。時間序列,就是以時間排序的一組隨機變數,例如國家統計局每年或每月定期釋出的 GDP 或 CPI 指數;24 小時內某一股票、基金、指數的數值變化等,都是時間序列。

下圖擷取自雅虎財經網站,它就是納斯達克指數某一天內數值變化(時間序列)的視覺化結果。

8484713-916c0b117f28458d.png
image.png

時間序列處理

我們拿到一些時間序列原始資料時,可能會遇到下面的一些情況:

某一段時間缺失,需要填充。
時間序列錯位,需要對齊。
資料表 a 和資料表 b 所採用的時間間隔不一致,需要重新取樣。
……

面對這些問題,我們就要通過一些處理手段來獲得最終想要的資料。本節課程中,我們會繼續用到 Pandas 提供的時間序列處理模組,下面先看一些基本的方法和操作。

目前,Pandas 針對時間序列處理的類和方法如下:


8484713-058fc1e4d667fad1.png
image.png

我們按照順序來看一看這些方法可以做什麼。

Timestamp 時間戳

時間戳,即代表一個時間時刻。我們可以直接用 pd.Timestamp()來建立時間戳。我們使用 ipython 演示,在重點中通過 anaconda/bin/ipython 開啟。(小提示:使用 ipython 時,可以通過 Tab 鍵完成程式碼自動補全。)

In [1]: import pandas as pd

In [2]: pd.Timestamp("2017-1-1")
Out[2]: Timestamp('2017-01-01 00:00:00')

In [3]: pd.Timestamp(2017,10,1)
Out[3]: Timestamp('2017-10-01 00:00:00')

In [4]: pd.Timestamp("2017-1-1 12:59:59")
Out[4]: Timestamp('2017-01-01 12:59:59')

時間戳索引

我們可以看到,單個時間戳為 Timestamp 資料,而時間戳以列表形式存在時,Pandas 將強制轉換為 DatetimeIndex。此時,我們就不能再使用 pd.Timestamp()來建立時間戳了,而是 pd.to_datetime()來建立:

In [6]: pd.to_datetime(["2017-1-1","2017-1-2","2017-1-3"])
Out[6]: DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'], dtype='datetime64[ns]', freq=None)

注意輸出部分和上面的區別。

pd.to_datetime() 不僅僅可用來建立 DatetimeIndex,它還可以將對時間戳序列格式進行轉換等操作。例如下面,常見的時間戳書寫樣式,都可以通過pd.to_datetime() 規範化。

In [7]: pd.to_datetime(['Jul 1, 2017', '2017-10-10', None])
Out[7]: DatetimeIndex(['2017-07-01', '2017-10-10', 'NaT'], dtype='datetime64[ns]', freq=None)

In [8]: pd.to_datetime(['2017/10/1', '2017.1.31'])
Out[8]: DatetimeIndex(['2017-10-01', '2017-01-31'], dtype='datetime64[ns]', freq=None)

對於歐洲時區普遍採用的書寫樣式,我們還可以通過 dayfirst=True 引數進行修正:

In [11]: pd.to_datetime('1-10-2017')
Out[11]: Timestamp('2017-01-10 00:00:00')

In [12]: pd.to_datetime('1-10-2017', dayfirst=True)
Out[12]: Timestamp('2017-10-01 00:00:00')

當然,Pandas 所熟悉的 Seris 和 DataFrame 格式的字串,也可以直接通過 to_datetime 轉換:

In [15]: pd.to_datetime(pd.Series(['2017-1-1', '2017-1-2', '2017-1-3']))
Out[15]:
0   2017-01-01
1   2017-01-02
2   2017-01-03
dtype: datetime64[ns]

In [16]: pd.to_datetime(['2017-1-1', '2017-1-2', '2017-1-3'])
In [16]: DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'], dtype='datetime64[ns]', freq=None)

In [17]: pd.to_datetime(pd.DataFrame({'year': [2017, 2017], 'month': [1, 2], 'day': [3, 4], 'hour': [5, 6]}))
Out[17]:
0   2017-01-03 05:00:00
1   2017-02-04 06:00:00
dtype: datetime64[ns]

其中:

pd.to_datetime(Series/DataFrame)返回的是Series。
pd.to_datetime(List)返回的是DatetimeIndex。

如果要轉換如上所示的DataFrame,必須存在的列名有year,month,day。另外 hour, minute, second, millisecond, microsecond, nanosecond可選。

當我們在使用pd.to_datetime() 轉換資料時,很容易遇到無效資料。有一些任務對無效資料非常苛刻,所以報錯讓我們找到這些無效資料是不錯的方法。當然,也有一些任務不在乎零星的無效資料,這時候就可以選擇忽略。

遇到無效資料包錯

In [17]: pd.to_datetime(['2017-1-1', 'invalid'], errors='raise')
ValueError: Unknown string format

忽略無效資料

In [18]: pd.to_datetime(['2017-1-1', 'invalid'], errors='ignore')
Out[18]: array(['2017-1-1', 'invalid'], dtype=object)

將無效資料顯示為 NaT

In [19]: pd.to_datetime(['2017-1-1', 'invalid'], errors='coerce')
Out[19]: DatetimeIndex(['2017-01-01', 'NaT'], dtype='datetime64[ns]', freq=None)

接下來,我們看一看生成 DatetimeIndex 的另一個重要方法 pandas.data_range。你應該可以從名字看出該方法的作用,我們可以通過指定一個規則,讓 pandas.data_range 生成有序的 DatetimeIndex。

pandas.data_range 方法帶有的預設引數如下:

pandas.date_range(start=None, end=None, periods=None, freq=’D’, tz=None, normalize=False,
name=None, closed=None, **kwargs)

常用引數的含義如下:

start= :設定起始時間
end=:設定截至時間
periods= :設定時間區間,若 None 則需要設定單獨設定起止和截至時間。
freq= :設定間隔週期。
tz=:設定時區。
其中,freq= 引數是非常關鍵的引數,我們可以設定的週期有:

freq='s': 秒
freq='min' : 分鐘
freq='H': 小時
freq='D': 天
freq='w': 周
freq='m': 月
freq='BM': 每個月最後一天
freq='W':每週的星期日

# 從 2017-1-1 到 2017-1-2,以小時間隔
In [21]: pd.date_range('2017-1-1','2017-1-2',freq='H')
Out[21]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:00:00',
               '2017-01-01 02:00:00', '2017-01-01 03:00:00',
               '2017-01-01 04:00:00', '2017-01-01 05:00:00',
               '2017-01-01 06:00:00', '2017-01-01 07:00:00',
               '2017-01-01 08:00:00', '2017-01-01 09:00:00',
               '2017-01-01 10:00:00', '2017-01-01 11:00:00',
               '2017-01-01 12:00:00', '2017-01-01 13:00:00',
               '2017-01-01 14:00:00', '2017-01-01 15:00:00',
               '2017-01-01 16:00:00', '2017-01-01 17:00:00',
               '2017-01-01 18:00:00', '2017-01-01 19:00:00',
               '2017-01-01 20:00:00', '2017-01-01 21:00:00',
               '2017-01-01 22:00:00', '2017-01-01 23:00:00',
               '2017-01-02 00:00:00'],
              dtype='datetime64[ns]', freq='H')

# 從 2017-1-1 開始,以 1s 為間隔,向後推 10 次
In [23]: pd.date_range('2017-1-1',periods=10,freq='s')
Out[23]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 00:00:01',
               '2017-01-01 00:00:02', '2017-01-01 00:00:03',
               '2017-01-01 00:00:04', '2017-01-01 00:00:05',
               '2017-01-01 00:00:06', '2017-01-01 00:00:07',
               '2017-01-01 00:00:08', '2017-01-01 00:00:09'],
              dtype='datetime64[ns]', freq='S')

# 從 2017-1-1 開始,以 1H20min 為間隔,向後推 10 次
In [24]: pd.date_range('1/1/2017', periods=10, freq='1H20min')
Out[24]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-01 01:20:00',
               '2017-01-01 02:40:00', '2017-01-01 04:00:00',
               '2017-01-01 05:20:00', '2017-01-01 06:40:00',
               '2017-01-01 08:00:00', '2017-01-01 09:20:00',
               '2017-01-01 10:40:00', '2017-01-01 12:00:00'],
              dtype='datetime64[ns]', freq='80T')

除了生成 DatetimeIndex,我們還可以對已有的 DatetimeIndex 進行操作。這些操作包括選擇、切片等。類似於對 Series 的操作。

In [31]: a = pd.date_range('2017-1-1',periods=10,freq='1D1H')

In [32]: a
Out[32]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-02 01:00:00',
               '2017-01-03 02:00:00', '2017-01-04 03:00:00',
               '2017-01-05 04:00:00', '2017-01-06 05:00:00',
               '2017-01-07 06:00:00', '2017-01-08 07:00:00',
               '2017-01-09 08:00:00', '2017-01-10 09:00:00'],
              dtype='datetime64[ns]', freq='25H')

# 選取索引為 1 的時間戳
In [33]: a[1]
Out[33]: Timestamp('2017-01-02 01:00:00', freq='25H')

# 對索引從 0 到 4 的時間進行切片
In [34]: a[:5]
Out[34]:
DatetimeIndex(['2017-01-01 00:00:00', '2017-01-02 01:00:00',
               '2017-01-03 02:00:00', '2017-01-04 03:00:00',
               '2017-01-05 04:00:00'],
              dtype='datetime64[ns]', freq='25H')

時序資料檢索

DatetimeIndex 之所以稱之為時間戳索引,當然是它的主要用途是作為 Series 或者 DataFrame 的索引。下面,我們就隨機生成一些資料,然後看一看如果對時間序列資料進行操作。

In [1]: import numpy as np

In [2]: import pandas as pd

# 生成時間索引
In [3]: i = pd.date_range('2017-1-1', periods=20, freq='M')

# 生成隨機資料並新增時間作為索引
In [4]: data = pd.Series(np.random.randn(len(i)), index = i)

# 檢視資料
In [5]: data
Out[5]:
2017-01-31   -1.233579
2017-02-28    0.494723
2017-03-31   -2.160592
2017-04-30    0.517173
2017-05-31   -1.984762
2017-06-30    0.655989
2017-07-31    0.919411
2017-08-31    0.114805
2017-09-30   -0.080374
2017-10-31    1.360448
2017-11-30   -0.417094
2017-12-31    0.555434
2018-01-31    1.659271
2018-02-28   -0.514907
2018-03-31    0.330979
2018-04-30   -0.707362
2018-05-31   -0.724524
2018-06-30    0.362518
2018-07-31    0.157280
2018-08-31   -0.724665
Freq: M, dtype: float64

上面就生成了一個以時間為所以的 Series 序列。其實,這就回到了對 Pandas 中 Series 和 DataFrame 型別資料操作的問題。下面演示一些操作:

# 檢索 2017 年的所有資料
In [12]: data['2017']
Out[12]:
2017-01-31   -1.233579
2017-02-28    0.494723
2017-03-31   -2.160592
2017-04-30    0.517173
2017-05-31   -1.984762
2017-06-30    0.655989
2017-07-31    0.919411
2017-08-31    0.114805
2017-09-30   -0.080374
2017-10-31    1.360448
2017-11-30   -0.417094
2017-12-31    0.555434
Freq: M, dtype: float64

# 檢索 2017 年 7 月到 2018 年 3 月之間的所有資料
In [13]: data['2017-07':'2018-03']
Out[13]:
2017-07-31    0.919411
2017-08-31    0.114805
2017-09-30   -0.080374
2017-10-31    1.360448
2017-11-30   -0.417094
2017-12-31    0.555434
2018-01-31    1.659271
2018-02-28   -0.514907
2018-03-31    0.330979
Freq: M, dtype: float64

# 使用 loc 方法檢索 2017 年 1 月的所有資料
In [14]: data.loc['2017-01']
Out[14]:
2017-01-31   -1.233579
Freq: M, dtype: float64

# 使用 truncate 方法檢索 2017-3-1 到 2018-4-2 期間的資料
In [17]: data.truncate(before='2017-3-1',after='2018-4-2')
Out[17]:
2017-03-31   -2.160592
2017-04-30    0.517173
2017-05-31   -1.984762
2017-06-30    0.655989
2017-07-31    0.919411
2017-08-31    0.114805
2017-09-30   -0.080374
2017-10-31    1.360448
2017-11-30   -0.417094
2017-12-31    0.555434
2018-01-31    1.659271
2018-02-28   -0.514907
2018-03-31    0.330979
Freq: M, dtype: float64

時序資料偏移

對於時序資料的處理,肯定不只是查詢和切片這麼簡單。我們這裡可能會用到 Shifting 方法,將時間索引進行整體偏移。

In [1]: import numpy as np

In [2]: import pandas as pd

# 生成時間索引
In [3]: i = pd.date_range('2017-1-1', periods=5, freq='M')

# 生成隨機資料並新增時間作為索引
In [4]: data = pd.Series(np.random.randn(len(i)), index = i)

# 檢視資料
In [5]: data
Out[5]:
2017-01-31    0.830480
2017-02-28    0.348324
2017-03-31   -0.622078
2017-04-30   -1.192675
2017-05-31    0.441947
Freq: M, dtype: float64

# 將索引向前位移 3 個單位,也就是資料向後位移 3 個單位,缺失資料 Pandas 會用 NaN 自動填充
In [8]: data.shift(3)
Out[8]:
2017-01-31         NaN
2017-02-28         NaN
2017-03-31         NaN
2017-04-30    0.830480
2017-05-31    0.348324
Freq: M, dtype: float64

# 將索引向後位移 3 個單位,也就是資料向前位移 3 個單位
In [9]: data.shift(-3)
Out[9]:
2017-01-31   -1.192675
2017-02-28    0.441947
2017-03-31         NaN
2017-04-30         NaN
2017-05-31         NaN
Freq: M, dtype: float64

# 將索引的時間向後移動 3 天
In [10]: data.shift(3,freq='D')
Out[10]:
2017-02-03    0.830480
2017-03-03    0.348324
2017-04-03   -0.622078
2017-05-03   -1.192675
2017-06-03    0.441947
dtype: float64

時序資料重取樣

除了 Shifting 方法,重取樣 Resample 也會經常用到。Resample 可以提升或降低一個時間索引序列的頻率,大有用處。例如:當時間序列資料量非常大時,我們可以通過低頻率取樣的方法得到規模較小到時間覆蓋依然較為全面的新資料集。另外,對於多個不同頻率的資料集需要資料對齊時,重取樣可以十分重要的手段。

In [1]: import pandas as pd

In [2]: import numpy as np

In [3]: i = pd.date_range('2017-1-1', periods=20, freq='D')

In [4]: data = pd.Series(np.random.randn(len(i)), index = i)

In [5]: data
Out[5]:
2017-01-01    0.384984
2017-01-02    0.341555
2017-01-03   -0.100246
2017-01-04   -0.660066
2017-01-05    0.007575
2017-01-06    2.402068
2017-01-07   -0.365657
2017-01-08   -0.853025
2017-01-09    0.588139
2017-01-10    0.047322
2017-01-11    0.213384
2017-01-12    1.056038
2017-01-13   -1.588518
2017-01-14    0.076655
2017-01-15    1.467056
2017-01-16   -1.877541
2017-01-17    0.003218
2017-01-18   -0.811914
2017-01-19    0.143571
2017-01-20    0.837088
Freq: D, dtype: float64

# 按照 2 天進行降取樣,並對 2 天對應的資料求和作為新資料
In [6]: data.resample('2D').sum()
Out[6]:
2017-01-01    0.726539
2017-01-03   -0.760312
2017-01-05    2.409643
2017-01-07   -1.218682
2017-01-09    0.635461
2017-01-11    1.269422
2017-01-13   -1.511864
2017-01-15   -0.410485
2017-01-17   -0.808696
2017-01-19    0.980658
Freq: 2D, dtype: float64

# 按照 2 天進行降取樣,並對 2 天對應的資料求平均值作為新資料
In [7]: data.resample('2D').mean()
Out[7]:
2017-01-01    0.363269
2017-01-03   -0.380156
2017-01-05    1.204821
2017-01-07   -0.609341
2017-01-09    0.317730
2017-01-11    0.634711
2017-01-13   -0.755932
2017-01-15   -0.205243
2017-01-17   -0.404348
2017-01-19    0.490329
Freq: 2D, dtype: float64

# 按照 2 天進行降取樣,並選取對應 2 天的最大值作為新資料
In [9]: data.resample('2D').max()
Out[9]:
2017-01-01    0.384984
2017-01-03   -0.100246
2017-01-05    2.402068
2017-01-07   -0.365657
2017-01-09    0.588139
2017-01-11    1.056038
2017-01-13    0.076655
2017-01-15    1.467056
2017-01-17    0.003218
2017-01-19    0.837088
Freq: 2D, dtype: float64

# 按照 2 天進行降取樣,並將對應 2 天資料的原值、最大值、最小值、以及臨近值列出
In [10]: data.resample('2D').ohlc()
Out[10]:
                open      high       low     close
2017-01-01  0.384984  0.384984  0.341555  0.341555
2017-01-03 -0.100246 -0.100246 -0.660066 -0.660066
2017-01-05  0.007575  2.402068  0.007575  2.402068
2017-01-07 -0.365657 -0.365657 -0.853025 -0.853025
2017-01-09  0.588139  0.588139  0.047322  0.047322
2017-01-11  0.213384  1.056038  0.213384  1.056038
2017-01-13 -1.588518  0.076655 -1.588518  0.076655
2017-01-15  1.467056  1.467056 -1.877541 -1.877541
2017-01-17  0.003218  0.003218 -0.811914 -0.811914
2017-01-19  0.143571  0.837088  0.143571  0.837088

取樣操作起來非常簡單,只是需要注意取樣後對新資料不同的處理方法。上面介紹的是降頻取樣。我們也可以升頻取樣。

繼續沿用上面 data 的示例資料

# 時間頻率從天提升到小時,並使用相同的資料對新增加行填充
In [11]: data.resample('H').ffill()
Out[11]:
2017-01-01 00:00:00    0.384984
2017-01-01 01:00:00    0.384984
2017-01-01 02:00:00    0.384984
2017-01-01 03:00:00    0.384984
2017-01-01 04:00:00    0.384984

                         ...

2017-01-19 21:00:00    0.143571
2017-01-19 22:00:00    0.143571
2017-01-19 23:00:00    0.143571
2017-01-20 00:00:00    0.837088
Freq: H, Length: 457, dtype: float64


# 時間頻率從天提升到小時,不對新增加行填充
In [12]: data.resample('H').asfreq()
Out[12]:
2017-01-01 00:00:00    0.384984
2017-01-01 01:00:00         NaN
2017-01-01 02:00:00         NaN

                         ...

2017-01-19 23:00:00         NaN
2017-01-20 00:00:00    0.837088
Freq: H, Length: 457, dtype: float64

# 時間頻率從天提升到小時,只對新增加前 3 行填充
In [13]: data.resample('H').ffill(limit=3)
Out[13]:
2017-01-01 00:00:00    0.384984
2017-01-01 01:00:00    0.384984
2017-01-01 02:00:00    0.384984
2017-01-01 03:00:00    0.384984
2017-01-01 04:00:00         NaN
2017-01-01 05:00:00         NaN

                         ...

2017-01-19 21:00:00         NaN
2017-01-19 22:00:00         NaN
2017-01-19 23:00:00         NaN
2017-01-20 00:00:00    0.837088
Freq: H, Length: 457, dtype: float64

相關文章