时间序列处理¶
Pandas 本身是金融从业者开发的,在时间序列方面具有优势。一般而言,时间有三种表达方法:
- 时间戳 事件发生的时间点 (如 2024年11月9日 9:00点整).
- 时间段 或者 时间周期 时间长度(2024年,从1月1日到12月31日)
- 时间跨度 或者 历时指一个事件的 (如实验进行了 22.56 秒)
python中的时间和日期方法¶
Python 中有很多关于时间,日期,时间跨度和时间增量的用法。虽然pandas更有利于数据处理,但基本的python关于时间的表达对比很有用处。
原生Python中关于时间和日期的表达: datetime 和 dateutil¶
Python基本的时间和日期在内置 datetime 模块中.
第三方库 dateutil 模块与上面模块配合也非常有用。
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'
事件类型的数组: NumPy的 datetime64类型¶
datetime64类型是 64-bit 整数编码,这样可以是内存非常紧凑,节约空间。
import numpy as np
date = np.array('2024-07-04', dtype=np.datetime64)
date
array('2024-07-04', dtype='datetime64[D]')
只要有了这个格式,我们就可以进行向量化操作:
date + np.arange(12)
array(['2024-07-04', '2024-07-05', '2024-07-06', '2024-07-07',
'2024-07-08', '2024-07-09', '2024-07-10', '2024-07-11',
'2024-07-12', '2024-07-13', '2024-07-14', '2024-07-15'],
dtype='datetime64[D]')
NumPy datetime64 数组非常好的平衡了时间精度与时间跨度问题。
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关于时间的一些基本规范:
| 代码 | 含义 | 时间跨度 (相对) | 时间跨度 (绝对) |
|---|---|---|---|
Y |
Year | ± 9.2e18 years | [9.2e18 BC, 9.2e18 AD] |
M |
Month | ± 7.6e17 years | [7.6e17 BC, 7.6e17 AD] |
W |
Week | ± 1.7e17 years | [1.7e17 BC, 1.7e17 AD] |
D |
Day | ± 2.5e16 years | [2.5e16 BC, 2.5e16 AD] |
h |
Hour | ± 1.0e15 years | [1.0e15 BC, 1.0e15 AD] |
m |
Minute | ± 1.7e13 years | [1.7e13 BC, 1.7e13 AD] |
s |
Second | ± 2.9e12 years | [ 2.9e9 BC, 2.9e9 AD] |
ms |
Millisecond | ± 2.9e9 years | [ 2.9e6 BC, 2.9e6 AD] |
us |
Microsecond | ± 2.9e6 years | [290301 BC, 294241 AD] |
ns |
Nanosecond | ± 292 years | [ 1678 AD, 2262 AD] |
ps |
Picosecond | ± 106 days | [ 1969 AD, 1970 AD] |
fs |
Femtosecond | ± 2.6 hours | [ 1969 AD, 1970 AD] |
as |
Attosecond | ± 9.2 seconds | [ 1969 AD, 1970 AD] |
日常工作的时间序列,默认单位为纳秒 datetime64[ns], 用它表示可以满足大部分需求。
pandas中间的日期和时间:理想与现实的最佳解决方案¶
Pandas 关于日期和时间的处理方法全部通过 Timestamp 对象实现,其底层运用 numpy.datetime64有效存储和向量化接口,将Python中的datetime 和dateutil易用性结合起来。Pandas通过一组Timestamp对象,可以创建一个作为Series 或者DataFrame索引的DatetimeIndex.
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 使用Timestamp类型做索引非常方便,如,我们可以直接通过时间索引创建一个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
这种情况下,可以直接使用时间来进行切片
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(小时)代码一样,我们可以使用这些代码来指定任何所需的频率间距。下表总结了可用的主要代码:
| Code | Description | Code | Description |
|---|---|---|---|
D |
Calendar day | B |
Business day |
W |
Weekly | ||
M |
Month end | BM |
Business month end |
Q |
Quarter end | BQ |
Business quarter end |
A |
Year end | BA |
Business year end |
H |
Hours | BH |
Business hours |
T |
Minutes | ||
S |
Seconds | ||
L |
Milliseonds | ||
U |
Microseconds | ||
N |
nanoseconds |
每月、每季度和每年的频率都在指定周期结束时标记。通过在其中任何一个后添加“ S ”后缀,它们将改为在周期开始时标记:
| Code | Description | Code | Description |
|---|---|---|---|
MS |
Month start | BMS |
Business month start |
QS |
Quarter start | BQS |
Business quarter start |
AS |
Year start | BAS |
Business year start |
此外,您可以通过在季度或年度代码后添加一个三字母月份代码作为后缀来更改所使用的月份:
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')
所有这些短代码都指的是 Pandas 时间序列偏移的特定实例,可以在 pd.tseries.offsets 模块中找到。例如,我们可以直接创建一个工作日偏移,如下所示:
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 文档中的 “DateOffset” 部分 。
重采样、移位和加窗¶
使用日期和时间作为索引来直观地组织和访问数据的能力是 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='2024',
data_source='stooq')
goog.head()
| Open | High | Low | Close | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 2023-12-29 | 140.68 | 141.435 | 139.9000 | 140.93 | 14880961 |
| 2023-12-28 | 141.85 | 142.270 | 140.8283 | 141.28 | 12192549 |
| 2023-12-27 | 142.83 | 143.320 | 141.0512 | 141.44 | 17288358 |
| 2023-12-26 | 142.98 | 143.945 | 142.5001 | 142.82 | 11170066 |
| 2023-12-22 | 142.13 | 143.250 | 142.0550 | 142.72 | 18513524 |
为了简便,我们只使用收盘价:
goog = goog['Close']
在完成常规的 Matplotlib 设置后,我们可以使用 plot() 方法来可视化这一点
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
goog.plot();
重采样和转换频率¶
时间序列数据的一个常见需求是以更高或更低的频率重新采样。 这可以使用 resample() 方法完成,或者使用更简单的 asfreq() 方法。 两者之间的主要区别在于,resample() 本质上是数据聚合,而 asfreq() 本质上是数据选择。
看看 Google 的收盘价,我们来比较一下在对数据进行降采样时两者的返回结果。 这里我们将按财政年度末重新采样数据:
goog.plot(alpha=0.5, style='-')
goog.resample('BYE').mean().plot(style=':')
goog.asfreq('BYE').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 值,并且不会出现在图上。底部面板显示了两种填补空白策略之间的差异:向前填充和向后填充。
fig, ax = plt.subplots(3, sharey=True)
# apply a frequency to the data
goog = goog.asfreq('D', method='pad')
goog.plot(ax=ax[0])
goog.shift(900).plot(ax=ax[1])
goog.shift(-900).plot(ax=ax[2])
# legends and annotations
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(['shift(-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 天。
这种类型的位移的一个常见场景是在计算随时间变化的差异时。例如,我们使用位移后的值来计算整个数据集期间谷歌股票的一年投资回报率:
ROI = 100 * (goog.shift(-365) / goog - 1)
ROI.plot()
plt.ylabel('% Return on Investment');
这有助于我们看到谷歌股票的整体趋势:到目前为止,投资谷歌最赚钱的时期是(事后看来并不意外)其首次公开募股后不久,以及2009年经济衰退中期。
滚动窗口¶
滚动统计是 Pandas 实现的第三类特定于时间序列的操作。
这些操作可以通过 Series 和 DataFrame 对象的 rolling() 属性来完成,它返回的视图类似于我们在 groupby 操作中看到的视图(参见 聚合与分组部分)。
这个滚动视图默认提供了多种聚合操作。
例如,这里是谷歌股票价格的一年期中心滚动均值和标准差:
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 数据分析(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 中。我们将指定希望将日期作为索引,并希望这些日期被自动解析:
data = pd.read_csv('data/FremontBridge.csv', index_col='Date', parse_dates=True)
data.head()
C:\Users\getwa\AppData\Local\Temp\ipykernel_20856\3227980017.py:1: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
data = pd.read_csv('data/FremontBridge.csv', index_col='Date', parse_dates=True)
| Fremont Bridge Sidewalks, south of N 34th St | Fremont Bridge Sidewalks, south of N 34th St Cyclist West Sidewalk | Fremont Bridge Sidewalks, south of N 34th St Cyclist East Sidewalk | |
|---|---|---|---|
| Date | |||
| 2012-10-02 13:00:00 | 55.0 | 7.0 | 48.0 |
| 2012-10-02 14:00:00 | 130.0 | 55.0 | 75.0 |
| 2012-10-02 15:00:00 | 152.0 | 81.0 | 71.0 |
| 2012-10-02 16:00:00 | 278.0 | 167.0 | 111.0 |
| 2012-10-02 17:00:00 | 563.0 | 393.0 | 170.0 |
data.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 105144 entries, 2012-10-02 13:00:00 to 2024-09-30 12:00:00 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Fremont Bridge Sidewalks, south of N 34th St 105116 non-null float64 1 Fremont Bridge Sidewalks, south of N 34th St Cyclist West Sidewalk 105116 non-null float64 2 Fremont Bridge Sidewalks, south of N 34th St Cyclist East Sidewalk 105116 non-null float64 dtypes: float64(3) memory usage: 3.2 MB
为了方便起见,我们将进一步处理这个数据集,通过缩短列名并添加一个“总计”列:
data.columns = ['Total','West', 'East']
#data['Total'] = data.eval('West + East')
现在让我们来看一下这组数据的汇总统计:
data.dropna().describe()
| Total | West | East | |
|---|---|---|---|
| count | 105116.000000 | 105116.000000 | 105116.000000 |
| mean | 106.545569 | 45.701967 | 60.843601 |
| std | 132.098939 | 59.570117 | 82.227507 |
| min | 0.000000 | 0.000000 | 0.000000 |
| 25% | 13.000000 | 5.000000 | 7.000000 |
| 50% | 60.000000 | 26.000000 | 32.000000 |
| 75% | 146.000000 | 63.000000 | 80.000000 |
| max | 1097.000000 | 667.000000 | 850.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=[':', '--', '-']);
结果非常有趣:我们看到工作周期间有双峰的通勤模式,而周末则有单峰的休闲模式。 深入分析这些数据并研究天气、温度、季节以及其他因素对人们通勤模式的影响会很有趣;更多讨论请参见博客文章《西雅图的骑行真的在增加吗?》,该文章使用了这部分数据的子集。