7.14 處理時(shí)間序列
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
本節(jié)是《Python 數(shù)據(jù)科學(xué)手冊(cè)》(Python Data Science Handbook)的摘錄。
Pandas 是在金融建模的背景下開發(fā)的,正如你所料,它包含一組相當(dāng)廣泛的工具,用于處理日期,時(shí)間和時(shí)間索引數(shù)據(jù)。日期和時(shí)間數(shù)據(jù)有幾種,我們將在這里討論:
- 時(shí)間戳:引用特定時(shí)刻(例如,2015 年 7 月 4 日上午 7:00)。
- 時(shí)間間隔(interval)和時(shí)間段:引用特定開始和結(jié)束點(diǎn)之間的時(shí)間長(zhǎng)度;例如,2015 年。時(shí)間段通常引用時(shí)間間隔的特殊情況,其中每個(gè)間隔具有統(tǒng)一的長(zhǎng)度并且不重疊(例如,構(gòu)成每天的 24 小時(shí)長(zhǎng)的時(shí)間段)。
- 時(shí)間增量或間隔(duration):引用確切的時(shí)間長(zhǎng)度(例如,間隔為 22.56 秒)。
在本節(jié)中,我們將介紹如何在 Pandas 中使用這些類型的日期/時(shí)間數(shù)據(jù)。這個(gè)簡(jiǎn)短的章節(jié)絕不是 Python 或 Pandas 中可用的時(shí)間序列工具的完整指南,而是用戶應(yīng)如何處理時(shí)間序列的廣泛概述。
我們將首先簡(jiǎn)要討論 Python 中處理日期和時(shí)間的工具,然后再更具體地討論 Pandas 提供的工具。在列出了一些更深入的資源之后,我們將回顧一些在 Pandas 中處理時(shí)間序列數(shù)據(jù)的簡(jiǎn)短示例。
Python 中的日期和時(shí)間
Python 世界有許多可用的日期,時(shí)間,增量和時(shí)間跨度表示。雖然 Pandas 提供的時(shí)間序列工具往往對(duì)數(shù)據(jù)科學(xué)應(yīng)用最有用,但查看它們與 Python 中使用的其他包的關(guān)系會(huì)很有幫助。
Python 原生日期和時(shí)間:datetime和dateutil
Python 處理日期和時(shí)間的基本對(duì)象位于內(nèi)置的datetime模塊中。你可以與第三方dateutil模塊一起使用它,在日期和時(shí)間快速執(zhí)行許多有用的功能。例如,你可以使用datetime類型手動(dòng)構(gòu)建日期:
from datetime import datetime
datetime(year=2015, month=7, day=4)
# datetime.datetime(2015, 7, 4, 0, 0)
或者,使用dateutil模塊,你可以從各種字符串格式解析日期:
from dateutil import parser
date = parser.parse("4th of July, 2015")
date
# datetime.datetime(2015, 7, 4, 0, 0)
一旦你有了datetime對(duì)象,你可以做一些事情,比如打印星期幾:
date.strftime('%A')
# 'Saturday'
在最后一行中,我們使用了一個(gè)標(biāo)準(zhǔn)的字符串格式代碼來打印星期幾("%A"),你可以閱讀 Python datetime文檔的strftime部分。其他有用的日期工具的文檔,可以在dateutil的在線文檔中找到。需要注意的一個(gè)相關(guān)包是pytz,其中包含用于處理時(shí)區(qū)的工具,它是大部分時(shí)間序列數(shù)據(jù)的令人頭疼的部分。
datetime和dateutil的強(qiáng)大之處,是它們的靈活性和簡(jiǎn)單的語法:你可以使用這些對(duì)象及其內(nèi)置方法,輕松執(zhí)行你可能感興趣的幾乎任何操作。
他們的缺陷是當(dāng)你處理大量的日期和時(shí)間的時(shí)候:
正如 Python 數(shù)值變量的列表不如 NumPy 風(fēng)格的數(shù)值數(shù)組,與編碼日期的類型化數(shù)組相比,Python 日期時(shí)間對(duì)象的列表不是最優(yōu)的。
時(shí)間的類型化數(shù)組:NumPy 的datetime64
Python 的日期時(shí)間格式的缺陷,啟發(fā)了 NumPy 團(tuán)隊(duì),向 NumPy 添加一組原生時(shí)間序列數(shù)據(jù)類型。datetime64 dtype將日期編碼為 64 位整數(shù),因此可以非常緊湊地表示日期數(shù)組。datetime64需要一個(gè)非常具體的輸入格式:
import numpy as np
date = np.array('2015-07-04', dtype=np.datetime64)
date
# array(datetime.date(2015, 7, 4), dtype='datetime64[D]')
但是,一旦我們格式化了這個(gè)日期,我們就可以快速對(duì)它進(jìn)行向量化操作:
date + np.arange(12)
'''
array(['2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',
'2015-07-08', '2015-07-09', '2015-07-10', '2015-07-11',
'2015-07-12', '2015-07-13', '2015-07-14', '2015-07-15'], dtype='datetime64[D]')
'''
由于 NumPy datetime64數(shù)組中的統(tǒng)一類型,這類操作可以比我們直接使用 Python 的datetime對(duì)象快得多,特別是當(dāng)數(shù)組變大時(shí)(我們?cè)凇癗umPy 數(shù)組的計(jì)算:通用函數(shù)”中介紹了這種類型的向量化)。
datetime64和timedelta64對(duì)象的一個(gè)細(xì)節(jié)是,它們建立在基本時(shí)間單位上。因?yàn)?code>datetime64對(duì)象限制為 64 位精度,所以可編碼時(shí)間的范圍是這個(gè)基本單位的2^64倍。換句話說,datetime64在時(shí)間分辨率和最大時(shí)間跨度之間進(jìn)行權(quán)衡。
例如,如果你想要納秒的時(shí)間分辨率,你只有足夠的信息來編碼2^64納秒或不到 600 年的范圍。NumPy 將從輸入中推斷出所需的單位;例如,這是基于日期的日期時(shí)間:
np.datetime64('2015-07-04')
# numpy.datetime64('2015-07-04')
這是基于分鐘的日期時(shí)間:
np.datetime64('2015-07-04 12:00')
# numpy.datetime64('2015-07-04T12:00')
請(qǐng)注意,時(shí)區(qū)會(huì)自動(dòng)設(shè)置為執(zhí)行代碼的計(jì)算機(jī)上的本地時(shí)間。你可以使用多種格式代碼之一,來強(qiáng)制任何所需的基本單位;例如,在這里我們將強(qiáng)制基于納秒的時(shí)間:
np.datetime64('2015-07-04 12:59:59.50', 'ns')
# numpy.datetime64('2015-07-04T12:59:59.500000000')
下表來自 NumPy datetime64文檔,列出了可用的格式代碼,以及它們可以編碼的相對(duì)和絕對(duì)時(shí)間跨度:
| 代碼 | 含義 | 時(shí)間跨度(相對(duì)) | 時(shí)間跨度(絕對(duì)) |
|---|---|---|---|
Y |
年 | ± 9.2e18 年 | [9.2e18 BC, 9.2e18 AD] |
M |
月 | ± 7.6e17 年 | [7.6e17 BC, 7.6e17 AD] |
W |
星期 | ± 1.7e17 年 | [1.7e17 BC, 1.7e17 AD] |
D |
日 | ± 2.5e16 年 | [2.5e16 BC, 2.5e16 AD] |
h |
小時(shí) | ± 1.0e15 年 | [1.0e15 BC, 1.0e15 AD] |
m |
分鐘 | ± 1.7e13 年 | [1.7e13 BC, 1.7e13 AD] |
s |
秒鐘 | ± 2.9e12 年 | [ 2.9e9 BC, 2.9e9 AD] |
ms |
毫秒 | ± 2.9e9 年 | [ 2.9e6 BC, 2.9e6 AD] |
us |
微秒 | ± 2.9e6 年 | [290301 BC, 294241 AD] |
ns |
納秒 | ± 292 年 | [ 1678 AD, 2262 AD] |
ps |
皮秒 | ± 106 天 | [ 1969 AD, 1970 AD] |
fs |
飛秒 | ± 2.6 小時(shí) | [ 1969 AD, 1970 AD] |
as |
阿秒 | ± 9.2 秒 | [ 1969 AD, 1970 AD] |
對(duì)于我們?cè)诂F(xiàn)實(shí)世界中看到的數(shù)據(jù)類型,有用的默認(rèn)值是datetime64[ns],因?yàn)樗梢跃幋a現(xiàn)代日期的有用范圍,具有相當(dāng)好的精度。
最后,我們將注意到,雖然datetime64數(shù)據(jù)類型解決了 Python 內(nèi)置datetime類型的一些缺陷,但它缺少datetime提供的許多便利方法和函數(shù)。特別是dateutil。更多信息可以在 NumPy 的datetime64文檔中找到。
Pandas 中的日期和時(shí)間:兩全其美
例如,我們可以使用 Pandas 工具重復(fù)上面的演示。我們可以解析格式靈活的字符串日期,并使用格式代碼輸出星期幾:
import pandas as pd
date = pd.to_datetime("4th of July, 2015")
date
# Timestamp('2015-07-04 00:00:00')
date.strftime('%A')
# 'Saturday'
另外,我們可以直接在同一個(gè)對(duì)象上進(jìn)行 NumPy 風(fēng)格的向量化操作:
date + pd.to_timedelta(np.arange(12), 'D')
'''
DatetimeIndex(['2015-07-04', '2015-07-05', '2015-07-06', '2015-07-07',
'2015-07-08', '2015-07-09', '2015-07-10', '2015-07-11',
'2015-07-12', '2015-07-13', '2015-07-14', '2015-07-15'],
dtype='datetime64[ns]', freq=None)
'''
在下一節(jié)中,我們將仔細(xì)研究,使用 Pandas 提供的工具處理時(shí)間序列數(shù)據(jù)。
Pandas 時(shí)間序列:按時(shí)間索引
Pandas 時(shí)間序列工具真正有用的地方,是按時(shí)間戳索引數(shù)據(jù)。例如,我們可以構(gòu)造一個(gè)具有時(shí)間索引的Series對(duì)象:
index = pd.DatetimeIndex(['2014-07-04', '2014-08-04',
'2015-07-04', '2015-08-04'])
data = pd.Series([0, 1, 2, 3], index=index)
data
'''
2014-07-04 0
2014-08-04 1
2015-07-04 2
2015-08-04 3
dtype: int64
'''
現(xiàn)在我們的Series中有這些數(shù)據(jù),我們可以使用前面章節(jié)中討論過的任何Series索引模式,傳遞可以強(qiáng)制轉(zhuǎn)換為日期的值:
data['2014-07-04':'2015-07-04']
'''
2014-07-04 0
2014-08-04 1
2015-07-04 2
dtype: int64
'''
還有其他特殊的僅限日期的索引操作,例如傳入一年來獲取該年所有數(shù)據(jù)的切片:
data['2015']
'''
2015-07-04 2
2015-08-04 3
dtype: int64
'''
之后,我們將看到日期索引的其他便捷之處的示例。但首先,仔細(xì)研究可用的時(shí)間序列數(shù)據(jù)結(jié)構(gòu)。
Pandas 時(shí)間序列數(shù)據(jù)結(jié)構(gòu)
本節(jié)將介紹用于處理時(shí)間序列數(shù)據(jù)的基本Pandas數(shù)據(jù)結(jié)構(gòu):
- 對(duì)于時(shí)間戳,Pandas 提供
Timestamp類型。 如前所述,它本質(zhì)上是 Python 原生datetime的替代品,但它基于更高效的numpy.datetime64數(shù)據(jù)類型。 相關(guān)的索引結(jié)構(gòu)是DatetimeIndex。 - 對(duì)于時(shí)間周期,Pandas 提供
Period類型。這基于numpy.datetime64編碼固定頻率的間隔。 相關(guān)的索引結(jié)構(gòu)是PeriodIndex。 - 對(duì)于時(shí)間增量或間隔,Pandas 提供
Timedelta類型。Timedelta是 Python 原生datetime.timedelta類型的更有效的替代品,它基于numpy.timedelta64。相關(guān)的索引結(jié)構(gòu)是TimedeltaIndex。
這些日期/時(shí)間對(duì)象中,最基本的是Timestamp和DatetimeIndex對(duì)象。雖然可以直接調(diào)用這些類對(duì)象,但更常見的是使用pd.to_datetime()函數(shù),它可以解析各種格式。將單個(gè)日期傳遞給pd.to_datetime()會(huì)產(chǎn)生Timestamp;默認(rèn)情況下傳遞一系列日期會(huì)產(chǎn)生一個(gè)DatetimeIndex:
dates = pd.to_datetime([datetime(2015, 7, 3), '4th of July, 2015',
'2015-Jul-6', '07-07-2015', '20150708'])
dates
'''
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='datetime64[ns]', freq=None)
'''
任何DatetimeIndex都可以使用to_period()函數(shù),轉(zhuǎn)換為PeriodIndex并添加頻率代碼;在這里我們用'D'來表示每日頻率:
dates.to_period('D')
'''
PeriodIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='int64', freq='D')
'''
例如,當(dāng)從日期中減去另一個(gè)日期時(shí),會(huì)創(chuàng)建一個(gè)TimedeltaIndex:
dates - dates[0]
'''
TimedeltaIndex(['0 days', '1 days', '3 days', '4 days', '5 days'], dtype='timedelta64[ns]', freq=None)
'''
常規(guī)序列:pd.date_range()
為了更方便地創(chuàng)建常規(guī)日期序列,Pandas 為此提供了一些函數(shù):pd.date_range()用于時(shí)間戳,pd.period_range()用于周期,pd.timedelta_range()用于時(shí)間增量。我們已經(jīng)看到,Python 的range()和 NumPy 的np.arange()將起點(diǎn),終點(diǎn)和可選的步長(zhǎng)轉(zhuǎn)換成一個(gè)序列。類似地,pd.date_range()接受開始日期,結(jié)束日期和可選頻率代碼,來創(chuàng)建常規(guī)日期序列。默認(rèn)情況下,頻率為一天:
pd.date_range('2015-07-03', '2015-07-10')
'''
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06',
'2015-07-07', '2015-07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]', freq='D')
'''
或者,可以不使用起點(diǎn)和終點(diǎn)來指定日期范圍,而是使用起始點(diǎn)和周期數(shù)量來指定日期范圍:
pd.date_range('2015-07-03', periods=8)
'''
DatetimeIndex(['2015-07-03', '2015-07-04', '2015-07-05', '2015-07-06',
'2015-07-07', '2015-07-08', '2015-07-09', '2015-07-10'],
dtype='datetime64[ns]', freq='D')
'''
可以通過改變freq參數(shù)來修改頻率,默認(rèn)為D。例如,這里我們將構(gòu)建一系列每小時(shí)的時(shí)間戳:
pd.date_range('2015-07-03', periods=8, freq='H')
'''
DatetimeIndex(['2015-07-03 00:00:00', '2015-07-03 01:00:00',
'2015-07-03 02:00:00', '2015-07-03 03:00:00',
'2015-07-03 04:00:00', '2015-07-03 05:00:00',
'2015-07-03 06:00:00', '2015-07-03 07:00:00'],
dtype='datetime64[ns]', freq='H')
'''
要?jiǎng)?chuàng)建Period或Timedelta值的常規(guī)序列,非常相似的pd.period_range()和pd.timedelta_range()函數(shù)是有用的。以下是一些每月的周期:
pd.period_range('2015-07', periods=8, freq='M')
'''
PeriodIndex(['2015-07', '2015-08', '2015-09', '2015-10', '2015-11', '2015-12',
'2016-01', '2016-02'],
dtype='int64', freq='M')
'''
以及按小時(shí)遞增的間隔序列:
pd.timedelta_range(0, periods=10, freq='H')
'''
TimedeltaIndex(['00:00:00', '01:00:00', '02:00:00', '03:00:00', '04:00:00',
'05:00:00', '06:00:00', '07:00:00', '08:00:00', '09:00:00'],
dtype='timedelta64[ns]', freq='H')
'''
所有這些都需要了解 Pandas 頻率代碼,我們將在下一節(jié)中進(jìn)行總結(jié)。
頻率和偏移
這些 Pandas 時(shí)間序列工具的基礎(chǔ)是頻率或日期偏移的概念。就像我們?cè)谏厦婵吹?code>D(天)和H(小時(shí))代碼一樣,我們可以使用這些代碼來指定任何所需的頻率間隔。下表總結(jié)了可用的主要代碼:
| 代碼 | 描述 | 代碼 | 描述 |
|---|---|---|---|
D |
日歷日 | B |
商業(yè)日 |
W |
星期 | ||
M |
月份 | BM |
商業(yè)月份 |
Q |
季度 | BQ |
商業(yè)季度 |
A |
年度 | BA |
商業(yè)年度 |
H |
小時(shí) | BH |
商業(yè)小時(shí) |
T |
分鐘 | ||
S |
秒鐘 | ||
L |
毫秒 | ||
U |
微秒 | ||
N |
納秒 |
月度,季度和年度的頻率都標(biāo)記在指定時(shí)間段的末尾。通過為這些中的任何一個(gè)添加S后綴,它們將在開頭標(biāo)記:
| 代碼 | 描述 | 代碼 | 描述 |
|---|---|---|---|
MS |
月份的起始 | BMS |
商業(yè)月份的起始 |
QS |
季度的起始 | BQS |
商業(yè)季度的起始 |
AS |
年度的起始 | BAS |
商業(yè)年度的起始 |
此外,你可以通過添加三個(gè)字母的月份代碼作為后綴,來更改用于標(biāo)記任何季度或年度代碼的月份:
-
Q-JAN,BQ-FEB,QS-MAR,BQS-APR,以及其他。 -
A-JAN,BA-FEB,AS-MAR,BAS-APR,以及其他。
同樣,可以通過添加三個(gè)字母的星期代碼,來修改每周頻率的分割點(diǎn):
-
W-SUN,W-MON,W-TUE,W-WED,以及其他。
除此之外,代碼可以與數(shù)字組合以指定其他頻率。例如,對(duì)于 2 小時(shí) 30 分鐘的頻率,我們可以將小時(shí)(H)和分鐘(T)代碼組合如下:
pd.timedelta_range(0, periods=9, freq="2H30T")
'''
TimedeltaIndex(['00:00:00', '02:30:00', '05:00:00', '07:30:00', '10:00:00',
'12:30:00', '15:00:00', '17:30:00', '20:00:00'],
dtype='timedelta64[ns]', freq='150T')
'''
from pandas.tseries.offsets import BDay
pd.date_range('2015-07-01', periods=5, freq=BDay())
'''
DatetimeIndex(['2015-07-01', '2015-07-02', '2015-07-03', '2015-07-06',
'2015-07-07'],
dtype='datetime64[ns]', freq='B')
'''
頻率和偏移的使用的更多討論,請(qǐng)參閱 Pandas 文檔的“日期偏移”部分。
重采樣,平移和窗口化
使用日期和時(shí)間作為索引,來直觀地組織和訪問數(shù)據(jù)的能力,是 Pandas 時(shí)間序列工具的重要組成部分。一般情況下,索引數(shù)據(jù)的優(yōu)勢(shì)(操作期間的自動(dòng)對(duì)齊,直觀的數(shù)據(jù)切片和訪問等)仍然有效,并且 Pandas 提供了一些額外的時(shí)間序列特定的操作。
我們將以一些股票價(jià)格數(shù)據(jù)為例,看看其中的一些。由于 Pandas 主要是在金融環(huán)境中開發(fā)的,因此它包含一些非常具體的金融數(shù)據(jù)工具。
例如,附帶的pandas-datareader包(可通過conda install pandas-datareader安裝)知道如何從許多可用來源導(dǎo)入金融數(shù)據(jù),包括 Yahoo finance,Google Finance 等。在這里,我們將加載 Google 的收盤價(jià)歷史記錄:
from pandas_datareader import data
goog = data.DataReader('GOOG', start='2004', end='2016',
data_source='google')
goog.head()
| Open | High | Low | Close | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 2004-08-19 | 49.96 | 51.98 | 47.93 | 50.12 | NaN |
| 2004-08-20 | 50.69 | 54.49 | 50.20 | 54.10 | NaN |
| 2004-08-23 | 55.32 | 56.68 | 54.47 | 54.65 | NaN |
| 2004-08-24 | 55.56 | 55.74 | 51.73 | 52.38 | NaN |
| 2004-08-25 | 52.43 | 53.95 | 51.89 | 52.95 | NaN |
為簡(jiǎn)單起見,我們僅使用收盤價(jià):
goog = goog['Close']
在普通的 Matplotlib 樣板設(shè)置之后,我們可以使用plot()方法將其可視化(參見第四章)):
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
goog.plot();

重采樣和轉(zhuǎn)換頻率
時(shí)間序列數(shù)據(jù)的一個(gè)常見需求,是以更高或更低的頻率重采樣。這可以使用resample()方法,或更簡(jiǎn)單的asfreq()方法來完成。兩者之間的主要區(qū)別在于,resample()基本上是數(shù)據(jù)聚合,而asfreq()基本上是數(shù)據(jù)選擇。
看一下谷歌的收盤價(jià),讓我們比較一下我們對(duì)數(shù)據(jù)下采樣時(shí)的回報(bào)。在這里,我們將在商業(yè)年度結(jié)束時(shí)重采樣數(shù)據(jù):
goog.plot(alpha=0.5, style='-')
goog.resample('BA').mean().plot(style=':')
goog.asfreq('BA').plot(style='--');
plt.legend(['input', 'resample', 'asfreq'],
loc='upper left');

注意區(qū)別:在每一點(diǎn),resample報(bào)告前一年的平均值,而asfreq報(bào)告年末的值。
對(duì)于上采樣,resample()和asfreq()在很大程度上是等效的,盡管resample有更多可用的選項(xiàng)。在這種情況下,兩種方法的默認(rèn)設(shè)置是將上采樣點(diǎn)留空,即填充 NA 值。就像之前討論過的pd.fillna()函數(shù)一樣,asfreq()接受一個(gè)method參數(shù)來指定值的估算方式。在這里,我們將以每日頻率(即包括周末)重新采樣商業(yè)日數(shù)據(jù):
fig, ax = plt.subplots(2, sharex=True)
data = goog.iloc[:10]
data.asfreq('D').plot(ax=ax[0], marker='o')
data.asfreq('D', method='bfill').plot(ax=ax[1], style='-o')
data.asfreq('D', method='ffill').plot(ax=ax[1], style='--o')
ax[1].legend(["back-fill", "forward-fill"]);

頂部面板是默認(rèn)值:非工作日保留為 NA 值,并且不會(huì)顯示在圖表上。底部面板顯示填補(bǔ)空白的兩種策略之間的差異:向前填充和向后填充。
時(shí)間平移
另一種常見的時(shí)間序列特定的操作是按時(shí)間平移數(shù)據(jù)。Pandas 有兩個(gè)密切相關(guān)的計(jì)算方法:shift()和tshift()。簡(jiǎn)而言之,它們之間的區(qū)別在于,shift()平移數(shù)據(jù),而tshift()平移索引。在這兩種情況下,平移都指定為頻率的倍數(shù)。
在這里,我們使用shift()和tshift()來平移 900 天;
fig, ax = plt.subplots(3, sharey=True)
# 對(duì)數(shù)據(jù)應(yīng)用頻率
goog = goog.asfreq('D', method='pad')
goog.plot(ax=ax[0])
goog.shift(900).plot(ax=ax[1])
goog.tshift(900).plot(ax=ax[2])
# 圖例和注解
local_max = pd.to_datetime('2007-11-05')
offset = pd.Timedelta(900, 'D')
ax[0].legend(['input'], loc=2)
ax[0].get_xticklabels()[2].set(weight='heavy', color='red')
ax[0].axvline(local_max, alpha=0.3, color='red')
ax[1].legend(['shift(900)'], loc=2)
ax[1].get_xticklabels()[2].set(weight='heavy', color='red')
ax[1].axvline(local_max + offset, alpha=0.3, color='red')
ax[2].legend(['tshift(900)'], loc=2)
ax[2].get_xticklabels()[1].set(weight='heavy', color='red')
ax[2].axvline(local_max + offset, alpha=0.3, color='red');

我們?cè)谶@里看到shift(900)將數(shù)據(jù)移動(dòng) 900 天,將其中的一些移出圖的末尾(并在另一端留下 NA 值),而``tshift(900)將索引移動(dòng) 900 天。
這種類型轉(zhuǎn)換的常見背景,是計(jì)算隨時(shí)間的差異。 例如,我們使用移位值來計(jì)算 Google 股票在數(shù)據(jù)集過程中的一年投資回報(bào):
ROI = 100 * (goog.tshift(-365) / goog - 1)
ROI.plot()
plt.ylabel('% Return on Investment');

這有助于我們看到谷歌股票的總體趨勢(shì):到目前為止,投資谷歌的最有利的時(shí)期(回想一下,不出所料)在其 IPO 后不久,以及在 2009 年中期經(jīng)濟(jì)衰退期間。
滾動(dòng)窗口
滾動(dòng)統(tǒng)計(jì)量是 Pandas 實(shí)現(xiàn)的第三種時(shí)間序列特定的操作。
這些可以通過Series和DataFrame對(duì)象的rolling()屬性來完成,它返回一個(gè)視圖,類似于我們?cè)?code>groupby操作中看到的東西(參見“聚合和分組”)。這個(gè)滾動(dòng)視圖默認(rèn)提供許多聚合操作。
例如,以下是 Google 股票價(jià)格的一年中心化滾動(dòng)均值和標(biāo)準(zhǔn)差:
rolling = goog.rolling(365, center=True)
data = pd.DataFrame({'input': goog,
'one-year rolling_mean': rolling.mean(),
'one-year rolling_std': rolling.std()})
ax = data.plot(style=['-', '--', ':'])
ax.lines[0].set_alpha(0.3)

與分組操作一樣,aggregate()和apply()方法可用于自定義滾動(dòng)計(jì)算。
在哪里了解更多
本節(jié)僅簡(jiǎn)要概述了 Pandas 提供的時(shí)間序列工具的一些最基本功能;更完整的討論請(qǐng)參閱 Pandas 在線文檔的“時(shí)間序列/日期”部分。另一個(gè)優(yōu)秀的資源是 Wes McKinney 的書籍《利用 Python 進(jìn)行數(shù)據(jù)分析》(Python for Data Analysis (OReilly 2012))。
雖然現(xiàn)在已有幾年歷史,但它是 Pandas 用法的寶貴資源。特別是,本書重點(diǎn)講解商業(yè)和金融環(huán)境中的時(shí)間序列工具,并更多地關(guān)注商業(yè)日歷,時(shí)區(qū)和相關(guān)主題的特定細(xì)節(jié)。
與往常一樣,你也可以使用 IPython 幫助功能,來探索和嘗試可用于此處討論的函數(shù)和方法的更多選項(xiàng)。 我發(fā)現(xiàn)這通常是學(xué)習(xí)新 Python 工具的最佳方式。
示例:可視化西雅圖自行車數(shù)量
作為處理時(shí)間序列數(shù)據(jù)的一個(gè)更為復(fù)雜的例子,讓我們來看看西雅圖Fremont Bridge的自行車數(shù)量。這些數(shù)據(jù)來自于 2012 年底安裝的自動(dòng)化自行車計(jì)數(shù)器,在橋的東西側(cè)人行道上設(shè)有感應(yīng)式傳感器。每小時(shí)自行車計(jì)數(shù)可以從 http://data.seattle.gov/ 下載;這是數(shù)據(jù)集的直接鏈接。
截至 2016 年夏季,CSV 可以按如下方式下載:
# !curl -o FremontBridge.csv https://data.seattle.gov/api/views/65db-xm6k/rows.csv?accessType=DOWNLOAD
下載此數(shù)據(jù)集后,我們可以使用 Pandas 將 CSV 讀入DataFrame。我們將指定,我們希望Date作為索引,并且我們希望自動(dòng)解析這些日期:
data = pd.read_csv('FremontBridge.csv', index_col='Date', parse_dates=True)
data.head()
| Fremont Bridge West Sidewalk | Fremont Bridge East Sidewalk | |
|---|---|---|
| Date | ||
| 2012-10-03 00:00:00 | 4.0 | 9.0 |
| 2012-10-03 01:00:00 | 4.0 | 6.0 |
| 2012-10-03 02:00:00 | 1.0 | 1.0 |
| 2012-10-03 03:00:00 | 2.0 | 3.0 |
| 2012-10-03 04:00:00 | 6.0 | 1.0 |
為方便起見,我們將通過縮短列名并添加'Total'列,來進(jìn)一步處理此數(shù)據(jù)集:
data.columns = ['West', 'East']
data['Total'] = data.eval('West + East')
現(xiàn)在讓我們來看看這些數(shù)據(jù)的摘要統(tǒng)計(jì)信息:
data.dropna().describe()
| West | East | Total | |
|---|---|---|---|
| count | 35752.000000 | 35752.000000 | 35752.000000 |
| mean | 61.470267 | 54.410774 | 115.881042 |
| std | 82.588484 | 77.659796 | 145.392385 |
| min | 0.000000 | 0.000000 | 0.000000 |
| 25% | 8.000000 | 7.000000 | 16.000000 |
| 50% | 33.000000 | 28.000000 | 65.000000 |
| 75% | 79.000000 | 67.000000 | 151.000000 |
| max | 825.000000 | 717.000000 | 1186.000000 |
可視化數(shù)據(jù)
我們可以通過可視化來獲得對(duì)數(shù)據(jù)集的一些了解。讓我們從繪制原始數(shù)據(jù)開始:
%matplotlib inline
import seaborn; seaborn.set()
data.plot()
plt.ylabel('Hourly Bicycle Count');

大約 25,000 小時(shí)的樣本太密集了,我們無法理解。我們可以通過將數(shù)據(jù)重采樣到更粗糙的網(wǎng)格,來獲得更多見解。讓我們按周重采樣:
weekly = data.resample('W').sum()
weekly.plot(style=[':', '--', '-'])
plt.ylabel('Weekly bicycle count');

這向我們展示了一些有趣的季節(jié)性趨勢(shì):正如你所料,人們?cè)谙奶祢T自行車比冬季更多,甚至在特定的季節(jié)內(nèi),自行車的使用每周也不同(可能取決于天氣;參見“深度:線性回歸”,我們?cè)谀抢镞M(jìn)一步探索它)。
另一種方便的匯總數(shù)據(jù)的方法是滾動(dòng)均值,使用pd.rolling_mean()函數(shù)。在這里,我們將對(duì)數(shù)據(jù)進(jìn)行 30 天的滾動(dòng)操作,確保窗口居中:
daily = data.resample('D').sum()
daily.rolling(30, center=True).sum().plot(style=[':', '--', '-'])
plt.ylabel('mean hourly count');

結(jié)果的鋸齒狀是由于窗口的硬截?cái)嘣斐傻?。我們可以使用窗口函?shù)(例如,高斯窗口)獲得更平滑的滾動(dòng)平均版本。以下代碼指定了窗口的寬度(我們選擇了 50 天)和窗口內(nèi)的高斯寬度(我們選擇了 10 天):
daily.rolling(50, center=True,
win_type='gaussian').sum(std=10).plot(style=[':', '--', '-']);

深挖數(shù)據(jù)
雖然這些平滑的數(shù)據(jù)視圖對(duì)于了解數(shù)據(jù)的總體趨勢(shì)很有用,但它們隱藏了許多有趣的結(jié)構(gòu)。例如,我們可能希望,將平均流量視為一天中的時(shí)間的函數(shù)。我們可以使用“聚合和分組”中討論的GroupBy功能來執(zhí)行此操作:
by_time = data.groupby(data.index.time).mean()
hourly_ticks = 4 * 60 * 60 * np.arange(6)
by_time.plot(xticks=hourly_ticks, style=[':', '--', '-']);

每小時(shí)流量是一個(gè)強(qiáng)烈的雙峰分布,早上 8 點(diǎn)到晚上 5 點(diǎn)都是峰值。這可能是一個(gè)重要證據(jù),通勤交通的一個(gè)重要組成部分跨越橋梁。西側(cè)人行道(通常用于前往西雅圖市中心)和東側(cè)的人行道(通常用于遠(yuǎn)離西雅圖市中心)之間的差異,進(jìn)一步證明了這一點(diǎn),前者在早上是強(qiáng)烈的峰值,而后者在晚上是強(qiáng)烈的峰值。
我們也可能對(duì)事情如何基于一周中的某一天發(fā)生變化感到好奇。 同樣,我們可以通過一個(gè)簡(jiǎn)單的groupby來實(shí)現(xiàn):
by_weekday = data.groupby(data.index.dayofweek).mean()
by_weekday.index = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']
by_weekday.plot(style=[':', '--', '-']);

這顯示了工作日和周末數(shù)量之間的強(qiáng)烈差異,周一至周五過橋的平均騎手?jǐn)?shù)量是周六和周日的兩倍。
考慮到這一點(diǎn),讓我們執(zhí)行復(fù)合的GroupBy,看一下工作日和周末的每小時(shí)趨勢(shì)。我們首先按照標(biāo)記周末的標(biāo)志,和一天中的時(shí)間分組:
weekend = np.where(data.index.weekday < 5, 'Weekday', 'Weekend')
by_time = data.groupby([weekend, data.index.time]).mean()
現(xiàn)在我們將使用“多個(gè)子圖”中描述的一些 Matplotlib 工具,來并排繪制兩個(gè)面板:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(14, 5))
by_time.ix['Weekday'].plot(ax=ax[0], title='Weekdays',
xticks=hourly_ticks, style=[':', '--', '-'])
by_time.ix['Weekend'].plot(ax=ax[1], title='Weekends',
xticks=hourly_ticks, style=[':', '--', '-']);

結(jié)果非常有趣:我們?cè)诠ぷ魅掌陂g看到雙峰通勤模式,在周末看到單峰休閑模式。更詳細(xì)地挖掘這些數(shù)據(jù),并檢查天氣,溫度,一年中的時(shí)間,以及其他因素對(duì)人們通勤模式的影響,將會(huì)很有趣;進(jìn)一步的討論請(qǐng)參閱我的博客文章“Is Seattle Really Seeing an Uptick In Cycling?”,它使用這些數(shù)據(jù)的一個(gè)子集。我們還將在“深入:線性回歸”中的建模環(huán)境中,回顧這個(gè)數(shù)據(jù)集。