Python量化投资——如何高效地“应用”一个函数到ndarray并动态改变参数
Python学习笔记——如何高效地应用一个函数到整个矩阵
问题
如果用python做量化投资,少不了对大量股票的大量历史数据进行回测或进行交易策略的计算,如果数据量大,往往计算的效率会成为一个问题:
我们会遇到的典型应用场景是:将N只股票的M行历史数据存储在一个DataFrame或ndarray中,每一列代表一只股票的历史数据,而每一行代表同一天的股价。因此,为了对一个矩阵中的所有股票计算交易策略,其本质是从矩阵中抽取出每一列,逐一根据相应的策略和参数生成交易信号,最后组成一张交易信号矩阵。
一般我们很容易针对一列数据写一个向量化的操作函数对单列数据进行操作,生成交易信号。然而,因为每一列之间数据有差距,往往不容易向量化列间的操作,可能还是得借助For-loop来实现。
那么要快速地对所有列进行操作,这就需要快速将同一个函数“apply”应用到每一列中去,如果能够不使用for-loop循环,采用向量化的操作方式,就能够极大地提升效率。
问题定义
那么我们的问题定义为:
我们用来进行数据处理的函数为
def op_list(arr, *args, **kwargs):
# 对单列股票历史数据进行计算,根据*args参数生成交易清单
pass
,而我们的股票数据存放在data中,这是一个ndarray
使用Numpy的方法
numpy中已经提供了一个向量化的解决方案:np.apply_along_axis()函数
只要简单的一句:
np.apply_along_axis(op_list, 0, data, *args, **kwargs)
其中的0代表矩阵的轴,0代表应用函数到每一列,如果取1表示应用函数到每一行
这样的速度比循环的方式效率高10~20倍
Numpy方法的局限性
然而这个方法有一个致命的缺陷:传入到函数中的*args和**kwargs是不能变的。举个例子,假如我们的op_list需要根据一个参数来生成交易清单。那么如果对所有的股票这个交易参数都是一样的,那么很简单,用上面的apply_along_axis传入这个参数就可以了,但是,如果我们需要应用不同的参数到每一只股票上,怎么办呢?这个函数就不行了,因为这个函数无法动态地改变每一次应用op_list()的时候传入的参数。那么这个问题应该如何解决?
应用函数的同时还能动态改变参数
经过多方查找没有找到答案,在google上各种搜索无果。
后来终于想到了map函数,它能将几个可循环(iterable)的对象动态地应用到一个函数中去。在这里,data不再是一个固定的数组,等着一个函数应用到它的某个轴上,我们需要把这个数组拆成一列一列,组成一个可循环对象,同时把我们需要应用的参数也做成一个列表,每个参数与数组拆分出来的一列一列数据列表一一对应,这样把这两个对象用map函数一一匹配起来,就能计算结果了!
例子如下:
首先组成一个par_list , list中元素的数量与data的列数相同,然后应用map:
map(op_list, data, par_list)
首先遇到的问题是map现在不再直接给出结果了(py3与py2的不同之处之一,更多区别可以看我的另一篇笔记:Py2与Py3的区别),因此需要用list来强制产生结果:
list(map(op_list, data, par_list)
结果出来了,但是发现map提取的是data的每一行数据而不是每一列,因此需要把data转置,但这样结果也转置了,因此还需要在结果后再转置一下,并转化为ndarray,因此有:
np.array(list(map(op_list, data.T, par_list))).T
经过验证,经过转置后的解决方案能够实现与np.apply_along_axis()相同的结果:
In [826]: np.array(list(map(moving_avg, val.T, pars))).T
Out[826]:
array([[ nan, nan, -2. , ..., nan, -1. , nan],
[ 5. , -3. , 5. , ..., 3.5, 1. , -0.5],
[ 4.5, -1.5, -3. , ..., 3.5, 2. , -3. ],
...,
[ 6.5, 2. , -3. , ..., nan, nan, 2.5],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan]])
In [827]: np.apply_along_axis(moving_avg, 0, val, 2)
Out[827]:
array([[ nan, nan, nan, ..., nan, nan, nan],
[ 5. , -3. , 1.5, ..., 3.5, 0. , -0.5],
[ 4.5, -1.5, 1. , ..., 3.5, 1.5, -3. ],
...,
[ 6.5, 2. , -2.5, ..., nan, nan, 2.5],
[ nan, nan, nan, ..., nan, nan, nan],
[ nan, nan, nan, ..., nan, nan, nan]])
上面两个例子都应用了同一个移动平均计算函数到一个2500行3000列的矩阵,第一个例子产生的结果与第二个有稍许不同,因为第一个例子应用的移动平均值窗口参数是变化的,要么为1要么为2,而第二个例子中的参数无法变化,全部为2(原因已经介绍过了),因此部分列结果相同,部分列不同。
两种方法的速度比较
测试结果发现完美解决问题,但是新的问题产生了,速度怎么样?毕竟我们是需要追求效率的!
下面是两种方法的速度测试结果,在一个2500行3000列的矩阵上应用一个移动平均值计算函数:
In [828]: %timeit np.array(list(map(moving_avg, val.T, pars))).T
127 ms ± 3.93 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [829]: %timeit np.apply_along_axis(moving_avg, 0, val, 2)
114 ms ± 307 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
结果显示,两种方法的速度几乎没有区别,map函数虽然将3000个不同的参数传入函数中,但速度并没有慢多少。与循环方案相比,两者都有数十倍的效率提升(同样的操作如果用pandas自带的iteritems循环来做,耗时将达到2s左右,如果用for-loop的话可能要十多秒开外了)
结论
两种方法都可以高效地实现函数在矩阵上的向量化应用,如果在应用过程中函数的参数固定,那么可以用apply_along_axis()函数,速度稍快,但如果需要动态改变参数(每只股票需要不同的参数),则推荐使用map()函数