对《利用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频率转换示意图:
文章图片
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
'''
不同季度频率之间的转换:
文章图片
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每日价格:
文章图片
图10-5 2009年股票价格:
文章图片
图10-6 苹果公司在2011年1月到3月的每日股价:
文章图片
图10-7 苹果公司在2009年到2011年的每季度价格:
文章图片
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的股票均线:
文章图片
图10-9 苹果公司的250日每日回报标准差:
文章图片
图10-10 各公司60日均线(对数Y轴):
文章图片
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 简单移动平均与指数加权移动平均:
文章图片
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指数的相关系数:
文章图片
图10-13 3只股票6个月的回报与标准普尔500指数的相关系数:
文章图片
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数据分析|numpy+pandas
- 数据分析与挖掘|Pandas学习——分类数据
- R|资金流入流出预测-挑战Baseline
- python|snowflake 数据库_Snowflake数据分析教程
- 数据分析|数据分析 - 基础原理 之 第三章(数据质量管理 - 第一节:数据质量评估)
- 亿信华辰讲述如何做好客商主数据管理
- hive|Hive之数仓的分层及建模理论
- 数据可视化|数字化转型大趋势下,如何通过数据分析助力
- python|用 Pandas 做 ETL,不要太快