向量化 sapply 函数

MCP*_*tor 10 performance r vectorization

我正在尝试对以下函数进行矢量化以删除 sapply 循环。我正在计算累积偏度。

cskewness <- function(.x) {
  skewness <- function(.x) {
    sqrt(length(.x)) * sum((.x - mean(.x))^3) / (sum((.x - mean(.x))^2)^(3 / 2))
  }
  sapply(seq_along(.x), function(k, z) skewness(z[1:k]), z = .x)
}
Run Code Online (Sandbox Code Playgroud)

我的代数没搞对。有这个是错误的:

skewness2 <- function(.x) {
  n <- length(.x)
  csum <- cumsum(.x)
  cmu <- csum / 1:length(.x)
  num <- cumsum(.x - cmu)^3
  den <- cumsum((.x - cmu)^2)^(3/2)
  sqrt(n) * num / den
}
Run Code Online (Sandbox Code Playgroud)

正确的代码会产生:

x <- c(1,2,4,5,8)

> cskewness(x)
[1]       NaN 0.0000000 0.3818018 0.0000000 0.4082483
> skewness2(x)
[1]      NaN 1.000000 1.930591 3.882748 4.928973
Run Code Online (Sandbox Code Playgroud)

Tho*_*ing 9

更新

感谢jblood94努力捕捉重大异常值可能造成的危险,让我有机会重新思考和优化代码。

我们可以用它scale来预处理输入向量,这样可以最小化异常值的有害影响,并且偏度也不会受到影响

cskewness_tic <- function(y) {
    # rescale `y` to avoid detrimental impacts by outliers
    y <- scale(y)
    # cumulative length of y
    k <- seq_along(y)
    # cumulative n-th raw moments of y
    m3 <- cumsum(y^3)
    m2 <- cumsum(y^2)
    m1 <- cumsum(y)
    u <- m1 / k
    # expand cubic terms and refactor skewness in terms of num/den
    num <- (m3 - 3 * u * m2 + 3 * u^2 * m1 - k * u^3) / k
    den <- sqrt((m2 + k * u^2 - 2 * u * m1) / k)^3
    c(NaN, (num / den)[-1])
}
Run Code Online (Sandbox Code Playgroud)

基准

包含用于基准测试的函数列表(请参阅@jblood94 的答案的详细cumskewness信息)cumskewnessCpp

set.seed(0)
x <- sample(1e6) + 1e10
microbenchmark(
    cskewness_tic = cskewness_tic(x),
    cumskewness = cumskewness(x),
    cumskewnessCpp = cumskewnessCpp(x),
    check = "equal",
    unit = "relative",
    times = 50L
)
Run Code Online (Sandbox Code Playgroud)

节目

Unit: relative
           expr      min       lq     mean   median       uq      max neval
  cskewness_tic 3.144719 3.268279 3.461112 3.314326 3.846614 2.395820    50
    cumskewness 4.647973 4.627621 4.455523 4.585634 4.448119 2.687571    50
 cumskewnessCpp 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000    50
Run Code Online (Sandbox Code Playgroud)

以前的解决方案

首先,非矢量化方法并不像您想象的那么糟糕,因此*apply如果您认为它符合您的最终目标并简化您的工作,那么它也是一个不错的选择。无需带着恐惧面对循环。


如果您想从矢量化中受益(例如速度),您可以尝试根据原始矩扩展偏度的数学表达式(请参阅thisthis),然后cumsum对每个解耦项进行运算

cskewness_tic <- function(y) {
    # cumulative length of y
    k <- seq_along(y)
    # cumulative n-th raw moments of y
    m3 <- cumsum(y^3)
    m2 <- cumsum(y^2)
    m1 <- cumsum(y)
    u <- m1 / k
    # expand cubic terms and refactor skewness in terms of num/den
    num <- (m3 - 3 * u * m2 + 3 * u^2 * m1 - k * u^3) / k
    den <- sqrt((m2 + k * u^2 - 2 * u * m1) / k)^3
    num / den
}
Run Code Online (Sandbox Code Playgroud)

这样

> cskewness_tic(x)
[1]       NaN 0.0000000 0.3818018 0.0000000 0.4082483
Run Code Online (Sandbox Code Playgroud)

基准

方法列表

cskewness_origin <- function(.x) {
    skewness <- function(.x) {
        sqrt(length(.x)) * sum((.x - mean(.x))^3) / (sum((.x - mean(.x))^2)^(3 / 2))
    }
    sapply(seq_along(.x), function(k, z) skewness(z[1:k]), z = .x)
}

cskewness_jblood94 <- function(.x) {
    d <- outer(.x, cumsum(.x) / (1:length(.x)), "-")
    d[lower.tri(d)] <- 0
    sqrt(1:length(.x)) * colSums(d^3) / colSums(d^2)^(3 / 2)
}

cskewness_tic <- function(y) {
    # cumulative length of y
    k <- seq_along(y)
    # cumulative n-th order moments of y
    m3 <- cumsum(y^3)
    m2 <- cumsum(y^2)
    m1 <- cumsum(y)
    u <- m1 / k
    # expand cubic terms and refactor skewness in terms of num/den
    num <- (m3 - 3 * u * m2 + 3 * u^2 * m1 - k * u^3) / k
    den <- sqrt((m2 + k * u^2 - 2 * u * m1) / k)^3
    num / den
}
Run Code Online (Sandbox Code Playgroud)

通过运行以下基准测试

set.seed(0)
x <- sample(1e3)
microbenchmark(
    origin = cskewness_origin(x),
    jblood94 = cskewness_jblood94(x),
    tic = cskewness_tic(x),
    unit = "relative",
    check = "equivalent",
    times = 50L
)
Run Code Online (Sandbox Code Playgroud)

我们看

Unit: relative
     expr      min       lq     mean   median       uq      max neval
   origin 252.7504 247.1442 253.5965 254.8663 257.2905 239.0963    50
 jblood94 302.8267 309.0854 326.7546 318.7141 310.2365 388.3786    50
      tic   1.0000   1.0000   1.0000   1.0000   1.0000   1.0000    50
Run Code Online (Sandbox Code Playgroud)


jbl*_*d94 7

使用单遍for循环将是高效的:

cumskewness <- function(x) {
  n <- length(x)
  
  if (n == 0L) {
    return(x)
  } else if (n == 1L) {
    return(0)
  }
  
  m2 <- m3  <- term1 <- 0
  out <- numeric(n)
  out[1] <- NaN
  m1 <- x[1]
  
  for (i in 2:n) {
    n0 <- i - 1
    delta <- x[i] - m1
    delta_n <- delta/i
    m1 <- m1 + delta_n
    term1 <- delta*delta_n*n0
    m3 <- m3 + term1*delta_n*(n0 - 1) - 3*delta_n*m2
    m2 <- m2 + term1
    out[i] <- sqrt(i)*m3/m2^1.5
  }
  
  out
}
Run Code Online (Sandbox Code Playgroud)

写起来也很简单Rcpp

library(Rcpp)

cppFunction('
  NumericVector cumskewnessCpp(const NumericVector& x) {
    const int n = x.size();
    
    if (n == 0) {
      return(x);
    } else if (n == 1) {
      return(0);
    }
    
    int n1;
    double m1 = x(0);
    double m2, m3, delta, delta_n, term1;
    NumericVector out(n);
    out(0) = R_NaN;
    
    for (int i = 1; i < n; i++) {
      n1 = i + 1;
      delta = x(i) - m1;
      delta_n = delta/n1;
      m1 += delta_n;
      term1 = delta*delta_n*i;
      m3 += term1*delta_n*(i - 1) - 3*delta_n*m2;
      m2 += term1;
      out(i) = sqrt(n1)*m3/pow(m2, 1.5);
    }
    
    return out;
  }
')
Run Code Online (Sandbox Code Playgroud)

测试

cumskewness(x)
#> [1] NaN 0.0000000 0.3818018 0.0000000 0.4082483
cumskewnessCpp(x)
#> [1] NaN  0.000000e+00  3.818018e-01 -2.808667e-17  4.082483e-01
Run Code Online (Sandbox Code Playgroud)

标杆管理

包括来自 @ThomisIsCoding 的矢量化解决方案:

cskewness_tic <- function(y) {
  # cumulative length of y
  k <- seq_along(y)
  # cumulative n-th order moments of y
  m3 <- cumsum(y^3)
  m2 <- cumsum(y^2)
  m1 <- cumsum(y)
  u <- m1 / k
  # expand cubic terms and refactor skewness in terms of num/den
  num <- (m3 - 3 * u * m2 + 3 * u^2 * m1 - k * u^3) / k
  den <- sqrt((m2 + k * u^2 - 2 * u * m1) / k)^3
  num / den
}

set.seed(0)
x <- sample(1e3)

microbenchmark::microbenchmark(
  cskewness_tic = cskewness_tic(x),
  cumskewness = cumskewness(x),
  cumskewnessCpp = cumskewnessCpp(x),
  check = "equal",
  unit = "relative"
)
#> Unit: relative
#>            expr      min      lq     mean   median       uq       max neval
#>   cskewness_tic 2.035272 2.07954 2.006118 2.388216 2.282022 0.7228815   100
#>     cumskewness 3.930424 3.96835 4.003956 3.939762 3.711879 1.1339946   100
#>  cumskewnessCpp 1.000000 1.00000 1.000000 1.000000 1.000000 1.0000000   100
Run Code Online (Sandbox Code Playgroud)

使用注意事项cskewness_tic

灾难性的抵消可能会导致高阶矩的精度误差。当标准差相对平均值较小时就会发生这种情况。展示:

set.seed(0)
x <- sample(1e3) + 1e8

y1 <- cskewness(x)
y2 <- cumskewness(x)
y3 <- cumskewnessCpp(x)
y4 <- cskewness_tic(x); y4[1] <- NaN

all.equal(y1, y2)
#> [1] TRUE
all.equal(y1, y3)
#> [1] TRUE
all.equal(y1, y4)
#> [1] "Mean relative difference: 270.1872"
Run Code Online (Sandbox Code Playgroud)

原答案

skewness2 <- function(.x) {
  d <- outer(.x, cumsum(.x)/(1:length(.x)), "-")
  d[lower.tri(d)] <- 0
  sqrt(1:length(.x))*colSums(d^3)/colSums(d^2)^(3/2)
}

x <- c(1,2,4,5,8)
cskewness(x)
#> [1]       NaN 0.0000000 0.3818018 0.0000000 0.4082483
skewness2(x)
#> [1]       NaN 0.0000000 0.3818018 0.0000000 0.4082483
Run Code Online (Sandbox Code Playgroud)