Pandas學(xué)習(xí)筆記之時間序列總結(jié)

早起導(dǎo)讀:pandas是Python數(shù)據(jù)處理的利器,時間序列數(shù)據(jù)又是在很多場景中出現(xiàn),本文來自GitHub,詳細(xì)講解了Python和Pandas中的時間及時間序列數(shù)據(jù)的處理方法與實戰(zhàn),建議收藏閱讀。
關(guān)鍵詞:pandas NumPy?時間序列
時間戳 代表著一個特定的時間點(例如 2015 年 7 月 4 日上午 7 點)。 時間間隔和周期 代表著從開始時間點到結(jié)束時間點之間的時間單位長度;例如 2015 一整年。周期通常代表一段特殊的時間間隔,每個時間間隔的長度都是統(tǒng)一的,彼此之間不重疊(例如一天由 24 個小時組成)。 時間差或持續(xù)時間代表這一段準(zhǔn)確的時間長度(例如 22.56 秒持續(xù)時間)。
Python 中的日期和時間
Python 本身就帶有很多有關(guān)日期、時間、時間差和間隔的表示方法。Pandas 提供的時間序列工具在數(shù)據(jù)科學(xué)領(lǐng)域會更加的強(qiáng)大,但是首先學(xué)習(xí)相關(guān)的 Python 的工具包會對我們理解它們更加有幫助。
原生 Python 日期和時間:datetime 和 dateutil
Python 最基礎(chǔ)的日期和時間處理包就是datetime。如果加上第三方的dateutil模塊,你就能迅速的對日期和時間進(jìn)行許多有用的操作了。例如,你可以手動創(chuàng)建一個datetime對象:
from datetime import datetime
datetime(year=2015, month=7, day=4)
datetime.datetime(2015, 7, 4, 0, 0)
或者使用dateutil模塊,你可以從許多不同的字符串格式中解析出datetime對象:
from dateutil import parser
date = parser.parse("4th of July, 2015")
date
datetime.datetime(2015, 7, 4, 0, 0)
獲得datetime對象之后,你可以對它進(jìn)行很多操作,包括輸出這天是星期幾:
date.strftime('%A')
'Saturday'
在上面的代碼中,我們使用了標(biāo)準(zhǔn)的字符串格式化編碼來打印日期("%A"),你可以在時間格式化在線文檔中看到全部的說明。Python 的datetime在線文檔可以參考datetime 文檔。其他很有用的日期時間工具dateutil的文檔可在dateutil 在線文檔找到。還有一個值得注意的第三方包是pytz,用來處理最頭痛的時間序列數(shù)據(jù):時區(qū)。
datetime和dateutil的強(qiáng)大在于它們靈活而易懂的語法:你可以使用這些對象內(nèi)建的方法就可以完成幾乎所有你感興趣的時間操作。但是當(dāng)對付大量的日期時間組成的數(shù)組時,它們就無法勝任了:就像 Python 的列表和 NumPy 的類型數(shù)組對比一樣,Python 的日期時間對象在這種情況下就無法與編碼后的日期時間數(shù)組比較了。
時間的類型數(shù)組:NumPy 的 datetime64
Python 日期時間對象的弱點促使 NumPy 的開發(fā)團(tuán)隊在 NumPy 中加入了優(yōu)化的時間序列數(shù)據(jù)類型。datetime64數(shù)據(jù)類型將日期時間編碼成了一個 64 位的整數(shù),因此 NumPy 存儲日期時間的格式非常緊湊。datetime64規(guī)定了非常明確的輸入格式:
import numpy as np
date = np.array('2015-07-04', dtype=np.datetime64)
date
array('2015-07-04', dtype='datetime64[D]')
然后我們就能立刻在這個日期數(shù)組之上應(yīng)用向量化操作:
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 數(shù)組中所有元素都具有統(tǒng)一的datetime64類型,上面的向量化操作將會比我們使用 Python 的datetime對象高效許多,特別是當(dāng)數(shù)組變得很大的情況下。
關(guān)于datetime64和timedelta64對象還有一個細(xì)節(jié)就是它們都是在基本時間單位之上構(gòu)建的。因為datetime64被限制在 64 位精度上,因此它可被編碼的時間范圍就是? 乘以相應(yīng)的時間單位。換言之,datetime64需要在時間精度和最大時間間隔之間進(jìn)行取舍。
例如,如果時間單位是納秒,datetime64類型能夠編碼的時間范圍就是? 納秒,不到 600 年。NumPy 可以自動從輸入推斷需要的時間精度(單位);如下面是天為單位:
np.datetime64('2015-07-04')
numpy.datetime64('2015-07-04')
下面是分鐘為單位:
np.datetime64('2015-07-04 12:00')
numpy.datetime64('2015-07-04T12:00')
還需要注意的是,日期時間會自動按照本地計算機(jī)的時間來進(jìn)行設(shè)置。你可以通過額外指定時間單位參數(shù)來設(shè)置你需要的精度;例如,下面使用的是納秒單位:
np.datetime64('2015-07-04 12:59:59.50', 'ns')
numpy.datetime64('2015-07-04T12:59:59.500000000')
下面這張表,來自NumPy datetime64 類型在線文檔,列出了可用的時間單位代碼以及其相應(yīng)的時間范圍限制:
| 代碼 | 含義 | 時間范圍 (相對) | 時間范圍 (絕對) |
|---|---|---|---|
Y | 年 | ± 9.2e18 年 | [公元前 9.2e18 至 公元后 9.2e18] |
M | 月 | ± 7.6e17 年 | [公元前 7.6e17 至 公元后 7.6e17] |
W | 星期 | ± 1.7e17 年 | [公元前 1.7e17 至 公元后 1.7e17] |
D | 日 | ± 2.5e16 年 | [公元前 2.5e16 至 公元后 2.5e16] |
h | 小時 | ± 1.0e15 年 | [公元前 1.0e15 至 公元后 1.0e15] |
m | 分鐘 | ± 1.7e13 年 | [公元前 1.7e13 至 公元后 1.7e13] |
s | 秒 | ± 2.9e12 年 | [公元前 2.9e9 至 公元后 2.9e9] |
ms | 毫秒 | ± 2.9e9 年 | [公元前 2.9e6 至 公元后 2.9e6] |
us | 微秒 | ± 2.9e6 年 | [公元前 290301 至 公元后 294241] |
ns | 納秒 | ± 292 年 | [公元后 1678 至 公元后 2262] |
ps | 皮秒 | ± 106 天 | [公元后 1969 至 公元后 1970] |
fs | 飛秒 | ± 2.6 小時 | [公元后 1969 至 公元后 1970] |
as | 阿秒 | ± 9.2 秒 | [公元后 1969 至 公元后 1970] |
對于我們目前真實世界的數(shù)據(jù)來說,一個合適的默認(rèn)值可以是datetime64[ns],因為它既能包含現(xiàn)代的時間范圍,也能提供相當(dāng)高的時間精度。
最后,還要提醒的是,雖然datetime64數(shù)據(jù)類型解決了 Python 內(nèi)建datetime類型的低效問題,但是它卻缺少很多datetime特別是dateutil對象提供的很方便的方法。你可以在NumPy 的 datetime64 在線文檔中查閱更多相關(guān)內(nèi)容。
Pandas 中的日期和時間:兼得所長
Pandas 在剛才介紹的那些工具的基礎(chǔ)上構(gòu)建了Timestamp對象,既包含了datetime和dateutil的簡單易用,又吸收了numpy.datetime64的高效和向量化操作優(yōu)點。將這些Timestamp對象組合起來之后,Pandas 就能構(gòu)建一個DatetimeIndex,能在Series或DataFrame當(dāng)中對數(shù)據(jù)進(jìn)行索引查找;我們下面會看到很多有關(guān)的例子。
例如,我們使用 Pandas 工具可以重復(fù)上面的例子。我們可以將一個靈活表示時間的字符串解析成日期時間對象,然后用時間格式化代碼進(jìn)行格式化輸出星期幾:
import pandas as pd
date = pd.to_datetime("4th of July, 2015")
date
Timestamp('2015-07-04 00:00:00')
date.strftime('%A')
'Saturday'
并且,我們可以將 NumPy 風(fēng)格的向量化操作直接應(yī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)
下面,我們將詳細(xì)介紹使用 Pandas 提供的工具對時間序列進(jìn)行操作的方法。
Pandas 時間序列:使用時間索引
對于 Pandas 時間序列工具來說,使用時間戳來索引數(shù)據(jù),才是真正吸引人的地方。例如,我們可以創(chuàng)建一個Series對象具有時間索引標(biāo)簽:
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
這樣我們就有了一個Series數(shù)據(jù),我們可以將任何Series索引的方法應(yīng)用到這個對象上,我們可以傳入?yún)?shù)值,Pandas 會自動轉(zhuǎn)換為日期時間進(jìn)行操作:
data['2014-07-04':'2015-07-04']
2014-07-04 0
2014-08-04 1
2015-07-04 2
dtype: int64
還有很多有關(guān)日期的索引方式,如下面將年作為參數(shù)傳入,會得到一個全年數(shù)據(jù)的切片:
data['2015']
2015-07-04 2
2015-08-04 3
dtype: int64
后面我們會看到更多使用日期時間作為索引值的例子。首先來詳細(xì)看看時間序列數(shù)據(jù)的結(jié)構(gòu)。
Pandas 時間序列數(shù)據(jù)結(jié)構(gòu)
這部分內(nèi)容會介紹 Pandas 在處理時間序列數(shù)據(jù)時候使用的基本數(shù)據(jù)結(jié)構(gòu):
對于時間戳,Pandas 提供了 Timestamp類型。正如上面所述,它可以作為 Python 原生datetime類型的替代,但是它是構(gòu)建在numpy.datetime64數(shù)據(jù)類型之上的。對應(yīng)的索引結(jié)構(gòu)是DatetimeIndex。對于時間周期,Pandas 提供了 Period類型。它是在numpy.datetime64的基礎(chǔ)上編碼了一個固定周期間隔的時間。對應(yīng)的索引結(jié)構(gòu)是PeriodIndex。對于時間差或持續(xù)時間,Pandas 提供了 Timedelta類型。構(gòu)建于numpy.timedelta64之上,是 Python 原生datetime.timedelta類型的高性能替代。對應(yīng)的索引結(jié)構(gòu)是TimedeltaIndex。
上述這些日期時間對象中最基礎(chǔ)的是Timestamp和DatetimeIndex對象。雖然這些對象可以直接被創(chuàng)建,但是更通用的做法是使用pd.to_datetime()函數(shù),該函數(shù)可以將多種格式的字符串解析成日期時間。將一個日期時間傳遞給pd.to_datetime()會得到一個Timestamp對象;將一系列的日期時間傳遞過去會得到一個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對象,不過需要額外指定一個頻率的參數(shù)碼;下面我們使用'D'來指定頻率為天:
dates.to_period('D')
PeriodIndex(['2015-07-03', '2015-07-04', '2015-07-06', '2015-07-07',
'2015-07-08'],
dtype='period[D]', freq='D')
TimedeltaIndex對象可以通過日期時間相減來創(chuàng)建,例如:
dates - dates[0]
TimedeltaIndex(['0 days', '1 days', '3 days', '4 days', '5 days'], dtype='timedelta64[ns]', freq=None)
規(guī)則序列:pd.date_range()
Pandas 提供了三個函數(shù)來創(chuàng)建規(guī)則的日期時間序列,pd.date_range()來創(chuàng)建時間戳的序列,pd.period_range()來創(chuàng)建周期的序列,pd.timedelta_range()來創(chuàng)建時間差的序列。我們都已經(jīng)學(xué)習(xí)過 Python 的range()和 NumPy 的arange()了,它們接受開始點、結(jié)束點和可選的步長參數(shù)來創(chuàng)建序列。同樣,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')
而且,日期時間的范圍不僅能通過結(jié)束日期時間指定,還能通過開始日期時間和一個持續(xù)值來指定:
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。例如,下面創(chuàng)建一段以小時為間隔單位的時間范圍:
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')
要創(chuàng)建Period或Timedelta對象,可以類似的調(diào)用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='period[M]', freq='M')
下面是以小時為單位的持續(xù)時間序列:
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')
上述函數(shù)都需要我們理解 Pandas 的頻率編碼,我們馬上會介紹它。
頻率和偏移值
要使用 Pandas 時間序列工具,我們需要理解頻率和時間偏移值的概念。就像前面我們看到的D代表天和H代表小時一樣,我們可以使用這類符號碼指定需要的頻率間隔。下表總結(jié)了主要的頻率碼:
| 碼 | 說明 | 碼 | 說明 |
|---|---|---|---|
D | 自然日 | B | 工作日 |
W | 周 | ||
M | 自然日月末 | BM | 工作日月末 |
Q | 自然日季末 | BQ | 工作日季末 |
A | 自然日年末 | BA | 工作日年末 |
H | 自然小時 | BH | 工作小時 |
T | 分鐘 | ||
S | 秒 | ||
L | 毫秒 | ||
U | 微秒 | ||
N | 納秒 |
上面的月、季度和年都代表著該時間周期的結(jié)束時間。如果在這些碼后面加上S后綴,則代表這些時間周期的起始時間:
| 碼 | 說明 | 碼 | 說明 | |
|---|---|---|---|---|
MS | 自然日月初 | BMS | 工作日月初 | |
QS | 自然日季初 | BQS | 工作日季初 | |
AS | 自然日年初 | BAS | 工作日年初 |
并且你可以通過在季度或者年的符號碼后面添加三個字母的月份縮寫來指定周期進(jìn)行分隔的月份:
Q-JAN、BQ-FEB、QS-MAR、BQS-APR等A-JAN、BA-FEB、AS-MAR、BAS-APR等
同樣,每周的分隔日也可以通過在周符號碼后面添加三個字母的星期幾縮寫來指定:
W-SUN、W-MON、W-TUE、W-WED等
在此之上,符號碼還可以進(jìn)行組合用來代表其他的頻率。例如要表示 2 小時 30 分鐘的頻率,我們可以通過將小時(H)和分鐘(T)的符號碼進(jìn)行組合得到:
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')
上述的這些短的符號碼實際上是 Pandas 時間序列偏移值的對象實例的別名,你可以在pd.tseries.offsets模塊中找到這些偏移值實例。例如,我們也可以通過一個偏移值對象實例來創(chuàng)建時間序列:
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')
更多有關(guān)頻率和偏移值的討論,請參閱 Pandas 在線文檔日期時間偏移值章節(jié)。
重新取樣、移動和窗口
使用日期和時間作為索引來直觀的組織和訪問數(shù)據(jù)的能力,是 Pandas 時間序列工具的重要功能。前面介紹過的索引的那些通用優(yōu)點(自動對齊,直觀的數(shù)據(jù)切片和訪問等)依然有效,而且 Pandas 提供了許多額外的時間序列相關(guān)操作。
我們會在這里介紹其中的一些,使用股票價格數(shù)據(jù)作為例子。因為 Pandas 是在金融背景基礎(chǔ)上發(fā)展而來的,因此它具有一些特別的金融數(shù)據(jù)相關(guān)工具。例如,pandas-datareader包(可以通過conda install pandas-datareader進(jìn)行安裝)可以被用來從許多可用的數(shù)據(jù)源導(dǎo)入金融數(shù)據(jù),包括 Yahoo 金融,Google 金融和其他。下面我們將載入 Yahoo 的收市價歷史數(shù)據(jù):
from pandas_datareader import data
goog = data.DataReader('GOOG', start='2004', end='2021',
data_source='yahoo')
goog.tail()
| High | Low | Open | Close | Volume | Adj Close | |
|---|---|---|---|---|---|---|
| Date | ||||||
| 2020-07-09 | 1522.719971 | 1488.084961 | 1506.449951 | 1510.989990 | 1423300.0 | 1510.989990 |
| 2020-07-10 | 1543.829956 | 1496.540039 | 1506.150024 | 1541.739990 | 1856300.0 | 1541.739990 |
| 2020-07-13 | 1577.131958 | 1505.243042 | 1550.000000 | 1511.339966 | 1846400.0 | 1511.339966 |
| 2020-07-14 | 1522.949951 | 1483.500000 | 1490.310059 | 1520.579956 | 1585000.0 | 1520.579956 |
| 2020-07-15 | 1535.329956 | 1498.000000 | 1523.130005 | 1513.640015 | 1609800.0 | 1513.640015 |
為簡單起見,我們僅使用收市價:
goog = goog['Close']
我們可以使用plot()方法來做出圖表,當(dāng)然之前要先完成 Matplotlib 的相關(guān)初始化工作:
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
goog.plot();

重新采樣和改變頻率
對于時間序列數(shù)據(jù)來說有一個很普遍的需求是對數(shù)據(jù)根據(jù)更高或更低的頻率進(jìn)行重新取樣。這可以通過resample()方法或更簡單的asfreq()方法來實現(xiàn)。兩者的主要區(qū)別在于resample()主要進(jìn)行數(shù)據(jù)聚合操作,而asfreq()方法主要進(jìn)行數(shù)據(jù)選擇操作。
觀察一下谷歌的收市價,讓我們來比較一下使用兩者對數(shù)據(jù)進(jìn)行更低頻率來采樣的情況。下面我們對數(shù)據(jù)進(jìn)行每個工作日年度進(jìn)行重新取樣:
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ū)別:在每個點,resample返回了這一個年度的平均值,而asfreq返回了年末的收市值。
對于采用更高頻率的取樣來說,resample()和asfreq()方法大體上是相同的,雖然 resample 有著更多的參數(shù)。在這個例子中,默認(rèn)的方式是將更高頻率的采樣點填充為空值,即 NA 值。就像之前介紹過的pd.fillna()函數(shù)那樣,asfreq()方法接受一個method參數(shù)來指定值以那種方式插入。下面,我們將原本數(shù)據(jù)的工作日頻率擴(kuò)張為自然日頻率(即包括周末):
fig, ax = plt.subplots(2, sharex=True)
data = goog.tail(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)的:非工作日的數(shù)據(jù)點被填充為 NA 值,因此在圖中沒有顯示。下面的子圖表展示了兩種不同填充方法的差別:前向填充和后向填充。
時間移動
另一個普遍的時間序列相關(guān)操作是移動時間。Pandas 有兩個很接近的方法來實現(xiàn)時間的移動:shift()和tshift。簡單來說,shift()移動的是數(shù)據(jù),而tshift()移動的是時間索引。兩個方法使用的移動參數(shù)都是當(dāng)前頻率的倍數(shù)。
下面我們使用shift()和tshift()方法將數(shù)據(jù)和時間索引移動 900 天:
fig, ax = plt.subplots(3, sharey=True)
# 在數(shù)據(jù)上應(yīng)用一個頻率
goog = goog.asfreq('D', method='pad')
goog.plot(ax=ax[0]) # 畫出原圖
goog.shift(900).plot(ax=ax[1]) # 數(shù)據(jù)移動900天
goog.tshift(900).plot(ax=ax[2]) # 時間移動900天
# 圖例和標(biāo)簽
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');

fig, ax = plt.subplots(3, sharey=True)
# 在數(shù)據(jù)上應(yīng)用一個頻率
goog = goog.asfreq('D', method='pad')
goog.plot(ax=ax[0]) # 畫出原圖
goog.shift(900).plot(ax=ax[1]) # 數(shù)據(jù)移動900天
goog.tshift(900).plot(ax=ax[2]) # 時間移動900天

上例中,我們看到shift(900)將數(shù)據(jù)向前移動了 900 天,導(dǎo)致部分?jǐn)?shù)據(jù)都超過了圖表的右側(cè)范圍(左側(cè)新出現(xiàn)的值被填充為 NA 值),而tshift(900)將時間向后移動了 900 天。
這種時間移動的常見應(yīng)用場景是計算同比時間段的差值。例如,我們可以將數(shù)據(jù)時間向前移動 365 天來計算谷歌股票的年投資回報率:
ROI = 100 * (goog.tshift(-365) / goog - 1)
ROI.plot()
plt.ylabel('% Return on Investment');

goog.tshift(-365)
Date
2003-08-20 49.982655
2003-08-21 53.952770
2003-08-22 53.952770
2003-08-23 53.952770
2003-08-24 54.495735
...
2019-07-12 1541.739990
2019-07-13 1541.739990
2019-07-14 1511.339966
2019-07-15 1520.579956
2019-07-16 1513.640015
Freq: D, Name: Close, Length: 5810, dtype: float64
這幫助我們看到谷歌股票的整體趨勢:直到目前為止,投資谷歌股票回報最高的時期(完全不令人驚訝)是 IPO 之后的短暫時期以及 2009 中期經(jīng)濟(jì)衰退的時期。
滾動窗口
滾動窗口統(tǒng)計是第三種 Pandas 時間序列相關(guān)的普遍操作。這個統(tǒng)計任務(wù)可以通過Series和DataFrame對象的rolling()方法來實現(xiàn),這個方法的返回值類似與我們之前看到的groupby操作(參見聚合與分組)。在該滾動窗口視圖上可以進(jìn)行一系列的聚合操作。
例如,下面是對谷歌股票價格在 365 個記錄中居中求平均值和標(biāo)準(zhǔn)差的結(jié)果:
rolling = goog.rolling(365, center=True) # 對365個交易日的收市價進(jìn)行滾動窗口居中
data = pd.DataFrame({'input': goog,
'one-year rolling_mean': rolling.mean(), # 平均值Series
'one-year rolling_std': rolling.std()}) # 標(biāo)準(zhǔn)差Series
ax = data.plot(style=['-', '--', ':'])
ax.lines[0].set_alpha(0.3)

和 groupby 操作一樣,aggregate()和apply()方法可以在滾動窗口上實現(xiàn)自定義的統(tǒng)計計算。
更多學(xué)習(xí)資源
本節(jié)只是簡要的介紹了 Pandas 提供的時間序列工具中最關(guān)鍵的特性;需要完整的內(nèi)容介紹,你可以訪問 Pandas 在線文檔的"時間序列/日期"章節(jié)。
還有一個很棒的資源是Python for Data Analysis教科書,作者 Wes McKinney (OReilly, 2012)。雖然已經(jīng)出版了好幾年,這本書仍然是 Pandas 使用的非常有價值的資源。特別是書中著重介紹在商業(yè)和金融領(lǐng)域中使用時間序列相關(guān)工具的內(nèi)容,還有許多對商業(yè)日歷,時區(qū)等相關(guān)主題的討論。
當(dāng)然別忘了,你可以使用 IPython 的幫助和文檔功能來學(xué)習(xí)和嘗試這些工具方法的不同參數(shù)。這通常是學(xué)習(xí) Python 工具最佳實踐。
例子:西雅圖自行車統(tǒng)計可視化
最后作為一個更深入的處理時間序列數(shù)據(jù)例子,我們來看一下西雅圖費利蒙橋的自行車數(shù)量統(tǒng)計。該數(shù)據(jù)集來源自一個自動自行車的計數(shù)器,在 2012 年末安裝上線,它們能夠感應(yīng)到橋上東西雙向通過的自行車并進(jìn)行計數(shù)。按照小時頻率采樣的自行車數(shù)量計數(shù)數(shù)據(jù)集可以在這個鏈接處直接下載。
2016 年夏天的數(shù)據(jù)可以使用下面的命令下載:
# !curl -o FremontBridge.csv https://data.seattle.gov/api/views/65db-xm6k/rows.csv?accessType=DOWNLOAD
下載了數(shù)據(jù)集后,我們就可以用 Pandas 將 CSV 文件的內(nèi)容導(dǎo)入成DataFrame對象。我們指定使用日期作為行索引,還可以通過parse_dates參數(shù)要求 Pandas 自動幫我們轉(zhuǎn)換日期時間格式:
data = pd.read_csv(r'D:\python\Github學(xué)習(xí)材料\Python數(shù)據(jù)科學(xué)手冊\notebooks\data\FremontBridge.csv', index_col='Date', parse_dates=True)
data.head()
| Fremont Bridge Total | Fremont Bridge East Sidewalk | Fremont Bridge West Sidewalk | |
|---|---|---|---|
| Date | |||
| 2012-10-03 00:00:00 | 13.0 | 4.0 | 9.0 |
| 2012-10-03 01:00:00 | 10.0 | 4.0 | 6.0 |
| 2012-10-03 02:00:00 | 2.0 | 1.0 | 1.0 |
| 2012-10-03 03:00:00 | 5.0 | 2.0 | 3.0 |
| 2012-10-03 04:00:00 | 7.0 | 6.0 | 1.0 |
為了簡單,我們將這個數(shù)據(jù)集的列名改的簡短些,并增加總計“Total”列:
# data.columns = ['West', 'East']
# data['Total'] = data.eval('West + East')
data.columns = ['Total', 'East', 'West']
現(xiàn)在我們來看看這個數(shù)據(jù)集的總體情況:
data.dropna().describe()
| Total | East | West | |
|---|---|---|---|
| count | 10771.000000 | 10771.000000 | 10771.000000 |
| mean | 99.713861 | 51.416489 | 48.297373 |
| std | 120.397155 | 63.867062 | 67.568734 |
| min | 0.000000 | 0.000000 | 0.000000 |
| 25% | 15.000000 | 7.000000 | 7.000000 |
| 50% | 57.000000 | 29.000000 | 26.000000 |
| 75% | 134.000000 | 69.000000 | 60.000000 |
| max | 831.000000 | 626.000000 | 593.000000 |
可視化數(shù)據(jù)
我們可以通過將數(shù)據(jù)可視化成圖表來更好的觀察分析數(shù)據(jù)集。首先我們來展示原始數(shù)據(jù)圖表:
%matplotlib inline
import seaborn; seaborn.set()
data.plot()
plt.ylabel('Hourly Bicycle Count');

約 25000 小時的樣本數(shù)據(jù)畫在圖中非常擁擠,我們很觀察到什么有意義的結(jié)果。我們可以通過重新取樣,降低頻率來獲得更粗顆粒度的圖像。如下面按照每周來重新取樣:
weekly = data.resample('W').sum()
weekly.plot(style=[':', '--', '-'])
plt.ylabel('Weekly bicycle count');

上圖向我們展示非常有趣的季節(jié)性趨勢:你應(yīng)該已經(jīng)預(yù)料到,人們在夏季會比冬季更多的騎自行車,即使在一個季節(jié)中,每周自行車的數(shù)量也有很大起伏(這主要是由于天氣造成的;我們會在深入:線性回歸中會更加深入的討論)。
還有一個很方便的聚合操作就是滾動平均值,使用pd.rolling_mean()函數(shù)。下面我們進(jìn)行 30 天的滾動平均,窗口居中進(jìn)行統(tǒng)計:
daily = data.resample('D').sum()
daily.rolling(30, center=True).sum().plot(style=[':', '--', '-'])
plt.ylabel('mean hourly count');

上圖結(jié)果中的鋸齒圖案產(chǎn)生的原因是窗口邊緣的硬切割造成的。我們可以使用不同的窗口類型來獲得更加平滑的結(jié)果,例如高斯窗口。下面的代碼制定了窗口的寬度(50 天)和窗口內(nèi)的高斯寬度(10 天):
daily.rolling(50, center=True,
win_type='gaussian').sum(std=10).plot(style=[':', '--', '-']);

挖掘數(shù)據(jù)
雖然上面的光滑折線圖展示了大體的數(shù)據(jù)趨勢情況,但是很多有趣的結(jié)構(gòu)依然沒有展現(xiàn)出來。例如,我們希望對每天不同時段的平均交通情況進(jìn)行統(tǒng)計,我們可以使用聚合與分組中介紹過的 GroupBy 功能:
by_time = data.groupby(data.index.time).mean()
hourly_ticks = 4 * 60 * 60 * np.arange(6) # 將24小時分為每4個小時一段展示
by_time.plot(xticks=hourly_ticks, style=[':', '--', '-']);

小時交通數(shù)據(jù)圖展現(xiàn)了明顯的雙峰構(gòu)造,峰值大約出現(xiàn)在早上 8:00 和下午 5:00。這顯然就是大橋在通勤時間交通繁忙的最好證據(jù)。再注意到東西雙向峰值不同,證明了早上通勤時間多數(shù)的交通流量是從東至西(往西雅圖城中心方向),而下午通勤時間多數(shù)的交通流量是從西至東(離開西雅圖城中心方向)。
我們可能也會很好奇一周中每天的平均交通情況。當(dāng)然,還是通過簡單的 GroupBy 就能實現(xiàn):
by_weekday = data.groupby(data.index.dayofweek).mean()
by_weekday.index = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']
by_weekday.plot(style=[':', '--', '-']);

上圖清晰的展示了工作日和休息日的區(qū)別,周一到周五的流量基本上達(dá)到周六日的兩倍。
有了上面兩個分析的基礎(chǔ),讓我們來進(jìn)行一個更加復(fù)雜的分組查看工作日和休息日按照小時交通流量的情況。我們首先使用np.where將工作日和休息日分開:
weekend = np.where(data.index.weekday < 5, 'Weekday', 'Weekend')
by_time = data.groupby([weekend, data.index.time]).mean()
然后我們使用將在多個子圖表中介紹的方法將兩個子圖表并排展示:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(14, 5))
by_time.loc['Weekday'].plot(ax=ax[0], title='Weekdays',
xticks=hourly_ticks, style=[':', '--', '-'])
by_time.loc['Weekend'].plot(ax=ax[1], title='Weekends',
xticks=hourly_ticks, style=[':', '--', '-']);

這個結(jié)果非常有趣:我們可以在工作日看到明顯的雙峰構(gòu)造,但是在休息日就只能看到一個峰。如果我們繼續(xù)挖掘下去,這個數(shù)據(jù)集還有更多有趣的結(jié)構(gòu)可以被發(fā)現(xiàn),可以分析天氣、氣溫、每年的不同時間以及其他因素是如何影響居民的通勤方式的;要深入討論,可以參見作者的博客文章"Is Seattle Really Seeing an Uptick In Cycling?",里面使用了這個數(shù)據(jù)集的子集。
[1]https://github.com/jakevdp/PythonDataScienceHandbook
-END-

