利用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)