如何用purrr::map2或pmap替代for循环实现含滞后自引用的指标计算
问题描述
计算多只股票自定义金融指标时(单只股票对应约5000行数据),希望避免低效的R层for循环。测试发现不涉及前序值引用的逐行计算,用purrr::map2运行正常,但遇到需要引用待创建向量滞后(lag)值的递归计算场景时,常规map函数无法得到正确结果。
无滞后引用的正常运行示例
不依赖前序计算结果时,purrr::map2可以直接完成逐行运算,示例代码:
some_function <- function(a, b) { (a * b) + ((1 - a) * b) } a <- c(0.019, 0.026, 0.012, 0.022) # 自定义指标 b <- c(15.5, 16.7, 14.8, 13.1) # 收盘价 purrr::map2(a, b, some_function)
运行输出和原始收盘价完全一致:
15.5, 16.7, 14.8, 13.1
带滞后引用的失效场景
需要构建新向量c,计算逻辑为:第一行c取值等于对应行的b值,其余行需要引用c自身的前一期值,计算逻辑:
desired_function <- function(a, b, c) { (a * b) + ((1 - a) * lag(c)) }
初始化向量c后尝试传入三个参数调用map类函数:
c <- c(15.5, 0, 0, 0) purrr::map2(a, b, c, desired_function)
运行后返回全NULL结果,不符合预期。该场景下c的预期计算结果应为:15.50, 15.53, 15.52, 15.47。
金融指标计算中引用前一期值是非常普遍的需求,仅靠R层for循环实现笨重低效,需要更合适的实现方式。
解决方案
这类递推计算的核心特点是每一步的结果都依赖上一步的输出,而常规的map/apply类函数是逐元素独立计算,迭代过程中不会传递、更新上一步的计算结果,因此天然不适合这类场景,推荐两种高效实现方式:
方案1:使用purrr::accumulate系列函数实现递推
purrr包提供的accumulate/accumulate2函数专门为这类携带状态的迭代设计,不需要手动编写for循环,底层做了性能优化,比手写R层for循环效率高很多,代码可读性也更好。
针对上述场景的实现代码:
library(purrr) c <- accumulate2( .x = a[-1], # 从第二期开始传入指标a的取值 .y = b[-1], # 从第二期开始传入收盘价b的取值 .f = function(prev_c, curr_a, curr_b) { # 上一期的c值会自动作为第一个参数传入函数 curr_a * curr_b + (1 - curr_a) * prev_c }, .init = b[1] # 设定第一期c的初始值为b的第一个元素 ) |> unlist()
运行得到的结果四舍五入后为15.50, 15.53, 15.52, 15.47,和预期完全一致。
如果需要按股票分组计算,只需要配合dplyr::group_by+dplyr::mutate,在分组内调用accumulate2即可,不需要写嵌套循环。
方案2:Rcpp实现C++层循环(高性能场景)
如果数据量更大(比如单票数据超过10万行、全市场股票批量计算),可以直接用Rcpp编写C层的递推逻辑,C层循环的性能比R层循环高1~2个数量级,5000行级别的计算耗时基本在微秒级,示例代码:
#include <Rcpp.h> using namespace Rcpp; // [[Rcpp::export]] NumericVector calc_indicator(NumericVector a, NumericVector b) { int n = a.size(); NumericVector c(n); c[0] = b[0]; for(int i = 1; i < n; i++) { c[i] = a[i] * b[i] + (1 - a[i]) * c[i-1]; } return c; }
在R中运行上述代码完成函数编译后,直接调用calc_indicator(a, b)即可得到结果。
注意:不要尝试给普通map函数传入提前初始化的向量实现递归,map函数不会在迭代过程中修改外部传入的向量,也不会自动传递上一轮的计算结果,永远无法得到正确的递推值。
内容的提问来源于stack exchange,提问作者Dan Hill

