1. 程式人生 > >利用Python進行資料分析筆記-時間序列(轉換、索引、偏移)

利用Python進行資料分析筆記-時間序列(轉換、索引、偏移)

時間序列指能在任何能在時間上觀測到的資料。很多時間序列是有固定頻率(fixed frequency)的,意思是資料點會遵照某種規律定期出現,比如每15秒,每5分鐘,或每個月。時間序列也可能是不規律的(irregular),沒有一個固定的時間規律。如何參照時間序列資料取決於我們要做什麼樣的應用,我們可能會遇到下面這些:

  • Timestamps(時間戳),具體的某一個時刻
  • Fixed periods(固定的時期),比如2007年的一月,或者2010年整整一年
  • Intervals of time(時間間隔),通常有一個開始和結束的時間戳。Periods(時期)可能被看做是Intervals(間隔)的一種特殊形式。
  • Experiment or elapsed time(實驗或經過的時間);每一個時間戳都是看做是一個特定的開始時間(例如,在放入烤箱後,曲奇餅的直徑在每一秒的變化程度)

日期和時間資料型別及其工具

import pandas as pd
import numpy as np
from datetime import datetime
# 獲取時間
now = datetime.now()
now
datetime.datetime(2018, 5, 10, 13, 31, 51, 898458)
print(now.year,'年')
print(now.month,'月'
) print(now.day,'日') print(now.minute,'分') print(now.minute,'秒')
2018 年
5 月
10 日
31 分
31 秒

datetime能儲存日期和時間到微妙級別。timedelta表示兩個不同的datetime物件之間的時間上的不同:

# 時間對比
delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15)
delta
datetime.timedelta(926, 56700)
print('時間差多少天:',delta.days)
print('時間差多少秒:',delta.seconds)
時間差多少天: 926
時間差多少秒: 56700

我們可以在一個datetime物件上,新增或減少一個或多個timedelta,這樣可以產生新的變化後的物件

from datetime import timedelta

start = datetime(2011, 1, 7)
start + timedelta(12)    # 加12天
datetime.datetime(2011, 1, 19, 0, 0)
start - 2 * timedelta(12)   # 減24天
datetime.datetime(2010, 12, 14, 0, 0)

下表彙總了一些datetime模組中的資料型別:

1、字串與時間的轉換

我們可以對datetime物件,以及pandas的Timestamp物件進行格式化,這部分之後會介紹,使用str或strftime方法,傳入一個特定的時間格式就能進行轉換:

# 時間轉字串
stamp = datetime(2011, 1, 3)
str(stamp)
'2011-01-03 00:00:00'
stamp.strftime('%Y-%m-%d')
'2011-01-03'

下表是關於日期時間型別的格式:

我們可以利用上面的format codes(格式碼;時間日期格式)把字串轉換為日期,這要用到datetime.strptime:

# 字串轉時間
value = '2011-01-03'
datetime.strptime(value, '%Y-%m-%d')
datetime.datetime(2011, 1, 3, 0, 0)
datestrs = ['7/6/2011', '8/6/2011']
[datetime.strptime(x, '%m/%d/%Y') for x in datestrs]
[datetime.datetime(2011, 7, 6, 0, 0), datetime.datetime(2011, 8, 6, 0, 0)]

對於一個一直的時間格式,使用datetime.strptime來解析日期是很好的方法。但是,如果每次都要寫格式的話很煩人,尤其是對於一些比較常見的格式。在這種情況下,我們可以使用第三方庫dateutil中的parser.parse方法(這個庫會在安裝pandas的時候自動安裝)

from dateutil.parser import parse
parse('2011-01-03')
datetime.datetime(2011, 1, 3, 0, 0)
# dateutil能夠解析很多常見的時間表示格式
parse('Jan 31, 1997 10:45 PM')
datetime.datetime(1997, 1, 31, 22, 45)

在國際上,日在月之前是很常見的(譯者:美國是把月放在日前面的),所以我們可以設定dayfirst=True來指明最前面的是否是日

parse('6/12/2011', dayfirst=True)
datetime.datetime(2011, 12, 6, 0, 0)

pandas通常可以用於處理由日期組成的陣列,不論是否是DataFrame中的行索引或列。to_datetime方法能解析很多不同種類的日期表示。標準的日期格式,比如ISO 8601,能被快速解析

# 使用to_datetime
datestrs = ['2011-07-06 12:00:00', '2011-08-06 00:00:00']
pd.to_datetime(datestrs)
DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00'], dtype='datetime64[ns]', freq=None)

還能處理一些應該被判斷為缺失的值(比如None, 空字串之類的)

idx = pd.to_datetime(datestrs + [None])
idx
DatetimeIndex(['2011-07-06 12:00:00', '2011-08-06 00:00:00', 'NaT'], dtype='datetime64[ns]', freq=None)
idx[2]
NaT
pd.isnull(idx)
array([False, False,  True])

Nat(Not a Time)在pandas中,用於表示時間戳為空值(null value)。

dateutil.parse是一個很有用但不完美的工具。它可能會把一些字串識別為日期,例如,’42’就會被解析為2042年加上今天的日期。

datetime物件還有一些關於地區格式(locale-specific formatting)的選項,用於處理不同國家或不同語言的問題。例如,月份的縮寫在德國和法國,與英語是不同的。下表列出一些相關的選項:

時間序列基礎

在pandas中,一個基本的時間序列物件,是一個用時間戳作為索引的Series,在pandas外部的話,通常是用python 字串或datetime物件來表示的

dates = [datetime(2011, 1, 2), datetime(2011, 1, 5),
         datetime(2011, 1, 7), datetime(2011, 1, 8), 
         datetime(2011, 1, 10), datetime(2011, 1, 12)]
ts = pd.Series(np.random.randn(6), index=dates)
ts
2011-01-02    1.404005
2011-01-05    0.269604
2011-01-07   -0.558070
2011-01-08    0.876070
2011-01-10    0.694803
2011-01-12   -0.599207
dtype: float64
# 每隔兩個元素選一個元素
ts[::2]
2011-01-02    1.404005
2011-01-07   -0.558070
2011-01-10    0.694803
dtype: float64

pandas中的時間戳,是按numpy中的datetime64資料型別進行儲存的,可以精確到納秒的級別

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

1、索引,選擇,取子集

當我們基於標籤進行索引和選擇時,時間序列就像是pandas.Series

ts
2011-01-02    1.404005
2011-01-05    0.269604
2011-01-07   -0.558070
2011-01-08    0.876070
2011-01-10    0.694803
2011-01-12   -0.599207
dtype: float64
# 通過時間序列索引
stamp = ts.index[2]
ts[stamp]
-0.5580701255970213

為了方便,我們可以直接傳入一個字串用來表示日期

ts['1/10/2011']
0.6948026143470746
ts['20110110']
0.6948026143470746

對於比較長的時間序列,我們可以直接傳入一年或一年一個月,來進行資料選取

longer_ts = pd.Series(np.random.randn(1000),
                      index=pd.date_range('1/1/2000', periods=1000))  # periods表示週期
longer_ts[::50]  
2000-01-01   -1.434558
2000-02-20    0.199652
2000-04-10    0.396663
2000-05-30   -0.351714
2000-07-19   -1.464473
2000-09-07    0.113600
2000-10-27   -1.168503
2000-12-16   -0.395296
2001-02-04    0.109727
2001-03-26   -0.154458
2001-05-15    0.695305
2001-07-04    0.338459
2001-08-23    1.017848
2001-10-12    0.887390
2001-12-01    0.066979
2002-01-20    0.315712
2002-03-11    0.957264
2002-04-30    0.921923
2002-06-19    0.955736
2002-08-08    0.615709
Freq: 50D, dtype: float64
# 檢視2001年的後5個數
longer_ts['2001'][-5:] 
2001-12-27    0.553635
2001-12-28    0.900810
2001-12-29   -1.792167
2001-12-30    0.599491
2001-12-31    0.271903
Freq: D, dtype: float64
# 檢視2002年8月的前5個數
longer_ts['2002-8'][:5] 
2002-08-01    0.128209
2002-08-02    0.368129
2002-08-03    0.728122
2002-08-04    0.245300
2002-08-05   -0.685125
Freq: D, dtype: float64
# 利用datetime進行切片
longer_ts[datetime(2001, 1, 1)]
0.41985839386468266
# 按時間範圍切片
longer_ts['12/28/2000':'1/3/2001']
2000-12-28   -0.756228
2000-12-29   -0.202390
2000-12-30    0.877150
2000-12-31   -1.073438
2001-01-01    0.419858
2001-01-02   -0.302687
2001-01-03   -0.777208
Freq: D, dtype: float64

記住,這種方式的切片得到的只是原來資料的一個檢視,如果我們在切片的結果上進行更改的的,原來的資料也會變化。

有一個相等的例項方法(instance method)也能切片,truncate,能在兩個日期上,對Series進行切片

longer_ts.truncate(before='12/28/2000', after='1/3/2001')
2000-12-28   -0.756228
2000-12-29   -0.202390
2000-12-30    0.877150
2000-12-31   -1.073438
2001-01-01    0.419858
2001-01-02   -0.302687
2001-01-03   -0.777208
Freq: D, dtype: float64

所有這些都適用於DataFrame,我們對行進行索引

dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')   # periods表示週期, freq表示頻率
long_df = pd.DataFrame(np.random.randn(100, 4),
                       index=dates,
                       columns=['Colorado', 'Texas',
                                'New York', 'Ohio'])
long_df.iloc[::20] 
Colorado Texas New York Ohio
2000-01-05 -0.207091 -1.458642 -0.406117 -0.153867
2000-05-24 -0.047038 -0.496946 -0.091025 0.693195
2000-10-11 0.088201 -0.193686 -1.444394 -1.315864
2001-02-28 -0.654497 0.093796 -0.819060 0.755123
2001-07-18 1.885355 -0.206915 -1.392564 -1.514281
long_df.loc['2001-5']
Colorado Texas New York Ohio
2001-05-02 -1.742909 -0.996392 0.824744 0.352815
2001-05-09 -0.951432 -0.326518 -0.767074 0.168574
2001-05-16 0.926389 0.064682 -0.253195 -0.081806
2001-05-23 1.592441 -2.418966 0.713259 -1.198133
2001-05-30 0.700246 0.593380 0.179941 0.127628

2、重複索引的時間序列

在某些資料中,可能會遇到多個數據在同一時間戳下的情況

dates = pd.DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000', 
                          '1/2/2000', '1/3/2000'])
dup_ts = pd.Series(np.random.randn(5), index=dates)
dup_ts
2000-01-01    1.350517
2000-01-02   -0.646460
2000-01-02    0.503535
2000-01-02    1.518830
2000-01-03   -0.276995
dtype: float64

我們通過is_unique屬性來檢視index是否是唯一值

# 判斷索引值是否唯一
dup_ts.index.is_unique
False

對這個時間序列取索引的的話, 要麼得到標量,要麼得到切片,這取決於時間戳是否是重複的

dup_ts['1/2/2000'] 
2000-01-02   -0.646460
2000-01-02    0.503535
2000-01-02    1.518830
dtype: float64
# 索引的索引
dup_ts['1/2/2000'][2]
1.5188299993953172

3、生成日期範圍

date_range預設會生成按日頻度的時間戳,會保留開始或結束的時間戳。

pd.date_range(start='2012-04-01 12:56:3', periods=6)
DatetimeIndex(['2012-04-01 12:56:03', '2012-04-02 12:56:03',
               '2012-04-03 12:56:03', '2012-04-04 12:56:03',
               '2012-04-05 12:56:03', '2012-04-06 12:56:03'],
              dtype='datetime64[ns]', freq='D')
pd.date_range(end='2012-04-06', periods=6)
DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04',
               '2012-04-05', '2012-04-06'],
              dtype='datetime64[ns]', freq='D')
# 設定頻度
pd.date_range('2000-01-01', '2000-12-01', freq='BM')
DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28',
               '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31',
               '2000-09-29', '2000-10-31', '2000-11-30'],
              dtype='datetime64[ns]', freq='BM')

時間序列頻度:

有些時候我們的時間序列資料帶有小時,分,秒這樣的資訊,但我們想要讓這些時間戳全部歸一化到午夜(normalized to midnight, 即晚上0點),這個時候要用到normalize選項

nor_date = pd.date_range('2012-05-02 12:56:31', periods=5, normalize=True)
nor_date
DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05',
               '2012-05-06'],
              dtype='datetime64[ns]', freq='D')
# 可以看到小時,分,秒全部變為0
nor_date[2]
Timestamp('2012-05-04 00:00:00', freq='D')
# 設定頻度為4小時
pd.date_range('2000-01-01', '2000-01-03 23:59', freq='4H')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00',
               '2000-01-01 08:00:00', '2000-01-01 12:00:00',
               '2000-01-01 16:00:00', '2000-01-01 20:00:00',
               '2000-01-02 00:00:00', '2000-01-02 04:00:00',
               '2000-01-02 08:00:00', '2000-01-02 12:00:00',
               '2000-01-02 16:00:00', '2000-01-02 20:00:00',
               '2000-01-03 00:00:00', '2000-01-03 04:00:00',
               '2000-01-03 08:00:00', '2000-01-03 12:00:00',
               '2000-01-03 16:00:00', '2000-01-03 20:00:00'],
              dtype='datetime64[ns]', freq='4H')
# 設定頻度為1.5小時
pd.date_range('2000-01-01', periods=10, freq='1h30min')
DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00',
               '2000-01-01 03:00:00', '2000-01-01 04:30:00',
               '2000-01-01 06:00:00', '2000-01-01 07:30:00',
               '2000-01-01 09:00:00', '2000-01-01 10:30:00',
               '2000-01-01 12:00:00', '2000-01-01 13:30:00'],
              dtype='datetime64[ns]', freq='90T')

一個有用的類(class)是月中的第幾周(Week of month),用WOM表示。我們想得到每個月的第三個星期五:

# 頻度為每個月的第三個星期五
rng = pd.date_range('2012-01-01', '2012-09-01', freq='WOM-3FRI')
rng
DatetimeIndex(['2012-01-20', '2012-02-17', '2012-03-16', '2012-04-20',
               '2012-05-18', '2012-06-15', '2012-07-20', '2012-08-17'],
              dtype='datetime64[ns]', freq='WOM-3FRI')
# 日期範圍也能通過時區集合(time zone set)來建立
pd.date_range('3/9/2012 9:30', periods=10, freq='D', tz='UTC')
DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00',
               '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00',
               '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00',
               '2012-03-15 09:30:00+00:00', '2012-03-16 09:30:00+00:00',
               '2012-03-17 09:30:00+00:00', '2012-03-18 09:30:00+00:00'],
              dtype='datetime64[ns, UTC]', freq='D')

4、資料偏移(提前與推後)

偏移(shifting)表示按照時間把資料向前或向後推移。Series和DataFrame都有一個shift方法實現偏移,索引(index)不會被更改

ts = pd.Series(np.random.randn(4),
               index=pd.date_range('1/1/2000', periods=4, freq='M'))
ts
2000-01-31   -0.447984
2000-02-29    0.735111
2000-03-31    0.212321
2000-04-30   -0.987703
Freq: M, dtype: float64
# 位移時,會引入缺失值
ts.shift(-2)
2000-01-31    0.212321
2000-02-29   -0.987703
2000-03-31         NaN
2000-04-30         NaN
Freq: M, dtype: float64

shift的一個普通的用法是計算時間序列的百分比變化,可以表示為

ts / ts.shift(1) - 1
2000-01-31         NaN
2000-02-29   -2.640931
2000-03-31   -0.711171
2000-04-30   -5.651929
Freq: M, dtype: float64

因為普通的shift不會對index進行修改,一些資料會被丟棄。因此如果頻度是已知的,可以把頻度傳遞給shift,這樣的話時間戳會自動變化

ts.shift(2)
2000-01-31         NaN
2000-02-29         NaN
2000-03-31   -0.447984
2000-04-30    0.735111
Freq: M, dtype: float64
ts.shift(2, freq='M')
2000-03-31   -0.447984
2000-04-30    0.735111
2000-05-31    0.212321
2000-06-30   -0.987703
Freq: M, dtype: float64
# 偏移到月底
from pandas.tseries.offsets import Day, MonthEnd

now = datetime(2011, 11, 17)
offset = MonthEnd()  # 移到月底
now + offset
Timestamp('2011-11-30 00:00:00')
# 向前偏移一個月
offset.rollback(now)
Timestamp('2011-10-31 00:00:00')
offset.rollforward(now)
Timestamp('2011-11-30 00:00:00')
datetime.now()
datetime.datetime(2018, 5, 11, 8, 45, 22, 629877)