《python for data analysis》第十章,时间序列

时间:2021-12-11 22:12:34
《 python for data analysis 》一书的第十章例程, 主要介绍时间序列(time series)数据的处理。
label:
1. datetime object、timestamp object、period object
2. pandas的Series和DataFrame object的两种特殊索引:DatetimeIndex 和 PeriodIndex
3. 时区的表达与处理
4. imestamp object、period object的频率概念,及其频率转换
5. 两种频率转换:单个时间object——asfreq;以时间object为索引的时间序列——resample
6. 时间序列的移动窗口(rolling)
# -*- coding:utf-8 -*-
# 《python for data analysis》 第十章
# 时间序列
import pandas as pd
import numpy as np
import time
from datetime import datetime, timedelta
import matplotlib.pyplot as plt start = time.time()
np.random.seed(10) # 1、日期和时间数据类型及工具
# datetime以 year - month - date hour : minute : second 的格式存储时间
tnow = datetime.now()
print(tnow)
# 可调用datetime中的各个部分:年月日时分秒
print(tnow.year)
print(tnow.day)
# datetime.timedelta 可用于表示两个 datetime object之间的时间差, 以日,秒,毫秒的形式
print(tnow - timedelta(1, 10, 1000)) # 1天10秒100微秒之前
print('\n')
# 1.1、字符串和datetime的相互转换
# (1)str和strftime可以将datetime object转换成字符串
print(str(tnow)) # 按照年月日时分秒的格式全部转换
print(tnow.strftime('%Y-%m-%d')) # 按照年月日格式导出
print('')
# (2)strptime可以将字符串转换成datetime object
print(datetime.strptime('1999/1/2', '%Y/%m/%d'))
print(datetime.strptime('1999/1/2', '%Y/%d/%m'))
print('')
# datetime.strptime方法进行时间格式解析最为精准,但是需要自定义格式。
# 第三方包dateutil中的parser.parse方法可自适应地去进行时间格式解析,但有些时候可能会出现问题
from dateutil.parser import parse print('Jan 22, 1999, 11:23, pm')
print(parse('Jan 22, 1999, 11:23, pm'))
print('')
# 上述方法均是对单个字符串进行解析,pandas的to_datetime方法可以成组解析
timestring = ['2012/1/1', '2008/8/8']
df = pd.to_datetime(timestring)
print(df)
# pd.to-datetime还可以对缺失值进行处理,自动转成NaT(not a time)
timestring = timestring + [None]
df = pd.to_datetime(timestring)
print(df)
print('------------------------------------------------↑, section1\n\n')
# 2、时间序列基础
# pandas最基本的时间序列类型是以时间戳(timestamp)为索引(index)的Series
dates = [datetime.strptime('2000/1/' + str(i), '%Y/%m/%d') for i in range(1, 11)]
print(dates)
ts = pd.Series(np.random.randn(10), index=dates)
print(ts)
print(type(ts.index[1])) # time series中的每个index均为timestamp object
print('')
# 2.1、索引、选取、子集构造(time series的)
# time series也是series的一种,所以原来的series的各种索引、切片方法等均适用
print(ts[2:3])
print('')
# 特别的,time series还可以通过日期(字符串)进行索引
print(ts['2000/01/05'])
# 对于时间跨度很大的时间序列,切片方式更加丰富
print('\n')
ts = pd.Series(np.random.randn(1000), index=pd.date_range('1/1/2000', periods=1000))
print(ts.describe())
# 通过年份切片
print(ts['2001'].describe())
# 通过年月切片
print(ts['2001/01'].describe())
# 时间段切片
print(ts['2002/05/01':'2002/05/06'])
print('\n')
# 上述索引、切片方法对dataframe同样适用
# 2.2、带有重复索引的时间序列
date = ['2001/02/01', '2001/02/01', '2001/02/02']
ts = pd.Series(range(3), index=date)
print(ts)
print(ts['2001/02/01']) # 重复索引返回切片
print(ts['2001/02/02']) # 非重复索引返回标量值
print('')
# 通过聚合可以消除重复索引
print(ts.groupby(level=0).count())
print('---------------------------------↑, section2\n\n')
# 3、日期的范围、频率以及移动
# resample的详细介绍见section 6
# 3.1、生成日期范围
# 用pd.date_range生成指定长度的DateTime Index
# (1)给定起止时间
index = pd.date_range('2000/1/1', '2000/2/1')
print(index)
print('')
# (2)给定起始时间(或终止时间)加时间长度
index = pd.date_range(start='2000/1/1', periods=10) # 从2000/1/1开始,长度10天
print(index)
print('')
index = pd.date_range(end='2000/1/28', periods=10) # 到2000/1/28为止,长度10天
print(index)
# 上述函数中为给出生成时间序列的间隔,为缺省值1天,通过freq关键字可以显式指定
print('')
print(pd.date_range('2000/1/1', '2000/12/28', freq='BM')) # BM表示每个月的最后一个工作日
# freq的可选项:D(每天)、B(每工作日)、H(每小时)、T or min(每分钟)、S(每秒)……P314~315
# 3.2、频率与日期偏移量
# 上述提到的频率代码可以 以字符串形式 *组合
print('')
print(pd.date_range('2000/1/10', '2000/1/15', freq='6H30T10S')) # 以6小时30分钟10秒的间隔生成时间序列
# WOM日期(week of month)可以表示每月中的某些日子
print('')
print(pd.date_range('2000/1/10', '2001/1/10', freq='WOM-3FRI')) # 每个月的第三个周五
# 3.3、移动(超前或滞后)数据
print('\n')
ts = pd.DataFrame(
{'a': range(6),
'b': np.random.randn(6)}
)
ts.index = pd.date_range('2000/10/1', '2000/10/6')
print(ts)
print(ts.shift(2)) # 数据往时间增大方向推移2天
print(ts.shift(-2)) # 往小了方向推移
# 上述函数中仅传入推移大小,则移动数据
# 若还传入频率,则移动time index
print(ts.shift(1, freq='M')) # 数据不动,时间序列全部推移到本月最后一天
print(ts.shift(1, freq='3D')) # 数据不动,时间序列全部推移3天
# shift最大的功用在于可以计算数据的百分比变化
print('')
print(ts / ts.shift(1) - 1)
# 时间偏移量还可以直接作用于timestamp和datetime object
# 先导入时间偏移量
from pandas.tseries.offsets import Day, BMonthBegin print(tnow + 3 * Day()) # 向时间前进方向偏移3天
print(tnow + BMonthBegin()) # 偏移至下个月的第一个工作日
print('------------------------------------↑, section 3\n\n') # 4、时区处理
# UTC,协调世界时间。时区以UTC的偏移量表示
# 4.1、本地化与转换
# 先按照之前的方法创建一个time series
ts = pd.Series(range(2))
ts.index = pd.date_range(tnow, periods=2, normalize=True)
print(ts)
# 上述创建过程中未指定时区,则默认时区未None
print('\ntime zone is %s' % ts.index.tz)
# 给时间序列加上时区数据,称为本地化。有两种方法。
# (1)tz_localize方法
ts_china = ts.tz_localize('Asia/Shanghai')
print(ts_china) # time index 变成 本地时间+UTC偏移 的格式
print(ts_china.index.tz) # 该时间序列的时区已经附上上海
# (2)在创建序列时直接显示指定时区
ts_china = pd.Series(range(2), index=pd.date_range(tnow, periods=2, normalize=True, tz='Asia/Shanghai'))
print(ts_china)
print(ts_china.index.tz)
# 时区可以通过tz_convert方法进行转换
print(ts_china.tz_convert('UTC')) # 北京时间转换成UTC时间
print('\n')
# datetime、timestamp、DatetimeIndex这些objects均可以使用tz_localize、tz_convert这些方法
# 4.2、操作时区意识型TimeStamp Object
stamp = pd.Timestamp(str(tnow))
stamp_china = stamp.tz_localize('Asia/Shanghai')
print(stamp_china)
stamp_utc = stamp_china.tz_convert('utc')
print(stamp_utc)
# timestamp object 中有一个属性保存了utc时间戳值,即当前时间相对于UNIX纪元(1970年1月1日)的时间位移,以ns为单位
print(stamp_utc.value)
print(stamp_china.value) # 这两个时间为stamp在不同时区的显示时间,故绝对位移相等,均为stamp相对UNIX纪元的时间位移
print('\n')
# 4.3、不同时区之间的运算
# 不同时区的时间序列的运算结果均以UTC标准显示
ts = pd.Series(range(2), index=pd.date_range(tnow, periods=2))
ts1 = ts.tz_localize('US/Eastern')
ts2 = ts.tz_localize('Asia/Shanghai')
print(ts1.index)
print(ts2.index)
print((ts1 + ts2).index)
print('----------------------------------↑, section4\n\n') # 5、时期及其算术运算
# 一种新的object,时期(period)
# timestamp object表示某一时刻,相对的,period object是用来表示某一段周期的
# 创建一个period object
p = pd.Period(2000, freq='A-DEC') # 以12月结尾的周期年
print(p)
# period object支持加减整数实现位移
print(p - 2) # 2000年往前推2年
print(p + 1) # 2000年的后面一年
# 同频率的period还支持加减
print(pd.Period(2005, freq='M') - pd.Period(2000, freq='M')) # 5年共60个月
# period_range方法可以创建一组时期范围
plist = pd.period_range('2000Q1', '2002Q1', freq='Q') # 从2000年1季度到2002年1季度,季度间隔
print(plist)
print('')
# 以datetime object为内容的index称为datetime index,同理,也有period index
ts = pd.Series(np.random.randn(len(plist)), index=plist)
print(ts)
print('')
# period index的构造还可以通过字符串转换完成
string = ['2000Q1', '2000Q2', '2000Q3'] # 2000年的前3季度
p = pd.PeriodIndex(string, freq='Q-DEC')
print(p)
print('\n')
# 5.1、时期的频率转换
# period 和 period index 均可通过asfreq进行频率转换
p = pd.Period(2000, freq='Q-DEC')
print(p) # 2000年第一季度
print(p.asfreq('M')) # 季转月,默认最后一个月
print(p.asfreq('M', how='start')) # 用how关键字显式指定第一个月
print(p.asfreq('A')) # 季转年
print('')
# 同理,period index或者包含period index的time series也可以这么操作
print(ts.asfreq('B', how='start')) # ts时间序列中的period index(以季度为时期)转成以每季里面第一个工作日为时期的period index
print('')
# 5.2、按季度计算的时期频率
# Q-MAY 中的 MAY 表示该年末为五月,即6-8一个季度,9-11一个季度,12-2一个季度,3-5一个季度。 其余表示以此类推
# 单个 period object 和 一组period(如period index)均可通过运算来表示某个时刻,并通过to_timestamp方法变成timestamp object
a = pd.Period('2000Q1', freq='Q')
print(a)
# 通过period运算将2000年第一季度转换为2000年第一季度的倒数第三个工作日的上午9点30分的时间戳
tstamp = ((a.asfreq('B', 'e') - 2).asfreq('H', 's') + 9).asfreq('T', 's') + 30
print(tstamp) # 还是一个period object,不过是分钟级的period
print(tstamp.to_timestamp()) # period 2 timestamp
print('')
# 5.3、将timestamp转换为period(及其反向过程)
# (1)to_period方法可以将timestamp转换为period
stamp = pd.date_range('2000/12/1', periods=2, freq='D')
print(stamp.to_period('M')) # 指定period的频率为月
# (2)to_timestamp方法可以将period转换为timestamp
print(stamp.to_period('M').to_timestamp())
print('')
# 5.4、通过数组创建period index
# 很多时候时间数据是分成几个子部分(如年、月、日)存放在一张表格的某几列中的,PeriodIndex方法可以合并这样的列汇成一个period index
df = pd.DataFrame()
df['year'] = [2000] * 4 + [2001] * 4
df['month'] = range(1, 9)
df['day'] = range(22, 30)
print(df)
tindex = pd.PeriodIndex(year=df['year'], month=df['month'], day=df['day'], freq='D')
print(tindex)
print('---------------------------------------↑, section 5 \n\n') # 6、重采样与频率转换
# 重采样是指从一个频率变换到另一种频率的过程,这里的频率并不是传统意义上的频率,而是指时间序列相关函数中freq关键字
# 重采样分3种:
# 1、升采样,频率变大,或者说period周期变小,如Q采样成D(季度->天)
# 2、降采样,频率变小,或者说period周期变大,如Q采样成A(季度->年)
# 3、同period周期的采样,如W-MON采样成W-WED(每周一->每周三)
# 重采样通过resample方法实现
# 6.1、降采样
# 先生成一个分钟序列数据
ts = pd.Series(range(15), index=pd.date_range('2010/10/1', periods=15, freq='T'))
print(ts)
print('')
# resample方法需要指定区间哪一边为闭区间(相应另一边为开区间),需要指定区间以左右哪个边界进行命名
# 在0.22.0版本(更高版本估计也类似)的pandas种,缺省情况比较复杂,视freq不同而不同,所以保险一点还是用关键字显示指定。
# 可通过closed关键字修改哪一边为闭区间,可通过label关键字指定以区间的哪个边界进行区间命名
print(ts.resample('10min', closed='left', label='left').count())
print('')
print(ts.resample('10min', closed='right', label='right').count())
# 为更清楚地显示区间,可使用loffset关键字进行偏移。其实也可以通过对整个time series进行shift实现相同的功能。
print('')
print(ts.resample('10min', closed='right', label='right', loffset='-1s').count())
# 特别地,降采样中有一种采样称为OHLC采样,特定用于金融数据,计算出每个区间的开盘价(open),收盘价(close),最高价(high),最低价(low)
print('')
print(ts.resample('5min', closed='right', label='right').ohlc())
# 另一种实现降采样的方法是通过groupby方法实现,和resample各有适用情景
# 按照周几进行分组
ts = pd.Series(range(100), index=pd.date_range('2000/10/10', periods=100, freq='D'))
print(ts)
print('')
print(ts.groupby(lambda t: t.weekday).count())
# 6.2、升采样和插值
# 从大尺度采样到小尺度,自然而然会引入数值缺失的问题,故升采样需要插值处理
# 先创建一个周频率的时间序列
print('')
ts = pd.Series([1, 2], index=pd.date_range('2000/1/21', periods=2, freq='W-FRI')) # 从2000/1/21开始的两个周五
print(ts)
# 将ts升采样到日频率
# 若不插值则会出现数值缺失,可通过前向插值(ffill)和后向插值(bfill)进行插值
print('')
print(ts.resample('D').ffill())
print(ts.resample('D').bfill())
# resample还可以实现既非升采样也非降采样
# 如将上述每周5的数据重采样到每周1
print('')
print(ts.resample('W-MON').ffill())
print(ts.resample('W-MON').bfill())
# 6.3、通过时期进行重采样
# 上述重采样都是对timestamp index的series进行的,重采样也可以对period index的series进行
# (1)period的降采样和timestamp一样,直接对其应用resample方法即可
# 先构造一个period index的时间序列
print('')
ts = pd.Series(range(10), index=pd.period_range('2010/1', periods=10, freq='M'))
print(ts)
# 然后在resample中传入一个更大周期的freq即可完成降采样
ts = ts.resample('Q').sum()
print(ts)
print('')
# (2)period的升采样,需要指定新区间中哪一端存放原来的值
# 比如,年到季度的升采样,原来的值放到第一个季度还是最后一个季度需要指定
# 通过关键字convention进行指定,'end'则放到最后一个季度,'start'则放到第一个季度。缺省为'start'
print(ts.resample('M', convention='end').ffill())
print('')
print(ts.resample('M').ffill())
print('--------------------------↑, section 6\n\n') # 7、时间序列绘图
# pandas的时间序列数据可直接用plot()方法进行绘图,基于matplotlib包进行过处理的
# 导入数据,1990年至2010年的几只美股数据
stk = pd.read_csv('./data_set/stock_px.csv', parse_dates=True, index_col=0)
stk = stk[['AAPL', 'MSFT', 'SPX']] # 从中取出3只股票
stk = stk.resample('B').ffill() # 按工作日频率进行重采样,实现规则频率
print(stk.describe())
# 通过切片直接应用plot方法可完成时间序列的作图
fig, axes = plt.subplots(2, 2) # figure1
stk['AAPL'].plot(ax=axes[0, 0]) # 品种切片
stk.ix['2005'].plot(ax=axes[0, 1]) # 时间切片
stk['AAPL'].ix['06/2006':'08/2008'].plot(ax=axes[1, 0]) # 双重切片
# 还可以对原数据重采样成季度数据,再作图
stk['AAPL'].resample('Q-DEC').ffill().plot(ax=axes[1, 1]) # 默认为每个季度最后一个工作日,若数据缺失则用前一天的数据填充
# plt.show() # 将这一行取消注释,使能作图功能
print('-------------------------------------↑, section 7\n\n') # 8、移动窗口函数
# 移动窗口函数,用于在一个长序列中切割出一个子窗口进行相关量的统计
# 在金融数据中移动窗口应用较多,典型地,N日均线
# 对APPLE股价作250日移动平均
fig2, axes2 = plt.subplots(2, 2) # figure2
stk['AAPL'].plot(ax=axes2[0, 0])
pd.Series.rolling(stk['AAPL'], 250).mean().plot(ax=axes2[0, 0]) # 高版本推荐写法,不同于书上例程
# 当rolling接受的数据很少时,将不返回移动平均值,通过min_periods关键字可以指定这个阈值
stk['AAPL'].plot(ax=axes2[0, 1])
pd.Series.rolling(stk['AAPL'], 250, min_periods=10).mean().plot(ax=axes2[0, 1]) # 最少有10个非NA值就返回移动平均
# 图(0,0)和图(0,1)的区别体现在图(0,1)更快出现250日均线
# 通过rolling还可以延申成 扩展窗口平均, 即窗口长度可变,相当于时间序列长度
expanding_mean = lambda ts: pd.Series.rolling(ts, len(ts), min_periods=1).mean()
expanding_mean(stk['AAPL']).plot(ax=axes2[1, 0]) # 全长度均线
stk['AAPL'].plot(ax=axes2[1, 0])
# 8.1、指数加权函数
# 移动窗口常搭配一个衰减因子使用,用于使近期的观测值有更大的权重,从而更快体现原数据的变化
# 通过ewm方法实现
ma = pd.Series.rolling(stk['AAPL'], 250, min_periods=50).mean() # 均权的年均线
ewma = pd.Series.ewm(stk['AAPL'], span=250).mean()
ma.plot(ax=axes2[1, 1], style='--')
ewma.plot(ax=axes2[1, 1], style=':') # 通过图可以看到带有衰减因子的均线更快出现拐点(反应更快)
# 8.2、二元移动窗口函数
# 某些统计变量用到两个数据,比如相关系数
spx = stk['SPX']
aapl = stk['AAPL']
# 两种方法计算股价的百分数变化
spx_pctc = spx / spx.shift(1) - 1
aapl_pctc = stk['AAPL'].pct_change()
fig3, axes3 = plt.subplots(2, 2) # figure3
# 计算两者的移动窗口里的相关系数
corr = pd.Series.rolling(spx_pctc, window=125, min_periods=100).corr(aapl_pctc) # 6个月窗口期,移动相关系数
corr.plot(ax=axes3[0,0])
# 很多时候会以唯一数据作为标准,计算其余数据与标准数据的相关系数
# 此时,传入Series(标准数据)与DataFrame(其余数据)即可
corr = pd.DataFrame.rolling(stk[['AAPL', 'MSFT']].pct_change(), window=125, min_periods=60).corr(spx_pctc)
corr.plot(ax=axes3[0,1])
# 8.3、用户定义的移动窗口函数
ten_mean = lambda ts: np.mean(sorted(ts, reverse=True)[:10]) # 计算最大的前10个值得平均值
res = pd.DataFrame.rolling(stk[['AAPL', 'MSFT']], window=125, min_periods=50).apply(ten_mean)
print(res)
res.plot(ax=axes3[1,0])
plt.show()
# 其实,在高版本的pandas中,rolling的用法和groupby是比较接近的。
print('----------------------total time is %.5f s' % (time.time() - start))