《利用Python 进行数据分析》第十章(时间序列)

对《利用Python 进行数据分析》(Wes Mckinney著)一书中的第十章中时间序列进行代码实验。原书中采用的是Python2.7,而我采用的Python3.7在Pycharm调试的,因此对书中源代码进行了一定的修改,每步结果与原文校验对照一致(除了随机函数外;输出结果在注释中,简单的输出就没写结果),全手工敲写,供参考。
Pdf文档和数据集参见:《利用Python 进行数据分析》第二章:引言中的分析代码(含pdf和数据集下载链接)

时间序列:

  • 1、日期和时间数据类型及工具
    • 1.1 日期类型初识
    • 1.2 字符串和datetime相互转换
  • 2、时间序列基础
    • 2.1 索引、选取、子集构造
    • 2.2 带重复索引的时间序列
  • 3、日期的范围、频率以及移动
    • 3.1 生成日期范围
    • 3.2 频率和日期偏移量
    • 3.3 移动(超前和滞后)数据
  • 4、时区处理
    • 4.1 本地化和转换
    • 4.2 操作时区意识型Timestamp对象
    • 4.3 不同时区之间的运算
  • 5、时期及算术运算
    • 5.1 时期的构建
    • 5.2 时期的频率转换
    • 5.3 按季度计算的时期频率
    • 5.4 将Timestamp转化为Period(及其反向过程)
    • 5.5 通过数组创建PeriodIndex
  • 6、重采样及频率转换
    • 6.1 重采样
    • 6.2 降采样
    • 6.3 升采样和差值
    • 6.4 通过时期进行重采样
  • 7、时间序列绘图
  • 8、移动窗口函数
    • 8.1 移动窗口
    • 8.2 指数加权函数
    • 8.3 二次移动窗口函数
    • 8.4 用户定义的移动窗口函数

因为代码过长,放在一个代码段中显得冗长,因此进行了拆分,如下的库引入每个代码段中均可能有必要。
# -*- coding:utf-8 -*- from datetime import datetime, timedelta import pandas as pd import numpy as np from pandas import DataFrame, Series

1、日期和时间数据类型及工具 时间序列数据的意义取决于具体的应用场景
时间戳(timestamp):特定的时刻
固定时期(period):如2007年1月或2010年全年
时间间隔(interval):由起始和结束时间戳表示,时期(period)可以被看作间隔的特例
1.1 日期类型初识
# 主要用到datetime、time以及calendar模块 now = datetime.now() print(now) # 2020-09-28 14:05:42.871960 print(now.year, now.month, now.day)# 2020 9 28# datetime以毫秒形式储存日期和时间 delta = datetime(2011,1,7) - datetime(2008, 6, 24, 8, 15) print(delta) # 926 days, 15:45:00# datetime.timedelta表示两个datetime对象之间的时间差 timedelta(926, 56700) print(delta.days) # 926 print(delta.seconds) # 56700# 可以给datetime对象加上(减去)一个或多个timedelta,会产生一个新对象 start = datetime(2011,1,7) ret = start + timedelta(12) print(ret) # 2011-01-19 00:00:00ret = start - 2 * timedelta(12) print(ret) # 2010-12-14 00:00:00

1.2 字符串和datetime相互转换
# 利用str或strftime方法(传入格式化字符串),datetime对象和pandas的Timestamp对象可以被格式化为字符串 stamp = datetime(2011, 1, 3) print(str(stamp))# 2011-01-03 00:00:00 print(stamp.strftime('%Y-%m-%d'))# 2011-01-03# date.time.strptime也可以用这些格式化编码将字符串转化为日期 value = 'https://www.it610.com/article/2011-01-09' print(datetime.strptime(value,'%Y-%m-%d')) # 2011-01-09 00:00:00datestrs=['7/6/2011', '8/6/2011'] print([datetime.strptime(x, '%m/%d/%Y') for x in datestrs]) '''[datetime.datetime(2011, 7, 6, 0, 0), datetime.datetime(2011, 8, 6, 0, 0)]'''# datetime.strptime是通过已知格式进行日期解析,但每次编写都需要定义格式比较麻烦 # 所以我们可以使用dateutil 这个第三方库的parser.parse方法 from dateutil.parser import parse print(parse('2011-01-03')) # 2011-01-03 00:00:00# dateutil 可以解析几乎所有人类能理解的日期表现形式 print(parse('Jan 31, 1997 10:45 PM')) # 1997-01-31 22:45:00# 国际通用格式中,日常常出现在月的前面,传入dayfirst=True即可解决这个问题 print(parse('6/12/2011', dayfirst=True)) # 2011-12-06 00:00:00# to_datetime方法可以解析多种不同日期的表示形式 print(datestrs) # ['7/6/2011', '8/6/2011'] print(pd.to_datetime(datestrs)) ''' DatetimeIndex(['2011-07-06', '2011-08-06'], dtype='datetime64[ns]', freq=None) '''# to_datetime也可以处理缺失值(None、空字符串等) idx = pd.to_datetime(datestrs + [None]) print(idx) ''' atetimeIndex(['2011-07-06', '2011-08-06', 'NaT'], dtype='datetime64[ns]', freq=None) ''' print(idx[2]) # NaT , NaT(Not a Time)是pandas中时间戳数据的NA值 print(pd.isnull(idx))# [False FalseTrue]

2、时间序列基础
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) print(ts) ''' 2011-01-02-0.804594 2011-01-050.444492 2011-01-07-1.336713 2011-01-081.380549 2011-01-10-1.090957 2011-01-120.162639 dtype: float64 ''' # datetime对象是被放在一个DatetimeIndex中,现在ts就成为一个TimeSeries了 print(type(ts)) ''' DatetimeIndex(['2011-01-02', '2011-01-05', '2011-01-07', '2011-01-08', '2011-01-10', '2011-01-12'], dtype='datetime64[ns]', freq=None) ''' print(ts.index) ''' 2011-01-02-1.609187 2011-01-05NaN 2011-01-07-2.673426 2011-01-08NaN 2011-01-10-2.181915 2011-01-12NaN dtype: float64 ''' # 跟其他Series一样,不同索引的时间序列之间的算术运算会按自动日期对齐 print(ts + ts[::2]) print(ts.index.dtype) # datetime64[ns] # DatetimeIndex中的各个标量值是pandas的Timestamp对象 stamp = ts.index[0] print(stamp) # 2011-01-02 00:00:00

2.1 索引、选取、子集构造
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) print(ts) ''' 2011-01-020.348253 2011-01-05-0.068450 2011-01-07-1.073036 2011-01-081.059299 2011-01-100.497196 2011-01-120.713568 dtype: float64 ''' # TimeSeries是Series的一个类,所以在索引以及数据选取方面他们的行为是一样的 stamp=ts.index[2] print(ts[stamp]) # -1.073035582647907# 可以传入一个可以解释为日期的字符串 print(ts['1/10/2011']) # 0.4971955016152246 print(ts['20110110'])# 0.4971955016152246# 对于较长的时间序列,只需要传入“年”或“年月”即可轻松选取数据的切片 longer_ts = Series(np.random.randn(1000), index=pd.date_range('1/1/2000', periods=1000)) print(longer_ts) ''' 2000-01-010.650802 2000-01-022.018351 2000-01-030.676741 2000-01-040.779642 2000-01-050.851207 ... 2002-09-22-1.794156 2002-09-230.515699 2002-09-240.257113 2002-09-25-1.512441 2002-09-26-0.680429 Freq: D, Length: 1000, dtype: float64 ''' print(longer_ts['2001']) ''' 2001-01-011.357131 2001-01-02-0.840957 2001-01-03-1.000980 2001-01-04-1.183331 2001-01-050.453523 ... 2001-12-270.919488 2001-12-28-1.240291 2001-12-290.061306 2001-12-30-1.226537 2001-12-31-0.744249 Freq: D, Length: 365, dtype: float64 ''' print(longer_ts['2001-05']) ''' 2001-05-010.377640 2001-05-020.389160 2001-05-03-0.657888 2001-05-041.353799 2001-05-050.834874 ... 2001-05-26-1.333958 2001-05-271.405335 2001-05-28-0.217538 2001-05-29-0.029023 2001-05-30-0.889619 2001-05-31-0.986640 Freq: D, dtype: float64 ''' # 通过日期进行切片的方式只对规则Series有效 print(ts[datetime(2011,1,7):]) ''' 2011-01-07-1.073036 2011-01-081.059299 2011-01-100.497196 2011-01-120.713568 dtype: float64 '''# 由于大部分时间都是按照时间先后排序,因此可以用不存在于该时间序列中的时间戳对其进行切片 print(ts) ''' 2011-01-020.348253 2011-01-05-0.068450 2011-01-07-1.073036 2011-01-081.059299 2011-01-100.497196 2011-01-120.713568 dtype: float64 ''' print(ts['1/6/2011':'1/11/2011'])# 取范围内的日期 ''' 2011-01-07-1.073036 2011-01-081.059299 2011-01-100.497196 dtype: float64 '''# 截取两个日期之间的TimeSeries print(ts.truncate(after='1/9/2011')) ''' 2011-01-020.348253 2011-01-05-0.068450 2011-01-07-1.073036 2011-01-081.059299 dtype: float64 ''' # 也可以对DataFrame操作,对DataFrame的行进行索引 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', 'NewYork', 'Ohio']) print(long_df.loc['5-2001']) ''' ColoradoTexasNewYorkOhio 2001-05-021.4679361.0631161.344797 -0.580989 2001-05-090.637778 -0.9058730.855643 -1.161038 2001-05-160.305796 -1.233853 -0.628636 -0.052159 2001-05-23 -1.0980290.0520490.5315451.161001 2001-05-30 -0.981410 -2.0684612.049203 -0.786793 '''

2.2 带重复索引的时间序列
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) print(dup_ts) ''' 2000-01-010 2000-01-021 2000-01-022 2000-01-023 2000-01-034 dtype: int32 '''# 通过检查索引的is_unique属性,可以知道它是不是唯一的 print(dup_ts.index.is_unique) # False# 通过对这个时间序列进行索引,要么产生标量值,要么产生切片,取决于所选的时间点是否重复 print(dup_ts['1/3/2000']) # 4 print(dup_ts['1/2/2000']) ''' 2000-01-021 2000-01-022 2000-01-023 dtype: int32 '''# 如果想要对具有非唯一的数据进行聚合,可以使用groupby,并传入level=0(索引的唯一一层!) grouped = dup_ts.groupby(level=0) print(grouped.mean()) ''' 2000-01-010 2000-01-022 2000-01-034 dtype: int32 ''' print(grouped.count()) ''' 2000-01-011 2000-01-023 2000-01-031 dtype: int64 '''

3、日期的范围、频率以及移动
# 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) print(ts)ts_resmp = ts.resample('D') print(ts_resmp) '''DatetimeIndexResampler [freq=, axis=0, closed=left, label=left, convention=start, base=0]'''ts_resmp_sum = ts.resample('3D').sum() print(ts_resmp_sum) # 按3天重新采样并求和 ''' 2011-01-021.041772 2011-01-05-0.854215 2011-01-08-2.727751 2011-01-110.809483 Freq: 3D, dtype: float64 ''' # 关于重采样是比较大的主题,在第6小节专门讨论

3.1 生成日期范围
# 用pandas_range可用于生成指定长度的DatetimeIndex index = pd.date_range('4/1/2012', '6/1/2012') print(index[:5]) ''' DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04', '2012-04-05'], dtype='datetime64[ns]', freq='D') '''# 默认情况下,date_range会按天计算的时间点 # 如果传入起始或起始结束日期,还需要传入一个表示一段时间的数字 print(pd.date_range(start='4/1/2012', periods=20)) ''' DatetimeIndex(['2012-04-01', '2012-04-02', '2012-04-03', '2012-04-04', '2012-04-05', '2012-04-06', '2012-04-07', '2012-04-08', '2012-04-09', '2012-04-10', '2012-04-11', '2012-04-12', '2012-04-13', '2012-04-14', '2012-04-15', '2012-04-16', '2012-04-17', '2012-04-18', '2012-04-19', '2012-04-20'], dtype='datetime64[ns]', freq='D') ''' print(pd.date_range(end='6/1/2012',periods =20)) ''' DatetimeIndex(['2012-05-13', '2012-05-14', '2012-05-15', '2012-05-16', '2012-05-17', '2012-05-18', '2012-05-19', '2012-05-20', '2012-05-21', '2012-05-22', '2012-05-23', '2012-05-24', '2012-05-25', '2012-05-26', '2012-05-27', '2012-05-28', '2012-05-29', '2012-05-30', '2012-05-31', '2012-06-01'], dtype='datetime64[ns]', freq='D') '''# 生成一个由每月最后一个工作日组成的日期索引,传入“BM"频率(business end of month) print(pd.date_range('1/1/2000','12/1/2000',freq='BM')) ''' DatetimeIndex(['2000-01-31', '2000-02-29', '2000-03-31', '2000-04-28', '2000-05-31', '2000-06-30', '2000-07-31', '2000-08-31', '2000-09-29', '2000-10-31', '2000-11-30'], dtype='datetime64[ns]', freq='BM') '''# date_range默认保留起始和结束时间戳的时间信息(如果有的话) print(pd.date_range('5/2/2012 12:56:31', periods=5)) ''' DatetimeIndex(['2012-05-02 12:56:31', '2012-05-03 12:56:31', '2012-05-04 12:56:31', '2012-05-05 12:56:31', '2012-05-06 12:56:31'], dtype='datetime64[ns]', freq='D') '''# normalize选项可以实现产生一组被规范化到午夜的时间戳 print(pd.date_range('5/2/2012 12:56:31', periods=5, normalize=True)) ''' DatetimeIndex(['2012-05-02', '2012-05-03', '2012-05-04', '2012-05-05', '2012-05-06'], dtype='datetime64[ns]', freq='D') '''

3.2 频率和日期偏移量
# pandas中的频率是由一个基础频率和一个乘数组成的 # 基础频率通常以一个字符串别名表示,比如“M"表示每月,”H“表示每小时 from pandas.tseries.offsets import Hour, Minute hour = Hour() print(hour) # # 传入一个整数即可定义便宜量的倍数 four_hours = Hour(4) print(four_hours) # <4 * Hours># 在基础频率前面放上一个整数即可创建倍数 print(pd.date_range('1/1/2000', '1/1/2000 23:59', freq='4h')) ''' DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 04:00:00', '2000-01-01 08:00:00', '2000-01-01 12:00:00', '2000-01-01 16:00:00', '2000-01-01 20:00:00'], dtype='datetime64[ns]', freq='4H') '''# 大部分偏移量对象都可以通过加法进行连接 print(Hour(2) + Minute(30)) # <150 * Minutes># 同时也可以传入频率字符串(如“2h30min”),这种字符串可以被高效地解析为等效的表达式 print(pd.date_range('1/1/2000',periods = 10, freq='1h30min')) ''' DatetimeIndex(['2000-01-01 00:00:00', '2000-01-01 01:30:00', '2000-01-01 03:00:00', '2000-01-01 04:30:00', '2000-01-01 06:00:00', '2000-01-01 07:30:00', '2000-01-01 09:00:00', '2000-01-01 10:30:00', '2000-01-01 12:00:00', '2000-01-01 13:30:00'], dtype='datetime64[ns]', freq='90T') '''# WOM日期,week of month是一种非常实用的频率类,如获得诸如“每月第3个星期五”之类的日期 rng = pd.date_range('1/1/2012','9/1/2012', freq="WOM-3FRI") print(rng) ''' DatetimeIndex(['2012-01-20', '2012-02-17', '2012-03-16', '2012-04-20', '2012-05-18', '2012-06-15', '2012-07-20', '2012-08-17'], dtype='datetime64[ns]', freq='WOM-3FRI') '''

3.3 移动(超前和滞后)数据
# 移动是指沿着时间轴将数据前移或后移,Series和DataFrame都有一个shift方法用于执行单纯的前移或后移操作 ts = Series(np.random.randn(4), index = pd.date_range('1/1/2000', periods=4, freq='M')) print(ts) ''' 2000-01-31-0.309081 2000-02-290.754501 2000-03-31-0.727029 2000-04-30-0.628417 Freq: M, dtype: float64 '''# Shift通常用于计算一个时间序列或多个时间序列中百分比变化:ts/st.shift(1) - 1 # 如果频率已知,则可以将其传给shift以便实现对时间戳进行位移而不是对数据进行简单位移 print(ts.shift(2, freq='M')) ''' 2000-03-31-0.309081 2000-04-300.754501 2000-05-31-0.727029 2000-06-30-0.628417 Freq: M, dtype: float64 '''# 还可以使用其他频率,可以灵活对数据进行超前或滞后处理 print(ts.shift(3,freq='D')) ''' 2000-02-03-0.309081 2000-03-030.754501 2000-04-03-0.727029 2000-05-03-0.628417 dtype: float64 '''print(ts.shift(1,freq='3D')) ''' 2000-02-03-0.309081 2000-03-030.754501 2000-04-03-0.727029 2000-05-03-0.628417 dtype: float64 '''print(ts.shift(1,freq='90T')) # 1h30mins ''' 2000-01-31 01:30:00-0.309081 2000-02-29 01:30:000.754501 2000-03-31 01:30:00-0.727029 2000-04-30 01:30:00-0.628417 Freq: M, dtype: float64 ''' print('----') # 通过偏移量对日期进行位移 from pandas.tseries.offsets import Day, MonthEnd now = datetime(2011, 11, 17) print(now + 3*Day()) # 2011-11-20 00:00:00# 如果加的是锚点偏移量(MonthEnd例如),第一次增量会将原日期向前滚动到符合频率规则的下一日期 print(now+MonthEnd()) # 2011-11-30 00:00:00 print(now+MonthEnd(2)) # 2011-12-31 00:00:00# 通过锚点偏移量的rollforward和rollback方法,可显式地将日期向前或向后”滚动“ offset = MonthEnd() print(offset.rollforward(now)) # 2011-11-30 00:00:00 print(offset.rollback(now)) # 2011-10-31 00:00:00# 日期偏移量还有一个巧妙的用法,即结合groupby使用这两个“滚动”方法 ts = Series(np.random.randn(20), index = pd.date_range('1/15/2000',periods=20,freq='4d')) print(ts.groupby(offset.rollforward).mean()) ''' 2000-01-310.168758 2000-02-29-0.167549 2000-03-310.379540 dtype: float64 '''# 当然实现上述功能最快的方法是使用resample函数 print(ts.resample("M").mean()) ''' 2000-01-310.168758 2000-02-29-0.167549 2000-03-310.379540 Freq: M, dtype: float64 '''

4、时区处理
# 时区信息来自第三方库Pytz import pytz print(pytz.common_timezones[-5:]) # ['US/Eastern', 'US/Hawaii', 'US/Mountain', 'US/Pacific', 'UTC']# 从pytz中获取时区对象,使用pytz.timezone即可 tz=pytz.timezone('US/Eastern') print(tz)# US/Eastern

4.1 本地化和转换
# 默认情况,pandas的时间序列是单纯的(naive)时区 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)# None# 在生成日期范围的时候还可以加上一个时区集 print(pd.date_range('2/9/2012 9:30', periods=10, freq='D', tz='UTC')) ''' DatetimeIndex(['2012-02-09 09:30:00+00:00', '2012-02-10 09:30:00+00:00', '2012-02-11 09:30:00+00:00', '2012-02-12 09:30:00+00:00', '2012-02-13 09:30:00+00:00', '2012-02-14 09:30:00+00:00', '2012-02-15 09:30:00+00:00', '2012-02-16 09:30:00+00:00', '2012-02-17 09:30:00+00:00', '2012-02-18 09:30:00+00:00'], dtype='datetime64[ns, UTC]', freq='D') '''# 从单纯到本地化的转换时通过tz_local方法处理的 ts_utc=ts.tz_localize('UTC') print(ts_utc) ''' 2012-03-09 09:30:00+00:00-0.366451 2012-03-10 09:30:00+00:00-1.254051 2012-03-11 09:30:00+00:000.733324 2012-03-12 09:30:00+00:00-0.267528 2012-03-13 09:30:00+00:00-0.938285 2012-03-14 09:30:00+00:00-1.037081 Freq: D, dtype: float64 '''print(ts_utc.index) ''' DatetimeIndex(['2012-03-09 09:30:00+00:00', '2012-03-10 09:30:00+00:00', '2012-03-11 09:30:00+00:00', '2012-03-12 09:30:00+00:00', '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00'], dtype='datetime64[ns, UTC]', freq='D') '''# 一旦时间序列被本地化到某个特定时区,就可以用tz_convert将其转换到别的时区 print(ts_utc.tz_convert('US/Eastern')) ''' 2012-03-09 04:30:00-05:00-0.366451 2012-03-10 04:30:00-05:00-1.254051 2012-03-11 05:30:00-04:000.733324 2012-03-12 05:30:00-04:00-0.267528 2012-03-13 05:30:00-04:00-0.938285 2012-03-14 05:30:00-04:00-1.037081 Freq: D, dtype: float64 '''# 对于上面的时间序列(跨越了美国东部时区的夏令时期转变期)可以先将其本地化到EST,再转为UTC或柏林时间 ts_eastern = ts.tz_localize('US/Eastern') print(ts_eastern.tz_convert('UTC')) ''' 2012-03-09 14:30:00+00:00-0.366451 2012-03-10 14:30:00+00:00-1.254051 2012-03-11 13:30:00+00:000.733324 2012-03-12 13:30:00+00:00-0.267528 2012-03-13 13:30:00+00:00-0.938285 2012-03-14 13:30:00+00:00-1.037081 dtype: float64 ''' print(ts_eastern.tz_convert('Europe/Berlin')) ''' 2012-03-09 15:30:00+01:00-0.366451 2012-03-10 15:30:00+01:00-1.254051 2012-03-11 14:30:00+01:000.733324 2012-03-12 14:30:00+01:00-0.267528 2012-03-13 14:30:00+01:00-0.938285 2012-03-14 14:30:00+01:00-1.037081 dtype: float64 '''# tz_localize和tz_convert也是DatetimeIndex的实例方法 print(ts.index.tz_localize('Asia/Shanghai')) ''' DatetimeIndex(['2012-03-09 09:30:00+08:00', '2012-03-10 09:30:00+08:00', '2012-03-11 09:30:00+08:00', '2012-03-12 09:30:00+08:00', '2012-03-13 09:30:00+08:00', '2012-03-14 09:30:00+08:00'], dtype='datetime64[ns, Asia/Shanghai]', freq=None) '''

4.2 操作时区意识型Timestamp对象
# 与时间序列和日期范围差不多,Timestamp对象也能从单纯型(naive)本地化为时区意识型,并从一个时区转换到另一时区 stamp = pd.Timestamp('2011-03-12 04:00') stamp_utc = stamp.tz_localize('utc') print(stamp_utc.tz_convert('US/Eastern')) # 2011-03-11 23:00:00-05:00# 在创建Timestamp时,还可以传入一个时区信息 stamp_moscow=pd.Timestamp('2011-03-12 04:00', tz='Europe/Moscow') print(stamp_moscow) # 2011-03-12 04:00:00+03:00# 时区意识型Timestamp对象在内部保存了一个UTC时间戳值,这个值在时区转换过程中是不会发生变化的 print(stamp_utc.value) # 1299902400000000000 print(stamp_utc.tz_convert('US/Eastern').value) # 1299902400000000000# 使用pandas的DateOffset对象执行时间算术运算时,运算过程会自动关注是否存在夏令时转变期 # 夏令时转变前30分钟 from pandas.tseries.offsets import Hour stamp = pd.Timestamp('2012-03-12 01:30', tz='US/Eastern') print(stamp) # 2012-03-12 01:30:00-04:00 print(stamp+Hour()) # 2012-03-12 02:30:00-04:00# 夏令时转变前90分钟 stamp=pd.Timestamp('2012-11-04 00:30', tz='US/Eastern') print(stamp) # 2012-11-04 00:30:00-04:00 print(stamp + 2*Hour())# 2012-11-04 01:30:00-05:00

4.3 不同时区之间的运算
# 如果两个时间序列的时区不同,将它们合并到一起时,最终结果就会是UTC # 由于时间戳其实是以UTC储存的,所以这是一个简单的运算,并不需要发生任何转换 rng = pd.date_range('3/7/2012 09:30', periods = 10, freq='B') ts = Series(np.random.randn(len(rng)), index=rng) print(ts) ''' 2012-03-07 09:30:000.041705 2012-03-08 09:30:000.461161 2012-03-09 09:30:000.197227 2012-03-12 09:30:00-1.409566 2012-03-13 09:30:000.227489 2012-03-14 09:30:00-1.624908 2012-03-15 09:30:000.717115 2012-03-16 09:30:00-1.355306 2012-03-19 09:30:00-1.684638 2012-03-20 09:30:00-0.566004 Freq: B, dtype: float64 ''' ts1= ts[:7].tz_localize('Europe/London') ts2= ts1[2:].tz_convert('Europe/Moscow') result = ts1 + ts2 print(result.index) ''' DatetimeIndex(['2012-03-07 09:30:00+00:00', '2012-03-08 09:30:00+00:00', '2012-03-09 09:30:00+00:00', '2012-03-12 09:30:00+00:00', '2012-03-13 09:30:00+00:00', '2012-03-14 09:30:00+00:00', '2012-03-15 09:30:00+00:00'], dtype='datetime64[ns, UTC]', freq=None) '''

5、时期及算术运算 5.1 时期的构建
# 时期表示的是时间区间,如日、数月、数季、数年等 # Period类所表示的就是此种类型,其构造函数需要用到一个字符串或整数,以及频率 p = pd.Period(2007,freq='A-DEC') print(p) # 2007# 上述p值表示的是2007年1月1日到2007年12月31日之间的整段时间 # 对Period对象加上或减去一个整数即可达到根据其频率进行位移的效果 print(p+5) # 2012 print(p-2) # 2005# 如果两个Period对象拥有相同的频率,则他们的差就是他们之间的单位数量 print(pd.Period('2014', freq='A-DEC') - p) # <7 * YearEnds: month=12># period_range函数可以用于创建规则的的时期范围 rng = pd.period_range('1/1/2000','6/30/2000', freq='M') print(rng) ''' PeriodIndex(['2000-01', '2000-02', '2000-03', '2000-04', '2000-05', '2000-06'], dtype='period[M]', freq='M') '''# PeriodIndex类保存了一组Period,可以在任何pandas数据结构中被用作轴索引 print(Series(np.random.randn(6), index = rng)) ''' 2000-01-2.155357 2000-02-0.912094 2000-030.358419 2000-040.337311 2000-051.036003 2000-06-0.613236 Freq: M, dtype: float64 '''# PeriodIndex类的构造函数还允许直接使用一组字符串 values = ['2001Q3', '2002Q2', '2003Q1'] index = pd.PeriodIndex(values, freq='Q-DEC') print(index) ''' PeriodIndex(['2001Q3', '2002Q2', '2003Q1'], dtype='period[Q-DEC]', freq='Q-DEC') '''

5.2 时期的频率转换
# Period和PeriodIndex都可以通过asfreq方法被转换成别的频率 p = pd.Period('2007', freq='A-DEC') print(p) # 2007 # 转换为一个年初或年末的一个月度时期 print(p.asfreq('M',how='start')) # 2007-01 print(p.asfreq('M',how='end')) # 2007-12# Period('2007','A-DEC')可以看做一个被划分为多个月度时期的时间段中的游标 p=pd.Period('2007', freq='A-JUN') print(p.asfreq('M', 'start')) # 2006-07 print(p.asfreq('M', 'end')) # 2007-06# 高频率转换为低频率时,超时期是由时期所属的位置决定的 # 如,在A-JUN频率中,月份“2007年8月”实际上是属于周期“2008年”的 p=pd.Period('2007-08','M') print(p.asfreq('A-JUN'))# 2008# PeriodIndex或TimeSeries的评率转换方式也是如此 rng =pd.period_range('2006','2009', freq='A-DEC') ts=Series(np.random.randn(len(rng)), index=rng) print(ts) ''' 2006-0.718306 20070.273010 2008-0.441507 2009-1.229443 Freq: A-DEC, dtype: float64 ''' print(ts.asfreq('M', how='start')) ''' 2006-01-0.718306 2007-010.273010 2008-01-0.441507 2009-01-1.229443 Freq: M, dtype: float64 ''' print(ts.asfreq('M', how='end')) ''' 2006-12-0.718306 2007-120.273010 2008-12-0.441507 2009-12-1.229443 Freq: M, dtype: float64 '''

Period频率转换示意图:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

5.3 按季度计算的时期频率
# pandas支持12中可能的季度型频率,即Q-JAN到Q-DEC # 我的理解,频率是哪个月份,就是那年Q4结束的月份,如本例 p=pd.Period('2012Q4', freq='Q-JAN') print(p) # 2012Q4# 在以1月结束的财年中,2012Q4是从11月到1月 print(p.asfreq('D', 'start')) # 2011-11-01 print(p.asfreq('D', 'end')) # 2012-01-31# 取该季度倒数第二个工作日下午4点的时间戳 p4pm =(p.asfreq('B', 'e') - 1).asfreq('T', 's') + 16*60 print(p4pm) # 2012-01-30 16:00 print(p4pm.to_timestamp()) # 2012-01-30 16:00:00# period_range可以用于生产季度型范围 rng = pd.period_range('2011Q3','2012Q4', freq='Q-JAN') ts=Series(np.arange(len(rng)), index = rng) print(ts) ''' 2011Q30 2011Q41 2012Q12 2012Q23 2012Q34 2012Q45 Freq: Q-JAN, dtype: int32 '''new_rng=(rng.asfreq('B', 'e') -1).asfreq('T', 's') + 16*60 ts.index=new_rng.to_timestamp() print(ts) ''' 2010-10-28 16:00:000 2011-01-28 16:00:001 2011-04-28 16:00:002 2011-07-28 16:00:003 2011-10-28 16:00:004 2012-01-30 16:00:005 dtype: int32 '''

不同季度频率之间的转换:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

5.4 将Timestamp转化为Period(及其反向过程)
# 通过使用to_period方法,可以将时间戳索引的Series和DataFrame对象转为以时期为索引 rng = pd.date_range('1/1/2000',periods=3,freq='M') ts = Series(np.random.randn(3), index=rng) pts=ts.to_period() print(ts) ''' 2000-01-310.563108 2000-02-290.784912 2000-03-311.014484 Freq: M, dtype: float64 ''' print(pts) ''' 2000-010.563108 2000-020.784912 2000-031.014484 Freq: M, dtype: float64 '''# 由于时期指的是非重叠时间区间,因此对于给定的频率,一个时间戳只能属于一个时期 rng = pd.date_range('1/29/2000', periods=6, freq='D') ts2=Series(np.random.randn(6), index=rng) print(ts2.to_period('M')) ''' 2000-01-0.139087 2000-01-0.136360 2000-01-2.787923 2000-02-1.520740 2000-02-0.473269 2000-020.600253 Freq: M, dtype: float64 '''pts=ts.to_period() print(pts) ''' 2000-01-0.219983 2000-02-1.073624 2000-03-0.681099 Freq: M, dtype: float64 ''' # 转化为时间戳,使用to_timestamp print(pts.to_timestamp(how='end')) ''' 2000-01-31 23:59:59.999999999-0.219983 2000-02-29 23:59:59.999999999-1.073624 2000-03-31 23:59:59.999999999-0.681099 dtype: float64 '''

5.5 通过数组创建PeriodIndex
# 固定频率的数据集通常会将时间信息分开存放在多列中 data = https://www.it610.com/article/pd.read_csv('python_data/ch08/macrodata.csv') print(data.year) ''' 01959.0 11959.0 21959.0 31959.0 41960.0 ... 1982008.0 1992008.0 2002009.0 2012009.0 2022009.0 Name: year, Length: 203, dtype: float64 ''' print(data.quarter) ''' 01.0 12.0 23.0 34.0 41.0 ... 1983.0 1994.0 2001.0 2012.0 2023.0 Name: quarter, Length: 203, dtype: float64 '''# 将两个数组以及一个频率传入PeriodIndex,可以将它们合并成DataFrame的一个索引 index = pd.PeriodIndex(year=data.year, quarter=data.quarter, freq='Q-DEC') print(index) ''' PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2', '1960Q3', '1960Q4', '1961Q1', '1961Q2', ... '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3', '2008Q4', '2009Q1', '2009Q2', '2009Q3'], dtype='period[Q-DEC]', length=203, freq='Q-DEC') ''' data.index = index print(data.infl) # infl为data的其中一个属性 ''' 1959Q10.00 1959Q22.34 1959Q32.74 1959Q40.27 1960Q12.31 ... 2008Q3-3.16 2008Q4-8.79 2009Q10.94 2009Q23.37 2009Q33.56 Freq: Q-DEC, Name: infl, Length: 203, dtype: float64 '''

6、重采样及频率转换 6.1 重采样
# 重采样是将时间序列从一个频率转换到另一个评率的过程 # 将高频率数据聚合到低频率称为讲采样,而将低频率数据转换到高频率则称为升采样 rng = pd.date_range('1/1/2000',periods = 100, freq='D') ts = Series(np.random.randn(len(rng)), index=rng) print(ts.resample('M').mean()) ''' 2000-01-310.066587 2000-02-29-0.240131 2000-03-31-0.126769 2000-04-300.387274 Freq: M, dtype: float64 ''' print(ts.resample('M', kind='period').mean()) ''' 2000-010.066587 2000-02-0.240131 2000-03-0.126769 2000-040.387274 Freq: M, dtype: float64 '''

6.2 降采样
# 降采样是将数据聚合到规整的低频率 rng =pd.date_range('1/1/2000',periods=12,freq='T') ts=Series(np.arange(12), index=rng) print(ts) ''' 2000-01-01 00:00:000 2000-01-01 00:01:001 2000-01-01 00:02:002 2000-01-01 00:03:003 2000-01-01 00:04:004 2000-01-01 00:05:005 2000-01-01 00:06:006 2000-01-01 00:07:007 2000-01-01 00:08:008 2000-01-01 00:09:009 2000-01-01 00:10:0010 2000-01-01 00:11:0011 Freq: T, dtype: int32 '''# 通过求和的方法将这些数据聚合到“5分钟”块中 print(ts.resample('5min').sum()) ''' 2000-01-01 00:00:0010 2000-01-01 00:05:0035 2000-01-01 00:10:0021 Freq: 5T, dtype: int32 '''# 默认情况下面元的左边界是包含的,因此00:00到00:05区间包含00:05,传入closed='letf'会让区间以左边界闭合 # !!此处跟书中不同,书中是默认包含右边界!! print(ts.resample('5min', closed='right').sum()) ''' 1999-12-31 23:55:000 2000-01-01 00:00:0015 2000-01-01 00:05:0040 2000-01-01 00:10:0011 Freq: 5T, dtype: int32 '''# 时间序列是以各方面左边界的时间戳进行标记的,传入label='right'即可用面元动的右边界对其标记 print(ts.resample('5min',label='right').sum()) ''' 2000-01-01 00:05:0010 2000-01-01 00:10:0035 2000-01-01 00:15:0021 Freq: 5T, dtype: int32 '''# 如果对结果索引做一些位移,如从左边界减去一秒,只需通过loffset设置一个字符串或日期偏移量即可 print(ts.resample('5min',loffset='-1s').sum()) ''' 1999-12-31 23:59:5910 2000-01-01 00:04:5935 2000-01-01 00:09:5921 Freq: 5T, dtype: int32 '''# OHLC重采样,金融领域中有一种无所不在的时间序列聚合方式 # 即计算各面元的四个值,第一个值(开盘)、最后一个值(收盘)、最大值(最高值)、最小值(最低) # 传入how='ohlc'即可得到一个含有这四种聚合值的DataFrame print(ts.resample('5min').ohlc()) ''' openhighlowclose 2000-01-01 00:00:000404 2000-01-01 00:05:005959 2000-01-01 00:10:0010111011 '''# 通过groupby进行重采样 rng = pd.date_range('1/1/2000', periods=100, freq='D') ts = Series(np.arange(100), index=rng) print(ts) print(ts.groupby(lambda x: x.month).mean()) ''' 115 245 375 495 dtype: int32 '''print(ts.groupby(lambda x: x.weekday).mean()) ''' 047.5 148.5 249.5 350.5 451.5 549.0 650.0 dtype: float64 '''

6.3 升采样和差值
# 升采样是指将数据从低频率转换到高频率 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']) print(frame[:5]) ''' ColoradoTexasNew YorkOhio 2000-01-05 -0.312261 -1.3036670.1664551.113591 2000-01-12 -0.7193990.8604890.9274831.041800 '''# 将其重采样到日频率,默认会引入缺失值 df_daily = frame.resample('D') print(df_daily) '''DatetimeIndexResampler [freq=, axis=0, closed=left, label=left, convention=start, base=0]'''# 假如想要用前面的周型填充“非星期三”,resample的填充和差值方式跟fillna和reindex的一样 print(frame.resample('D').ffill()) ''' ColoradoTexasNew YorkOhio 2000-01-05 -0.312261 -1.3036670.1664551.113591 2000-01-06 -0.312261 -1.3036670.1664551.113591 2000-01-07 -0.312261 -1.3036670.1664551.113591 2000-01-08 -0.312261 -1.3036670.1664551.113591 2000-01-09 -0.312261 -1.3036670.1664551.113591 2000-01-10 -0.312261 -1.3036670.1664551.113591 2000-01-11 -0.312261 -1.3036670.1664551.113591 2000-01-12 -0.7193990.8604890.9274831.041800 '''# 这里可以只填充指定的时期数(目的是限制前面的观测值持续使用) print(frame.resample('D').ffill(limit=2)) ''' ColoradoTexasNew YorkOhio 2000-01-05 -0.312261 -1.3036670.1664551.113591 2000-01-06 -0.312261 -1.3036670.1664551.113591 2000-01-07 -0.312261 -1.3036670.1664551.113591 2000-01-08NaNNaNNaNNaN 2000-01-09NaNNaNNaNNaN 2000-01-10NaNNaNNaNNaN 2000-01-11NaNNaNNaNNaN 2000-01-12 -0.7193990.8604890.9274831.041800 '''# 新的日期索引完全没有必要跟旧的相交 print(frame.resample('W-THU').ffill()) ''' ColoradoTexasNew YorkOhio 2000-01-06 -0.312261 -1.3036670.1664551.113591 2000-01-13 -0.7193990.8604890.9274831.041800 '''

6.4 通过时期进行重采样
frame = DataFrame(np.random.randn(24,4), index=pd.period_range('1-2000','12-2001',freq='M'), columns=['Colorado', 'Texas', 'New York', 'Ohio']) print(frame[:5]) ''' ColoradoTexasNew YorkOhio 2000-010.7789571.395773 -0.5544451.233439 2000-020.858590 -0.382989 -0.6555461.364961 2000-030.064890 -1.0074062.427516 -0.147838 2000-040.654691 -2.8571030.011106 -0.549523 2000-050.2903380.2267461.0079940.673866 '''annual_frame = frame.resample('A-DEC').mean() print(annual_frame) ''' ColoradoTexasNew YorkOhio 20000.406816 -0.262081 -0.1862500.125175 20010.0565890.3404770.1540830.218699 '''# 升采样稍微麻烦,因为要决定在新的频率中各区间的哪端用于放置原来的值,像asfreq方法 print(annual_frame.resample('Q-DEC').ffill()) ''' ColoradoTexasNew YorkOhio 2000Q10.406816 -0.262081 -0.1862500.125175 2000Q20.406816 -0.262081 -0.1862500.125175 2000Q30.406816 -0.262081 -0.1862500.125175 2000Q40.406816 -0.262081 -0.1862500.125175 2001Q10.0565890.3404770.1540830.218699 2001Q20.0565890.3404770.1540830.218699 2001Q30.0565890.3404770.1540830.218699 2001Q40.0565890.3404770.1540830.218699 '''print(annual_frame.resample('Q-DEC',convention='end').ffill()) ''' ColoradoTexasNew YorkOhio 2000Q40.406816 -0.262081 -0.1862500.125175 2001Q10.406816 -0.262081 -0.1862500.125175 2001Q20.406816 -0.262081 -0.1862500.125175 2001Q30.406816 -0.262081 -0.1862500.125175 2001Q40.0565890.3404770.1540830.218699 '''

7、时间序列绘图
close_px_all = pd.read_csv('python_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() print(close_px.head()) ''' AAPLMSFTXOM 2003-01-027.4021.1129.22 2003-01-037.4521.1429.24 2003-01-067.4521.5229.96 2003-01-077.4321.9328.95 2003-01-087.2821.3128.83 '''importmatplotlib.pyplot as plt plt.plot(close_px['AAPL']) plt.grid(alpha=0.3, linestyle='dashed') plt.show()# DataFrame调用plot时,时间序列会被绘制在一个subplot上,并有图例说明 close_px.loc['2009'].plot() # plt.savefig('10-5.png') plt.show()# 苹果公司在2011年1月到3月间的每日股价 close_px['AAPL'].loc['01-2011':'03-2011'].plot() plt.grid(alpha=0.3, linestyle='dashed') #plt.savefig('10-6.png') plt.show()# 季度型频率数据会用季度标记进行格式化 appl_q=close_px['AAPL'].resample('Q-DEC').ffill() appl_q.loc['2009':].plot() plt.grid(alpha=0.3, linestyle='dashed') #plt.savefig('10-7.png') plt.show()

以下图片对应原书中的图片序号,按代码输出顺序给出(其他段落同样):
图10-4 AAPL每日价格:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

图10-5 2009年股票价格:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

图10-6 苹果公司在2011年1月到3月的每日股价:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

图10-7 苹果公司在2009年到2011年的每季度价格:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

8、移动窗口函数 8.1 移动窗口
# 在移动窗口上计算各种统计函数是一类常见于时间序列的数组变换 # rolling_mean是其中最简单的一个,它接受一个TimeSeries或DataFrame以及一个window(表示期数) close_px_all = pd.read_csv('python_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.AAPL.plot() close_px.AAPL.rolling(250).mean().plot() plt.grid(alpha=0.3, linestyle='dashed') plt.show()appl_std250=close_px.AAPL.rolling(250, min_periods=10).std() print(appl_std250[5:12]) ''' 2003-01-09NaN 2003-01-10NaN 2003-01-13NaN 2003-01-14NaN 2003-01-150.077496 2003-01-160.074760 2003-01-170.112368 Freq: B, Name: AAPL, dtype: float64 '''appl_std250.plot() plt.grid(alpha=0.3, linestyle='dashed') plt.show()# 要计算扩展窗口平均,可以将扩展窗口看做一个特殊的窗口,其长度与时间序列一样,但只需一期(或多期)即可计算一个值 # 通过rolling().mean()定义扩展平均 expanding_mean = lambda x: x.rolling(len(x), min_periods=1).mean() # 对DataFrame调用rolling_mean(以及与之类似的函数)会将转换应用到所有列上 close_px.rolling(60).mean().plot(logy=True) plt.grid(alpha=0.3, linestyle='dashed') plt.show()

图10-8 苹果公司的250的股票均线:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

图10-9 苹果公司的250日每日回报标准差:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

图10-10 各公司60日均线(对数Y轴):
《利用Python 进行数据分析》第十章(时间序列)
文章图片

8.2 指数加权函数
# 另一种使用固定大小窗口及相等权数观测值得办法是,定义一个衰减因子常量,以便使最近的观测值拥有更大的权数 fig,axes = plt.subplots(2,1, sharex=True, sharey=True, figsize=(12,7)) aapl_px = close_px.AAPL['2005': '2009']# 对比苹果公司股价的60日移动平均和span=60的指数加权移动平均 ma60 = aapl_px.rolling(60, min_periods=50).mean() ewma60 = pd.DataFrame.ewm(aapl_px,span=60).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-weithed MA') axes[0].grid(alpha=0.3, linestyle='dashed') axes[1].grid(alpha=0.3, linestyle='dashed') plt.show()

图10-11 简单移动平均与指数加权移动平均:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

8.3 二次移动窗口函数
# 有些运算需(如相关系数和协方差)需要在两个时间序列上执行 # 通过计算百分数变化并使用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(125, min_periods=100).corr(spx_rets) corr.plot() plt.grid(alpha=0.3, linestyle='dashed') plt.show()# 计算DataFrame各列与标准普尔500指数的相关系数 corr = returns.rolling(125,min_periods=100).corr(spx_rets) corr.plot() plt.grid(alpha=0.3, linestyle='dashed') plt.show()

图10-12 AAPL6个月的回报与标准普尔500指数的相关系数:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

图10-13 3只股票6个月的回报与标准普尔500指数的相关系数:
《利用Python 进行数据分析》第十章(时间序列)
文章图片

8.4 用户定义的移动窗口函数
# rolling apply()函数可以在移动窗口上应用自己设计的数组函数 from scipy.stats import percentileofscore scroe_at_2percent = lambda x: percentileofscore(x, 0.02) result = returns.AAPL.rolling(250).apply(scroe_at_2percent) result.plot() plt.grid(alpha=0.3, linestyle='dashed') plt.show()

【《利用Python 进行数据分析》第十章(时间序列)】图10-14 AAPL 2%回报率的百分登记(一年窗口期):
《利用Python 进行数据分析》第十章(时间序列)
文章图片

    推荐阅读