7.14 處理時間序列
譯者:飛龍
本節是《Python 數據科學手冊》(Python Data Science Handbook)的摘錄。
Pandas 是在金融建模的背景下開發的,正如你所料,它包含一組相當廣泛的工具,用于處理日期,時間和時間索引數據。日期和時間數據有幾種,我們將在這里討論:
- 時間戳:引用特定時刻(例如,2015 年 7 月 4 日上午 7:00)。
- 時間間隔(interval)和時間段:引用特定開始和結束點之間的時間長度;例如,2015 年。時間段通常引用時間間隔的特殊情況,其中每個間隔具有統一的長度并且不重疊(例如,構成每天的 24 小時長的時間段)。
- 時間增量或間隔(duration):引用確切的時間長度(例如,間隔為 22.56 秒)。
在本節中,我們將介紹如何在 Pandas 中使用這些類型的日期/時間數據。這個簡短的章節絕不是 Python 或 Pandas 中可用的時間序列工具的完整指南,而是用戶應如何處理時間序列的廣泛概述。
我們將首先簡要討論 Python 中處理日期和時間的工具,然后再更具體地討論 Pandas 提供的工具。在列出了一些更深入的資源之后,我們將回顧一些在 Pandas 中處理時間序列數據的簡短示例。
Python 中的日期和時間
Python 世界有許多可用的日期,時間,增量和時間跨度表示。雖然 Pandas 提供的時間序列工具往往對數據科學應用最有用,但查看它們與 Python 中使用的其他包的關系會很有幫助。
Python 原生日期和時間:datetime
和dateutil
Python 處理日期和時間的基本對象位于內置的datetime
模塊中。你可以與第三方dateutil
模塊一起使用它,在日期和時間快速執行許多有用的功能。例如,你可以使用datetime
類型手動構建日期:
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
對象,你可以做一些事情,比如打印星期幾:
date.strftime('%A')
# 'Saturday'
在最后一行中,我們使用了一個標準的字符串格式代碼來打印星期幾("%A"
),你可以閱讀 Python datetime
文檔的strftime
部分。其他有用的日期工具的文檔,可以在dateutil
的在線文檔中找到。需要注意的一個相關包是pytz
,其中包含用于處理時區的工具,它是大部分時間序列數據的令人頭疼的部分。
datetime
和dateutil
的強大之處,是它們的靈活性和簡單的語法:你可以使用這些對象及其內置方法,輕松執行你可能感興趣的幾乎任何操作。
他們的缺陷是當你處理大量的日期和時間的時候:
正如 Python 數值變量的列表不如 NumPy 風格的數值數組,與編碼日期的類型化數組相比,Python 日期時間對象的列表不是最優的。
時間的類型化數組:NumPy 的datetime64
Python 的日期時間格式的缺陷,啟發了 NumPy 團隊,向 NumPy 添加一組原生時間序列數據類型。datetime64 dtype
將日期編碼為 64 位整數,因此可以非常緊湊地表示日期數組。datetime64
需要一個非常具體的輸入格式:
import numpy as np
date = np.array('2015-07-04', dtype=np.datetime64)
date
# array(datetime.date(2015, 7, 4), dtype='datetime64[D]')
但是,一旦我們格式化了這個日期,我們就可以快速對它進行向量化操作:
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
數組中的統一類型,這類操作可以比我們直接使用 Python 的datetime
對象快得多,特別是當數組變大時(我們在“NumPy 數組的計算:通用函數”中介紹了這種類型的向量化)。
datetime64
和timedelta64
對象的一個細節是,它們建立在基本時間單位上。因為datetime64
對象限制為 64 位精度,所以可編碼時間的范圍是這個基本單位的2^64
倍。換句話說,datetime64
在時間分辨率和最大時間跨度之間進行權衡。
例如,如果你想要納秒的時間分辨率,你只有足夠的信息來編碼2^64
納秒或不到 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')
請注意,時區會自動設置為執行代碼的計算機上的本地時間。你可以使用多種格式代碼之一,來強制任何所需的基本單位;例如,在這里我們將強制基于納秒的時間:
np.datetime64('2015-07-04 12:59:59.50', 'ns')
# numpy.datetime64('2015-07-04T12:59:59.500000000')
下表來自 NumPy datetime64
文檔,列出了可用的格式代碼,以及它們可以編碼的相對和絕對時間跨度:
代碼 | 含義 | 時間跨度(相對) | 時間跨度(絕對) |
---|---|---|---|
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 |
小時 | ± 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 小時 | [ 1969 AD, 1970 AD] |
as |
阿秒 | ± 9.2 秒 | [ 1969 AD, 1970 AD] |
對于我們在現實世界中看到的數據類型,有用的默認值是datetime64[ns]
,因為它可以編碼現代日期的有用范圍,具有相當好的精度。
最后,我們將注意到,雖然datetime64
數據類型解決了 Python 內置datetime
類型的一些缺陷,但它缺少datetime
提供的許多便利方法和函數。特別是dateutil
。更多信息可以在 NumPy 的datetime64
文檔中找到。
Pandas 中的日期和時間:兩全其美
例如,我們可以使用 Pandas 工具重復上面的演示。我們可以解析格式靈活的字符串日期,并使用格式代碼輸出星期幾:
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 風格的向量化操作:
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)
'''
在下一節中,我們將仔細研究,使用 Pandas 提供的工具處理時間序列數據。
Pandas 時間序列:按時間索引
Pandas 時間序列工具真正有用的地方,是按時間戳索引數據。例如,我們可以構造一個具有時間索引的Series
對象:
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
中有這些數據,我們可以使用前面章節中討論過的任何Series
索引模式,傳遞可以強制轉換為日期的值:
data['2014-07-04':'2015-07-04']
'''
2014-07-04 0
2014-08-04 1
2015-07-04 2
dtype: int64
'''
還有其他特殊的僅限日期的索引操作,例如傳入一年來獲取該年所有數據的切片:
data['2015']
'''
2015-07-04 2
2015-08-04 3
dtype: int64
'''
之后,我們將看到日期索引的其他便捷之處的示例。但首先,仔細研究可用的時間序列數據結構。
Pandas 時間序列數據結構
本節將介紹用于處理時間序列數據的基本Pandas數據結構:
- 對于時間戳,Pandas 提供
Timestamp
類型。 如前所述,它本質上是 Python 原生datetime
的替代品,但它基于更高效的numpy.datetime64
數據類型。 相關的索引結構是DatetimeIndex
。 - 對于時間周期,Pandas 提供
Period
類型。這基于numpy.datetime64
編碼固定頻率的間隔。 相關的索引結構是PeriodIndex
。 - 對于時間增量或間隔,Pandas 提供
Timedelta
類型。Timedelta
是 Python 原生datetime.timedelta
類型的更有效的替代品,它基于numpy.timedelta64
。相關的索引結構是TimedeltaIndex
。
這些日期/時間對象中,最基本的是Timestamp
和DatetimeIndex
對象。雖然可以直接調用這些類對象,但更常見的是使用pd.to_datetime()
函數,它可以解析各種格式。將單個日期傳遞給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()
函數,轉換為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')
'''
例如,當從日期中減去另一個日期時,會創建一個TimedeltaIndex
:
dates - dates[0]
'''
TimedeltaIndex(['0 days', '1 days', '3 days', '4 days', '5 days'], dtype='timedelta64[ns]', freq=None)
'''
常規序列:pd.date_range()
為了更方便地創建常規日期序列,Pandas 為此提供了一些函數:pd.date_range()
用于時間戳,pd.period_range()
用于周期,pd.timedelta_range()
用于時間增量。我們已經看到,Python 的range()
和 NumPy 的np.arange()
將起點,終點和可選的步長轉換成一個序列。類似地,pd.date_range()
接受開始日期,結束日期和可選頻率代碼,來創建常規日期序列。默認情況下,頻率為一天:
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')
'''
或者,可以不使用起點和終點來指定日期范圍,而是使用起始點和周期數量來指定日期范圍:
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
參數來修改頻率,默認為D
。例如,這里我們將構建一系列每小時的時間戳:
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')
'''
要創建Period
或Timedelta
值的常規序列,非常相似的pd.period_range()
和pd.timedelta_range()
函數是有用的。以下是一些每月的周期:
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')
'''
以及按小時遞增的間隔序列:
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 頻率代碼,我們將在下一節中進行總結。
頻率和偏移
這些 Pandas 時間序列工具的基礎是頻率或日期偏移的概念。就像我們在上面看到D
(天)和H
(小時)代碼一樣,我們可以使用這些代碼來指定任何所需的頻率間隔。下表總結了可用的主要代碼:
代碼 | 描述 | 代碼 | 描述 |
---|---|---|---|
D |
日歷日 | B |
商業日 |
W |
星期 | ||
M |
月份 | BM |
商業月份 |
Q |
季度 | BQ |
商業季度 |
A |
年度 | BA |
商業年度 |
H |
小時 | BH |
商業小時 |
T |
分鐘 | ||
S |
秒鐘 | ||
L |
毫秒 | ||
U |
微秒 | ||
N |
納秒 |
月度,季度和年度的頻率都標記在指定時間段的末尾。通過為這些中的任何一個添加S
后綴,它們將在開頭標記:
代碼 | 描述 | 代碼 | 描述 |
---|---|---|---|
MS |
月份的起始 | BMS |
商業月份的起始 |
QS |
季度的起始 | BQS |
商業季度的起始 |
AS |
年度的起始 | BAS |
商業年度的起始 |
此外,你可以通過添加三個字母的月份代碼作為后綴,來更改用于標記任何季度或年度代碼的月份:
-
Q-JAN
,BQ-FEB
,QS-MAR
,BQS-APR
,以及其他。 -
A-JAN
,BA-FEB
,AS-MAR
,BAS-APR
,以及其他。
同樣,可以通過添加三個字母的星期代碼,來修改每周頻率的分割點:
-
W-SUN
,W-MON
,W-TUE
,W-WED
,以及其他。
除此之外,代碼可以與數字組合以指定其他頻率。例如,對于 2 小時 30 分鐘的頻率,我們可以將小時(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')
'''
頻率和偏移的使用的更多討論,請參閱 Pandas 文檔的“日期偏移”部分。
重采樣,平移和窗口化
使用日期和時間作為索引,來直觀地組織和訪問數據的能力,是 Pandas 時間序列工具的重要組成部分。一般情況下,索引數據的優勢(操作期間的自動對齊,直觀的數據切片和訪問等)仍然有效,并且 Pandas 提供了一些額外的時間序列特定的操作。
我們將以一些股票價格數據為例,看看其中的一些。由于 Pandas 主要是在金融環境中開發的,因此它包含一些非常具體的金融數據工具。
例如,附帶的pandas-datareader
包(可通過conda install pandas-datareader
安裝)知道如何從許多可用來源導入金融數據,包括 Yahoo finance,Google Finance 等。在這里,我們將加載 Google 的收盤價歷史記錄:
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 |
為簡單起見,我們僅使用收盤價:
goog = goog['Close']
在普通的 Matplotlib 樣板設置之后,我們可以使用plot()
方法將其可視化(參見第四章)):
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
goog.plot();
重采樣和轉換頻率
時間序列數據的一個常見需求,是以更高或更低的頻率重采樣。這可以使用resample()
方法,或更簡單的asfreq()
方法來完成。兩者之間的主要區別在于,resample()
基本上是數據聚合,而asfreq()
基本上是數據選擇。
看一下谷歌的收盤價,讓我們比較一下我們對數據下采樣時的回報。在這里,我們將在商業年度結束時重采樣數據:
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');
注意區別:在每一點,resample
報告前一年的平均值,而asfreq
報告年末的值。
對于上采樣,resample()
和asfreq()
在很大程度上是等效的,盡管resample
有更多可用的選項。在這種情況下,兩種方法的默認設置是將上采樣點留空,即填充 NA 值。就像之前討論過的pd.fillna()
函數一樣,asfreq()
接受一個method
參數來指定值的估算方式。在這里,我們將以每日頻率(即包括周末)重新采樣商業日數據:
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"]);
頂部面板是默認值:非工作日保留為 NA 值,并且不會顯示在圖表上。底部面板顯示填補空白的兩種策略之間的差異:向前填充和向后填充。
時間平移
另一種常見的時間序列特定的操作是按時間平移數據。Pandas 有兩個密切相關的計算方法:shift()
和tshift()
。簡而言之,它們之間的區別在于,shift()
平移數據,而tshift()
平移索引。在這兩種情況下,平移都指定為頻率的倍數。
在這里,我們使用shift()
和tshift()
來平移 900 天;
fig, ax = plt.subplots(3, sharey=True)
# 對數據應用頻率
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');
我們在這里看到shift(900)
將數據移動 900 天,將其中的一些移出圖的末尾(并在另一端留下 NA 值),而``tshift(900)將索引移動 900 天。
這種類型轉換的常見背景,是計算隨時間的差異。 例如,我們使用移位值來計算 Google 股票在數據集過程中的一年投資回報:
ROI = 100 * (goog.tshift(-365) / goog - 1)
ROI.plot()
plt.ylabel('% Return on Investment');
這有助于我們看到谷歌股票的總體趨勢:到目前為止,投資谷歌的最有利的時期(回想一下,不出所料)在其 IPO 后不久,以及在 2009 年中期經濟衰退期間。
滾動窗口
滾動統計量是 Pandas 實現的第三種時間序列特定的操作。
這些可以通過Series
和DataFrame
對象的rolling()
屬性來完成,它返回一個視圖,類似于我們在groupby
操作中看到的東西(參見“聚合和分組”)。這個滾動視圖默認提供許多聚合操作。
例如,以下是 Google 股票價格的一年中心化滾動均值和標準差:
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()
方法可用于自定義滾動計算。
在哪里了解更多
本節僅簡要概述了 Pandas 提供的時間序列工具的一些最基本功能;更完整的討論請參閱 Pandas 在線文檔的“時間序列/日期”部分。另一個優秀的資源是 Wes McKinney 的書籍《利用 Python 進行數據分析》(Python for Data Analysis (OReilly 2012))。
雖然現在已有幾年歷史,但它是 Pandas 用法的寶貴資源。特別是,本書重點講解商業和金融環境中的時間序列工具,并更多地關注商業日歷,時區和相關主題的特定細節。
與往常一樣,你也可以使用 IPython 幫助功能,來探索和嘗試可用于此處討論的函數和方法的更多選項。 我發現這通常是學習新 Python 工具的最佳方式。
示例:可視化西雅圖自行車數量
作為處理時間序列數據的一個更為復雜的例子,讓我們來看看西雅圖Fremont Bridge的自行車數量。這些數據來自于 2012 年底安裝的自動化自行車計數器,在橋的東西側人行道上設有感應式傳感器。每小時自行車計數可以從 http://data.seattle.gov/ 下載;這是數據集的直接鏈接。
截至 2016 年夏季,CSV 可以按如下方式下載:
# !curl -o FremontBridge.csv https://data.seattle.gov/api/views/65db-xm6k/rows.csv?accessType=DOWNLOAD
下載此數據集后,我們可以使用 Pandas 將 CSV 讀入DataFrame
。我們將指定,我們希望Date
作為索引,并且我們希望自動解析這些日期:
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'
列,來進一步處理此數據集:
data.columns = ['West', 'East']
data['Total'] = data.eval('West + East')
現在讓我們來看看這些數據的摘要統計信息:
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 |
可視化數據
我們可以通過可視化來獲得對數據集的一些了解。讓我們從繪制原始數據開始:
%matplotlib inline
import seaborn; seaborn.set()
data.plot()
plt.ylabel('Hourly Bicycle Count');
大約 25,000 小時的樣本太密集了,我們無法理解。我們可以通過將數據重采樣到更粗糙的網格,來獲得更多見解。讓我們按周重采樣:
weekly = data.resample('W').sum()
weekly.plot(style=[':', '--', '-'])
plt.ylabel('Weekly bicycle count');
這向我們展示了一些有趣的季節性趨勢:正如你所料,人們在夏天騎自行車比冬季更多,甚至在特定的季節內,自行車的使用每周也不同(可能取決于天氣;參見“深度:線性回歸”,我們在那里進一步探索它)。
另一種方便的匯總數據的方法是滾動均值,使用pd.rolling_mean()
函數。在這里,我們將對數據進行 30 天的滾動操作,確保窗口居中:
daily = data.resample('D').sum()
daily.rolling(30, center=True).sum().plot(style=[':', '--', '-'])
plt.ylabel('mean hourly count');
結果的鋸齒狀是由于窗口的硬截斷造成的。我們可以使用窗口函數(例如,高斯窗口)獲得更平滑的滾動平均版本。以下代碼指定了窗口的寬度(我們選擇了 50 天)和窗口內的高斯寬度(我們選擇了 10 天):
daily.rolling(50, center=True,
win_type='gaussian').sum(std=10).plot(style=[':', '--', '-']);
深挖數據
雖然這些平滑的數據視圖對于了解數據的總體趨勢很有用,但它們隱藏了許多有趣的結構。例如,我們可能希望,將平均流量視為一天中的時間的函數。我們可以使用“聚合和分組”中討論的GroupBy
功能來執行此操作:
by_time = data.groupby(data.index.time).mean()
hourly_ticks = 4 * 60 * 60 * np.arange(6)
by_time.plot(xticks=hourly_ticks, style=[':', '--', '-']);
每小時流量是一個強烈的雙峰分布,早上 8 點到晚上 5 點都是峰值。這可能是一個重要證據,通勤交通的一個重要組成部分跨越橋梁。西側人行道(通常用于前往西雅圖市中心)和東側的人行道(通常用于遠離西雅圖市中心)之間的差異,進一步證明了這一點,前者在早上是強烈的峰值,而后者在晚上是強烈的峰值。
我們也可能對事情如何基于一周中的某一天發生變化感到好奇。 同樣,我們可以通過一個簡單的groupby
來實現:
by_weekday = data.groupby(data.index.dayofweek).mean()
by_weekday.index = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']
by_weekday.plot(style=[':', '--', '-']);
這顯示了工作日和周末數量之間的強烈差異,周一至周五過橋的平均騎手數量是周六和周日的兩倍。
考慮到這一點,讓我們執行復合的GroupBy
,看一下工作日和周末的每小時趨勢。我們首先按照標記周末的標志,和一天中的時間分組:
weekend = np.where(data.index.weekday < 5, 'Weekday', 'Weekend')
by_time = data.groupby([weekend, data.index.time]).mean()
現在我們將使用“多個子圖”中描述的一些 Matplotlib 工具,來并排繪制兩個面板:
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=[':', '--', '-']);
結果非常有趣:我們在工作日期間看到雙峰通勤模式,在周末看到單峰休閑模式。更詳細地挖掘這些數據,并檢查天氣,溫度,一年中的時間,以及其他因素對人們通勤模式的影響,將會很有趣;進一步的討論請參閱我的博客文章“Is Seattle Really Seeing an Uptick In Cycling?”,它使用這些數據的一個子集。我們還將在“深入:線性回歸”中的建模環境中,回顧這個數據集。