《利用Python进行数据分析》读书笔记。
第 10 章:时间序列。
时间序列(time series),是一种重要的结构化数据形式,在很多领域都有应用。
时间序列数据的形式主要有:
- 时间戳(timestamp), 特定的时刻数据,时点数据。
- 固定时期(period),如2007年1月份, 2010年全年,阶段数据。
- 时间间隔(interval),由起始和终止的时间戳表示。 period可以看做特殊的interval。
- 过程时间。 相对于特定起始时间的时间点。比如,从放入烤箱时起,每秒钟的饼干直径。
过程时间可以用起始时间戳加上一系列整数(表示从起始时间开始经过的时间)来表示。
其中,最简单、最常见的时间序列是用时间戳进行索引。
pandas提供了一组标准的时间序列处理工具和算法,可以高效处理非常大的时间序列,进行
切片/切块,聚合,定期/不定期采样等操作。
这些数据对金融和经济数据尤为有用,也可以用于日志分析。
日期和时间数据类型及工具
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
python提供了关于日期(date),时间(time),日历(calendar)的模块。 主要有:
- date : 存储日期(年,月,日)
- time : 存储时间(时,分,秒,毫秒)
- datetime: 存储日期和时间
- timedelta: 存储datetime之差(日,秒,毫秒)
# datetime 以毫秒形式存储日期和时间
from datetime import datetime
now = datetime.now()
print(now)
now.year, now.month, now.day
# datetime.timedelta 表示两个datetime对象之间的时间差
delta = datetime(2011, 1, 7) - datetime(2008, 6, 24, 8, 15)
print(delta)
delta.days, delta.seconds
# datetime 可以 加减 timedelta, 得到一个新的 datetime
from datetime import timedelta
start = datetime(2011, 1, 7)
start + timedelta(12)
字符串和datetime的转换¶
stamp = datetime(2011, 1, 3)
str(stamp)
stamp.strftime('%Y-%m-%d')
value = '2011-01-03'
datetime.strptime(value, '%Y-%m-%d')
datestrs = ['7/6/2011', '8/6/2011']
[datetime.strptime(x, '%m/%d/%Y') for x in datestrs]
# dateutil包提供了一些更方便的方法
from dateutil.parser import parse
parse('2011-01-03')
parse('Jan 31, 1997 10:45 PM')
# dayfirst, 指定日在月前面
parse('6/12/2011', dayfirst=True)
# pandas中提供了处理成组日期的方法
datestrs
pd.to_datetime(datestrs)
# 可以自动处理缺失值
idx = pd.to_datetime(datestrs + [None])
idx
# NaT 表示 Not a Time
idx[2]
pd.isnull(idx)
时间序列基础
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
pandas中最基本的时间序列类型是以时间戳(字符串或datetime对象)为索引的Series。
from datetime import 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 = Series(np.random.randn(6), index=dates)
ts
# ts是一个 TimeSeries, 其索引是一个 DatetimeIndex
print(type(ts))
print(ts.index)
# 不同索引的时间序列之间的算数运算会自动对齐
ts + ts[::2]
# DatetimeIndex使用 datetime64, 存储时间戳的纳秒数值
# 其值是pandas的Timestamp对象
print(ts.index.dtype)
ts.index[0]
索引、选取、子集构建¶
TimeSeries是Series的一个子类,所以在索引以及数据选取方面跟Series一样。
stamp = ts.index[2]
ts[stamp]
# 更方便的用法是传入可以被解释为日期的字符串
print(ts['1/10/2011'])
print(ts['20110110'])
# 对于较长的时间序列,只需传入“年”或“年月”即可轻松选取数据切片
longer_ts = Series(np.random.randn(1000),
index=pd.date_range('1/1/2000', periods=1000))
longer_ts.head()
longer_ts['2001'].tail()
longer_ts['2001-05'].tail()
# 可以用不存在于该时间序列中的时间戳对其进行切片(即范围查询)
# 这里可以传入字符串日期、datetime或者Timestamp
longer_ts['1/6/1999':'1/11/2000']
dates = pd.date_range('1/1/2000', periods=100, freq='W-WED')
long_df = DataFrame(np.random.randn(100, 4),
index=dates,
columns=['Colorado', 'Texas', 'New York', 'Ohio'])
long_df.ix['5-2001']
带有重复索引的时间序列¶
dates = pd.DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000', '1/2/2000',
'1/3/2000'])
dup_ts = Series(np.arange(5), index=dates)
dup_ts
dup_ts.index.is_unique
# 索引得到的可能是标量值,也可能是切片
print(dup_ts['1/2/2000'])
print('----------------------------')
print(dup_ts['1/3/2000'])
# 对具有非唯一时间戳的数据进行聚合一个办法是使用groupby,并传入level = 0
grouped = dup_ts.groupby(level = 0)
grouped.mean()
日期的范围、频率以及移动
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
pandas中的时间序列一般被认为是不规则的,没有固定的频率。
但是有时候需要用相对固定的频率对数据进行分析,比如每月、每天等。
pandas提供了一整套标准时间序列频率以及用于重采样、频率推断、生成固定频率日期范围的工具。
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 = Series(np.random.randn(6), index=dates)
ts
# 通过resample重采样
r=ts.resample('D')
r
r.mean()
ts.resample('2D').sum()
生成日期范围¶
# 生成指定长度的DatetimeIndex
index = pd.date_range('4/1/2012', '6/1/2012')
index
pd.date_range(start='4/1/2012', periods=20)
pd.date_range(end='6/1/2012', periods=20)
# BM(business end of month),每月最后一个工作日
pd.date_range('1/1/2000', '12/1/2000', freq='BM')
pd.date_range('5/2/2012 12:56:31', periods=5)
pd.date_range('5/2/2012 12:56:31', periods=5, normalize=True)
ts = Series(np.random.randn(4),
index=pd.date_range('1/1/2000', periods=4, freq='M'))
ts
ts.shift(2)
ts.shift(-2)
# shift通常用于计算一个时间序列或多个时间序列(如DataFrame列)中的百分比变化。
ts / ts.shift(1) - 1
# 单纯的移位操作不会修改索引,所以部分数据会被丢弃
# 如果频率已知,则可以将其传给shift以实现对时间戳进行位移而不是只对数据移位
ts.shift(2,freq = 'M') #时间戳移动,而数据不动
ts.shift(3,freq = 'D')
ts.shift(1,freq = '3D')
ts.shift(1,freq = '90T')
通过偏移量对日期进行位移¶
pandas的日期偏移量还可以用在datetime或Timestemp对象上
from pandas.tseries.offsets import Day, MonthEnd
now = datetime(2011, 11, 17)
now + 3 * Day()
# 如果加的是锚点偏移量,第一次增量会将原日期向前滚动到符合频率规则的下一个日期
# 如果本来就是锚点,那么下一个就是下一个锚点
now + MonthEnd()
now + MonthEnd(2)
# 通过锚点偏移量的rollforward和rollback方法,可显示地将日期向前或向后“滚动”
offset = MonthEnd()
offset.rollforward(now)
offset.rollback(now)
# 日期偏移量还有一个巧妙的用法,即结合groupby使用这两个“滚动”方法
ts = Series(np.random.randn(20),
index=pd.date_range('1/15/2000', periods=20, freq='4d'))
ts.groupby(offset.rollforward).mean()
# 当然,更简单快速的方式是使用resample
ts.resample('M').mean()
时区处理
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
时间序列最让人不爽的就是对时区的处理。很多人已经用协调世界时(UTC,格林尼治时间接替者,目前是国际标准)来处理时间序列。
时区就是以UTC偏移量的形式表示的。
Python中,时区信息来自第三方库pytz,它可以使Python可以使用Olson数据库。
pandas包装了pytz功能。因此不用记忆API,只要记得时区名称即可。时区名可以在文档中找到。
# 通过交互的方式查看时区
import pytz
pytz.common_timezones[-5:]
tz = pytz.timezone('Asia/Shanghai')
tz
本地化和转换¶
默认情况下,pandas中的序列是单纯的(naive)时区,其索引的tz字段为None.
rng = pd.date_range('3/9/2012 9:30', periods=6, freq='D')
ts = Series(np.random.randn(len(rng)), index=rng)
print(ts.index.tz)
# 在生成日期范围的时候可以加上一个时区集
print(pd.date_range('3/9/2012',periods = 10,freq = 'D',tz = 'UTC'))
# 转换时区是通过tz_localize方法处理的
ts_utc = ts.tz_localize('UTC')
ts_utc
ts_utc.index
# 一旦被转换为某个特定时期,就可以用tz_convert将其转换到其他时区了
ts_utc.tz_convert('US/Eastern')
操作时区意识型(time zone-aware)Timestamp对象¶
跟时间序列和日期序列差不多,Timestamp对象也能被从单纯型(navie)本地化为time zone-aware,并从一个时区转换为另一个时区。
stamp = pd.Timestamp('2011-03-12 04:00')
stamp_utc = stamp.tz_localize('utc')
stamp_utc.tz_convert('US/Eastern')
# 创建Timestamp时可以传入时区信息
stamp_moscow = pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow')
stamp_moscow
# Timestamp的内在UTC时间戳(纳秒数)不会随时区的转换而变化
stamp_utc.value
stamp_utc.tz_convert('US/Eastern').value
# 30 minutes before DST transition
from pandas.tseries.offsets import Hour
stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern')
stamp
stamp + Hour()
# 90 minutes before DST transition
stamp = pd.Timestamp('2012-11-04 00:30', tz='US/Eastern')
stamp
stamp + 2 * Hour()
不同时区之间的运算¶
如果时间时间时区不同,那么结果就会是UTC时间,由于时间戳其实是以UTC储存的,索引计算很方便。
rng = pd.date_range('3/7/2012 9:30', periods=10, freq='B')
ts = Series(np.random.randn(len(rng)), index=rng)
ts
#注意naive是不能直接转换为时区的,必须先转换为localize再进行转换
ts1 = ts[:7].tz_localize('Europe/London')
ts2 = ts1[2:].tz_convert('Europe/Moscow')
result = ts1 + ts2
#自动转换为UTC
result.index
时期及其算数运算
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
时期(period),表示时间区间,比如几日、几月、几年等。
Period类所表示的就是这种数据类型,其构造函数需要用到一个字符串或整数,以及频率。
p = pd.Period(2007, freq='A-DEC')
p
# 位移
p + 5
# 相同频率的Period可以进行加减,不同频率是不能加减的
pd.Period('2014', freq='A-DEC') - p
# period_range函数,可用于创建规则的时期范围
rng = pd.period_range('1/1/2000', '6/30/2000', freq='M')
rng
# 将 PeriodIndex 作为索引
Series(np.random.randn(6), index=rng)
# 直接使用一组字符串构建
values = ['2001Q3', '2002Q2', '2003Q1']
index = pd.PeriodIndex(values, freq='Q-DEC')
index
时期的频率转换¶
Period和PeriodIndex对象都可以通过其asfreq方法转换为别的频率。
# 将年度时期转换为月度时期
p = pd.Period('2007', freq='A-DEC')
p.asfreq('M', how='start')
p.asfreq('M', how='end')
p = pd.Period('2007', freq='A-JUN')
p.asfreq('M', 'start')
# 高频率时期转换为低频率时期
p = pd.Period('Aug-2007', 'M')
# 注意, 2007-08,属于周期2008年
p.asfreq('A-JUN')
# PeriodIndex 或 TimeSeries 的频率转换
rng = pd.period_range('2006', '2009', freq='A-DEC')
ts = Series(np.random.randn(len(rng)), index=rng)
ts
ts.asfreq('M', how='start')
ts.asfreq('B', how='end')
按季度计算的时期频率¶
季度型数据在会计、金融等领域中很常见。许多季度型数据都会涉及“财年末”的概念,通常是一年12个月中某月的最后一个日历日或工作日。就这一点来说,“2012Q4”根据财年末的会有不同含义。pandas支持12种可能的季度频率,即Q-JAN、Q-DEC。
p = pd.Period('2012Q4', freq='Q-JAN')
p
p.asfreq('D', 'start')
p.asfreq('D', 'end')
p4pm = (p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
p4pm
p4pm.to_timestamp()
# period_range还可以用于生产季度型范围,季度型范围的算数运算也跟上面是一样的
rng = pd.period_range('2011Q3', '2012Q4', freq='Q-JAN')
ts = Series(np.arange(len(rng)), index=rng)
ts
new_rng = (rng.asfreq('B', 'e') - 1).asfreq('T', 's') + 16 * 60
ts.index = new_rng.to_timestamp()
ts
Timestamp与Period的互相转换¶
通过to_period方法,可以将由时间戳索引的Series和DataFrame对象转换为以时期为索引的对象
rng = pd.date_range('1/1/2000', periods=3, freq='M')
ts = Series(randn(3), index=rng)
pts = ts.to_period()
ts
pts
# 由于时期指的是非重叠时间区间,因此对于给定的频率,一个时间戳只能属于一个时期。
# 新PeriodIndex的频率默认是从时间戳推断而来的,当然可以自己指定频率,结果中允许存在重复时期
rng = pd.date_range('1/29/2000', periods=6, freq='D')
ts2 = Series(randn(6), index=rng)
ts2.to_period('M')
pts = ts.to_period()
pts
# 转换为时间戳
pts.to_timestamp(how='end')
通过数组创建PeriodIndex¶
固定频率的数据集通常会将时间信息分开存放在多个列中。例如下面的这个宏观经济数据集中,年度和季度就分别存放在不同的列中。
data = pd.read_csv('data/ch08/macrodata.csv')
data.year.head()
data.quarter.head()
# 使用 year, quarter这两个数组,以及 一个频率 Q-DEC, 构建一个 PeriodIndex
index = pd.PeriodIndex(year=data.year, quarter=data.quarter, freq='Q-DEC')
index
# 将 PeriodIndex 作为 data 的索引
data.index = index
data.infl.head()
重采样及频率转换
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
pandas对象都提供了resample方法,用于重采样。
对于时间序列来说,重采样(resampling)指的是将时间序列从一个频率转换到另一个频率的过程。
其中两类特殊的重采样是:将高频率数据聚合到低频率称为降采样(downsampling),而将低频率数据转换到高频率称为升采样(uosampling)。
并不是所有的重采样都能被划分到这两类中,比如将W-WED转换为W-FRI既不是降采样也不是升采样。
rng = pd.date_range('1/1/2000', periods=100, freq='D')
ts = Series(randn(len(rng)), index=rng)
ts.resample('M').mean()
ts.resample('M', kind='period').mean()
resample方法的主要参数包括:
降采样¶
将数据的频率降低称为降采样,也就是将数据进行聚合。 一个数据点只能属于一个聚合时间段,所有时间段的并集组成整个时间帧。 在进行降采样时,应该考虑如下:
- 各区间那便是闭合的
- 如何标记各个聚合面元,用区间的开头还是结尾
# 1分钟数据
rng = pd.date_range('1/1/2000', periods=12, freq='T')
ts = Series(np.arange(12), index=rng)
ts
# 聚合到5分钟
# 注意:默认情况下,为 闭-开区间
ts.resample('5min').last()
# 指定closed = 'right' 改为 开- 闭 区间
ts.resample('5min', closed='right').last()
# 指定使用右侧标记作为标签
ts.resample('5min', closed='right', label='right').last()
# 对结果索引做一些位移
ts.resample('5min', loffset='-1s').last()
# 也可以通过调用结果对象的shift方法来实现。
OHLC重采样¶
对于ohlc数据,pandas做了专门处理
ts.resample('5min').ohlc()
通过groupby进行重采样¶
另一种方法是使用pandas的groupby功能。例如,你打算根据月份或者周几进行分组,只需传入一个能够访问时间序列的索引上的这些字段的函数即可:
rng = pd.date_range('1/1/2000', periods=100, freq='D')
ts = Series(np.arange(100), index=rng)
ts.groupby(lambda x: x.month).mean()
ts.groupby(lambda x: x.weekday).mean()
升采样和插值¶
将数据从低频率转换到高频率时,就不需要聚合了。
frame = DataFrame(np.random.randn(2, 4),
index=pd.date_range('1/1/2000', periods=2, freq='W-WED'),
columns=['Colorado', 'Texas', 'New York', 'Ohio'])
frame
# 重采样到日频率,默认会引入缺失值
df_daily = frame.resample('D')
df_daily.last()
# 可以跟fillna和reindex一样进行填充
frame.resample('D').ffill()
# 只填充指定的时期数(目的是限制前面的观测值的持续使用距离)
frame.resample('D').ffill(limit=2)
# 注意,新的日期索引完全没必要跟旧的相交,注意这个例子展现了数据日期可以延长
frame.resample('W-THU').ffill()
通过时期进行重采样¶
对那些使用时期索引的数据进行重采样是一件非常简单的事情。
frame = DataFrame(np.random.randn(24, 4),
index=pd.period_range('1-2000', '12-2001', freq='M'),
columns=['Colorado', 'Texas', 'New York', 'Ohio'])
frame[:5]
# 升采样要稍微麻烦些,因为你必须决定在新的频率中各区间的哪端用于放置原来的值
# 就像asfreq方法一样,convention默认为'end',可设置为'start'
# Q-DEC:季度型(每年以12月结束)
annual_frame = frame.resample('Q-DEC').mean()
annual_frame
annual_frame.resample('Q-DEC').ffill()
# Q-DEC: Quarterly, year ending in December
# note: output changed, default value changed from convention='end' to convention='start' + 'start' changed to span-like
# also the following cells
annual_frame.resample('Q-DEC', convention='start').ffill()
由于时期指的是时间区间,所以升采样和降采样的规则就比较严格:
- 在降采样中,目标频率必须是源频率的子时期(subperiod)
- 在升采样中,目标频率必须是原频率的超时期(superperiod)
如果不满足这些条件,就会引发异常,主要影响的是按季、年、周计算的频率。
例如,由Q-MAR定义的时间区间只能升采样为A-MAR、A-JUN等
annual_frame.resample('Q-MAR').ffill()
时间序列绘图
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
pandas时间序列的绘图功能在日期格式化方面比matplotlib原生的要好。
import matplotlib.pyplot as plt
plt.rc('figure', figsize=(12, 4))
close_px_all = pd.read_csv('data/ch09/stock_px.csv', parse_dates=True, index_col=0)
close_px = close_px_all[['AAPL', 'MSFT', 'XOM']]
close_px = close_px.resample('B').ffill()
close_px.info()
close_px['AAPL'].plot()
close_px.ix['2009'].plot()
close_px['AAPL'].ix['01-2011':'03-2011'].plot()
appl_q = close_px['AAPL'].resample('Q-DEC').ffill()
appl_q.ix['2009':].plot()
移动窗口函数
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
在移动窗口(可以带有指数衰减权数)上计算的各种统计函数也是一类常见于时间序列的数组变换。
称为移动窗口函数(moving window function),其中还包括那些窗口不定长的函数(如指数加权移动平均)。
跟其他统计函数一样,移动窗口函数也会自动排除缺失值。这样的函数通常需要指定一些数量的非NA观测值。
close_px_all = pd.read_csv('data/ch09/stock_px.csv', parse_dates=True, index_col=0)
close_px = close_px_all[['AAPL', 'MSFT', 'XOM']]
close_px = close_px.resample('B').ffill()
close_px.info()
# rolling_mean是其中最简单的一个。它接受一个TimeSeries或DataFrame以及一个window(表示期数)
close_px = close_px.asfreq('B').fillna(method='ffill')
close_px.AAPL.plot()
# pd.rolling_mean is deprecated
#pd.rolling_mean(close_px.AAPL, 250).plot()
# 250均线
close_px.AAPL.rolling(window=250,center=False).mean().plot()
# 两个 figure
plt.figure()
#close_px.AAPL.plot()
# 250期的标准差
close_px.AAPL.rolling(window=250,center=False).std().plot()
# 计算前面所有数的std,比如min_periods = 10时,计算前10个数的,
# min_periods = 20时,计算前20个数的,直到min_periods = 250为止,
# 这就是所谓的“指定的非NA观测值”
close_px.AAPL.rolling(window=250,min_periods = 10).std().plot()
要计算扩展窗口平均(expanding window mean),可以将扩展窗口看作一个特殊的窗口,
其长度与时间序列一样,但只需一期或多期即可计算一个值。
# Define expanding mean in terms of rolling_mean
expanding_mean = lambda x: rolling_mean(x, len(x), min_periods=1)
close_px.rolling(window=60,center=False).mean().plot(logy=True)
plt.close('all')
pandas中的移动窗口和指数加权函数:
指数加权函数¶
另一种使用固定大小窗口及相等权数观测值的方法是,
定义一个衰减因子(decay factor)常量,以便使近期的观测值拥有更大的权数。
衰减因子的定义方式有很多,比较流行的是使用时间间隔(span),
它可以使结果兼容于窗口大小等于时间间隔的简单移动窗口函数。
由于指数加权统计赋予近期的观测值更大的权重,因此更能适应较快的变化。
fig, axes = plt.subplots(nrows=2, ncols=1, sharex=True, sharey=True,
figsize=(12, 7))
aapl_px = close_px.AAPL['2005':'2009']
ma60 = aapl_px.rolling(window=60,min_periods=50,center=False).mean()
ewma60 = aapl_px.ewm(span=60,min_periods=0,adjust=True,ignore_na=False).mean()
aapl_px.plot(style='k-', ax=axes[0])
ma60.plot(style='k--', ax=axes[0])
aapl_px.plot(style='k-', ax=axes[1])
ewma60.plot(style='k--', ax=axes[1])
axes[0].set_title('Simple MA')
axes[1].set_title('Exponentially-weighted MA')
二元移动窗口函数¶
有些统计运算(如相关系数和协方差)需要在两个时间序列上执行。
比如,金融分析师常常对某只股票对某个参数(如标普500指数)的相关系数感兴趣。
我们可以通过计算百分比变化并使用rolling_corr的方式得到该结果。
spx_px = close_px_all['SPX']
spx_rets = spx_px / spx_px.shift(1) - 1
returns = close_px.pct_change()
corr = returns.AAPL.rolling(window=125,min_periods=100).corr(other=spx_rets)
corr.plot()
# 一次处理多个
corr = returns.rolling(window=125,min_periods=100).corr(other=spx_rets)
corr.plot()
用户自定义的移动窗口函数¶
rolling_apply函数使你能够在移动窗口上应用自己设计的数组函数。
唯一的要求就是:该函数要能从数组的各个片段中产生单个值。
比如,当用rolling_quantile计算样本分位数时,可能对样本中特定值的百分等级感兴趣。
from scipy.stats import percentileofscore
score_at_2percent = lambda x: percentileofscore(x, 0.02)
result = returns.AAPL.rolling(window=250,center=False).apply(func=score_at_2percent)
result.plot()
性能和内存使用方面的注意事项
%pylab inline
import pandas as pd
from datetime import datetime
from pandas import Series, DataFrame
TimeSeries和Period都是以64位整数表示的(即NumPy的datetime64数据类型)。
也就是说,对于每个数据点,其时间戳需要占用8字节内存。
因此,含有一百万个float64数据点的时间序列需要占用大约16MB的内存空间。由于pandas会尽量在多个时间序列之间共享索引,所以创建现有时间序列的视图不会占用更多内存。
此外,低频率索引(日以上)会被存放在一个中心缓存中,所以任何固定频率的索引都是该日期缓存的视图。所以。如果你有一个很大的低频率时间序列,索引所占用的内存空间将不会很大。
性能方面,pandas对数据对齐(两个不同索引的ts1 + ts2的幕后工作)和重采样运算进行了高度优化。
# 下面这个例子将一亿个数据点聚合为OHLC
rng = pd.date_range('1/1/2000', periods=10000000, freq='10ms')
ts = Series(np.random.randn(len(rng)), index=rng)
ts.head()
%timeit ts.resample('15min').ohlc()
rng = pd.date_range('1/1/2000', periods=10000000, freq='1s')
ts = Series(np.random.randn(len(rng)), index=rng)
%timeit ts.resample('15s').ohlc()