几个基本概念:
截面:表示某个时间点的数据
面板:多个数据项在多个时间点的截面数据构成一个面板
面板数据既可以被表示为层次化索引的DataFrame
,也可以被表示为三维的Panel pandas
对象
import pandas as pd
import numpy as np
from pandas import DataFrame,Series
from datetime import datetime
一、数据对齐问题
data = [[379.74,64.64,1165.24,71.15],[379.74,64.64,1165.24,71.15],[379.74,64.64,1165.24,71.15],
[379.74,64.64,1165.24,71.15],[379.74,64.64,1165.24,71.15],[379.74,64.64,1165.24,71.15],
[379.74,64.64,1165.24,71.15]]
prices = DataFrame(data,index=pd.date_range('2011-09-06',periods=7),
columns=['AAPL','JNJ','SPX','XOM'])
频率不同的时间序列的运算
有些经济学时间序列有时就是不规则的,比如盈利预测调整随时都可能发生。频率转换和重对齐的主要工具是resample
和reindex
方法。
resample
用于将数据转换到固定频率,而reindex
用于使数据符合一个新索引
ts1 = Series(np.random.rand(3),index=pd.date_range('2012-6-13',periods=3,freq='W-WED'))
如果将其重采样到工作日频率,则那些没有数据的就会出现一个“空洞”,所以加上一个ffill
表示向前填充
ts1.resample('B').ffill()
如果要将ts1
中最当前的值填充到ts2
中。将两者重采样为规整频率后再相加,并维持ts2
的日期索引,则reindex
是更好的解决方案
dates = pd.DatetimeIndex(['2012-6-12','2012-6-17','2012-6-18',
'2012-6-21','2012-6-22','2012-6-29'])
ts2 = Series(np.random.rand(6),index=dates)
ts1.reindex(ts2.index,method='ffill')
ts2 + ts1.reindex(ts2.index,method='ffill')
二、Period的使用
Period
提供了一种处理不同频率时间序列的方法,比如说发布以6月结尾的财年的每季盈利报告,表示为Q-JUN
gdp = Series([1.78,1.94,2.08,2.01,2.15,2.31,2.46],index=pd.period_range('1984Q2',periods=7,freq='Q-SEP'))
infl = Series([0.025,0.045,0.037,0.04],index=pd.period_range('1982',periods=4,freq='A-DEC'))
gdp
Out:
1984Q2 1.78
1984Q3 1.94
1984Q4 2.08
1985Q1 2.01
1985Q2 2.15
1985Q3 2.31
1985Q4 2.46
Freq: Q-SEP, dtype: float64
infl
Out:
1982 0.025
1983 0.045
1984 0.037
1985 0.040
Freq: A-DEC, dtype: float64
由Period
索引的两个不同频率的时间序列之间的运算必须进行显式转换。假设已知infl
值是在每年年末观测的,于是我们就可以将其转换到Q-SEP
以得到该频率下的正确时期:
infl_q = infl.asfreq('Q-SEP',how='end')
然后就可以被重复索引了
infl_q.reindex(gdp.index,method='ffill')
时间和“最当前”数据选取
但数据不规整的处理方法(观测值没有精确落在期望的时间点上),生成一个交易日内的日期范围和时间序列
rng = pd.date_range('2012-6-1 9:30','2012-6-1 15:59',freq='T')
生成5天的时间点
rng = rng.append([rng+pd.offsets.BDay(i) for i in range(1,4)])
ts = Series(np.arange(len(rng),dtype=float),index=rng)
1、利用Python的datetime.time
对象进行索引可抽取这些时间点上的值
from datetime import time
ts[time(10,0)]
2、该方法相当于
ts.at_time(time(10,0))
3、还有一个between
方法
ts.between_time(time(10,0),time(10,1))
可能刚好没有任何数据落在某个具体的时间
indexer = np.sort(np.random.permutation(len(ts))[700:])
irr_ts = ts.copy()
irr_ts[indexer] = np.nan
irr_ts['2012-6-1 9:50':'2012-6-1 10:00']
# 构造一个日期范围(每天上午十点),传入asof
selection = pd.date_range('2012-6-1 10:00',periods=4,freq='B')
三、拼接多个数据源
- 在一个特定的时间点上,从一个数据源切换到另一个数据源
- 利用
pandas
,contact
将两个TimeSeries
或DateFrame
对象合并在一起 - 用另一个时间序列对当前时间序列中的缺失值“打补丁”
- 将数据中的符号(国家、资产代码)替换为实际数据
案例1
data1 = DataFrame(np.ones((6,3),dtype=float),columns=['a','b','c'],index=pd.date_range('6/12/2012',periods=6))
data2 = DataFrame(np.ones((6,3),dtype=float)*2,columns=['a','b','c'],index=pd.date_range('6/13/2012',periods=6))
spliced = pd.concat([data1.loc[:'2012-6-14'],data2.loc['2012-6-15':]])
spliced
Out:
a b c
2012-06-12 1.0 1.0 1.0
2012-06-13 1.0 1.0 1.0
2012-06-14 1.0 1.0 1.0
2012-06-15 2.0 2.0 2.0
2012-06-16 2.0 2.0 2.0
2012-06-17 2.0 2.0 2.0
2012-06-18 2.0 2.0 2.0
案例2,假设data1
缺失data2
中存在的某个时间序列
data2 = DataFrame(np.ones((6,4),dtype=float)*2,columns=['a','b','c','d'],index=pd.date_range('6/13/2012',periods=6))
spliced = pd.concat([data1.loc[:'2012-6-14'],data2.loc['2012-6-15':]])
spliced
Out:
a b c d
2012-06-12 1.0 1.0 1.0 NaN
2012-06-13 1.0 1.0 1.0 NaN
2012-06-14 1.0 1.0 1.0 NaN
2012-06-15 2.0 2.0 2.0 2.0
2012-06-16 2.0 2.0 2.0 2.0
2012-06-17 2.0 2.0 2.0 2.0
2012-06-18 2.0 2.0 2.0 2.0
combine_first
可以引入合并点之前的数据,这样就也扩展了d
项的历史,且data2
没有‘2012-6-12
’的数据,则没有被填充
spliced_filled = spliced.combine_first(data2)
spliced_filled
Out:
a b c d
2012-06-12 1.0 1.0 1.0 NaN
2012-06-13 1.0 1.0 1.0 2.0
2012-06-14 1.0 1.0 1.0 2.0
2012-06-15 2.0 2.0 2.0 2.0
2012-06-16 2.0 2.0 2.0 2.0
2012-06-17 2.0 2.0 2.0 2.0
2012-06-18 2.0 2.0 2.0 2.0
dataframe
也有类似的方法upgrade
,如果只想填充空洞,则必须传入参数overwrite=False
spliced.update(data2,overwrite=False)
以上的方法都能实现将数据终端符号替换为实际数据
利用dataframe
的索引机制直接对列进行设置更简单
cp_spliced = spliced.copy()
cp_spliced[['a','c']] = data1[['a','c']]
四、收益指数和累计收益
import pandas_datareader.data as web
price = web.get_data_yahoo('AAPL','2019-1-1')['Adj Close']
price[-5:]
计算两个时间点之间的累计百分比回报只需计算价格的百分比变化即可:
price['2019-10-3']/price['2019-3-1']-1
利用cumprod
计算出一个简单的收益指数:
returns = price.pct_change()
ret_index = (1+returns).cumprod()
ret_index[0]=1 #将第一个值设置为1
ret_index
Out:
Date
2018-12-31 1.000000
2019-01-02 1.001141
2019-01-03 0.901420
2019-01-04 0.939901
2019-01-07 0.937809
...
2020-04-20 1.786217
2020-04-21 1.731005
2020-04-22 1.780864
2020-04-23 1.773962
2020-04-24 1.825176
Name: Adj Close, Length: 332, dtype: float64
得到收益指数后,计算指定时期内的累计收益就很简单了:
m_returns = ret_index.resample('BM').last().pct_change()
m_returns['2020']
其中‘how’
参数已经不适用了,可以将resample(how='last')
改成resample.last()
的形式,详情请看:
resample函数‘how’参数报错解决方案
五、分组变换和分析
1、首先随机生成1000个股票代码
import random; random.seed(0)
import string
N=1000
def rands(n):
choices = string.ascii_uppercase
return ''.join([random.choice(choices) for _ in range(n)])
tickers = np.array([rands(5) for _ in range(N)])
2、创建一个含有3列的dataframe承载假想数据
M=500
df = DataFrame({'Momentum':np.random.randn(M)/200+0.03,
'Value':np.random.randn(M)/200+0.08,
'ShortInterest':np.random.randn(M)/200-0.02},
index=tickers[:M])
3、为这些股票随机创建一个行业分类
ind_names = np.array(['FINANCIAL','TECH'])
sampler = np.random.randint(0,len(ind_names),N)
industries = Series(ind_names[sampler],index=tickers,name='industry')
4、根据行业分类进行分组并执行分组聚合和变换
by_industry = df.groupby(industries)
by_industry.mean()
Out:
Momentum Value ShortInterest
industry
FINANCIAL 0.030001 0.080211 -0.019961
TECH 0.029953 0.079883 -0.019949
对这些按行业分组的投资组合进行各种变换,编写自定义的变换函数
例如:行业内标准化处理,广泛用于股票资产投资组合的构建过程
行业内标准化处理
def zscore(group):
return (group-group.mean())/group.std()
df_stand = by_industry.apply(zscore)
这样处理之后,各行业的平均值为0,标准差为1:
df_stand.groupby(industries).agg(['mean','std'])
Out:
Momentum Value ShortInterest
mean std mean std mean std
industry
FINANCIAL 2.946117e-16 1.0 -9.475033e-16 1.0 3.793403e-15 1.0
TECH 3.747702e-15 1.0 -9.488209e-16 1.0 -1.912103e-15 1.0
使用内置函数(如rank)会更简洁
1、行业内降序排名
ind_rank = by_industry.rank(ascending=False)
ind_rank.groupby(industries).agg(['min','max'])
“排名和标准化”是一种常见的变换组合,通过将rank
和zscore
连接在一起即可完成整个变换过程
2、行业内排名和标准化
by_industry.apply(lambda x:zscore(x.rank()))
六、信号前沿分析
将金融和技术领域的几只股票做成一个投资组合,并加载他们的历史价格数据
names = ['AAPL','GOOG','MSFT','DELL','GS','MS','BAC','C']
def get_px(stock,start,end):
return web.get_data_yahoo(stock,start,end)['Adj Close']
px = DataFrame({n:get_px(n,'1/1/2019','4/1/2020') for n in names})
绘制每只股票的累计收益
import matplotlib.pyplot as plt
px = px.asfreq('B').fillna(method='pad')
rets = px.pct_change()
((1+rets).cumprod()-1).plot()
plt.show()
对于投资组合的构建,我们要计算特定回顾期的动量,然后按降序排列并标准化
def calc_mon(price,lookback,lag):
mom_ret = price.shift(lag).pct_change(lookback)
ranks = mom_ret.rank(axis=1,ascending=False)
demeaned = ranks-ranks.mean(axis=1)
return demeaned/demeaned.std(axis=1)
利用这个变换函数,我们再编写一个对策进行事后检验的函数:通过指定回顾期和持有期(买卖之间的日期)计算投资组合整体的夏普比率
compound = lambda x : (1+x).prod()-1
daily_sr = lambda x : x.mean()/x.std()
def strat_sr(prices,lb,hold):
# 计算投资组合权重
freq = '%dB' % hold
port = calc_mon(prices,lb,lag=1)
daily_rets = prices.pct_change()
# 计算投资组合收益
port = port.shift(1).resample(freq).first()
returns = daily_rets.resample(freq).compound()
## 报错了不知怎么解决
port_rets = (port * returns).sum(axis=1)
return daily_sr(port_rets)*np.sqrt(252/hold)