ふとbase::quantile()と自作関数を比べていると、baseの方が2~3倍くらい速いことに気付いた。
base_quantile <- function(x, probs) { # my_quantile()と結果を合わせるためにunname()する # typeは、1か3が「〇%の位置の要素を取り出す」感じのやつっぽい unname(quantile(x, probs = probs, type = 3)) } my_quantile <- function(x, probs) { sort(x)[floor(length(x) * probs)] } x <- runif(1e6) bench::mark( base_quantile(x, probs = 0.3), my_quantile(x, probs = 0.3) ) #> # A tibble: 2 x 10 #> expression min mean median max `itr/sec` mem_alloc n_gc n_itr #> <chr> <bch> <bch> <bch:t> <bch:t> <dbl> <bch:byt> <dbl> <int> #> 1 base_quan~ 32ms 33ms 32.4ms 34.6ms 30.3 19.3MB 9 3 #> 2 my_quanti~ 100ms 107ms 105.4ms 114.4ms 9.37 11.4MB 2 3 #> # ... with 1 more variable: total_time <bch:tm>
こんなシンプルな実装なのに、なぜ?と思っていつものようにr-sourceを見に行くと、どうもsort()のpartialという引数が胆らしい。
x <- sort(x, partial = unique(c(1, j[j>0L & j<=n], (j+1)[j>0L & j<n], n)) )
(r-source/quantile.R at 5a156a0865362bb8381dcd69ac335f5174a4f60c · wch/r-source · GitHub)
?sort曰く、
If
partialis notNULL, it is taken to contain indices of elements of the result which are to be placed in their correct positions in the sorted array by partial sorting.
とのことで、これはpartial sortingというやつらしい。要は、指定した位置の要素が確定したら並べ替えを終わる(という説明であってる?)。
Rのquantile()の場合は「要素を取り出す」わけではなくて線形補間した値を計算したり、Infの存在を考えてるのでややこしくなってるけど、基本的にはこんな感じでよさそう。
my_quantile2 <- function(x, probs) { idx <- floor(length(x) * probs) sort(x, partial = idx)[idx] } x <- runif(1e6) bench::mark( base_quantile(x, probs = 0.3), my_quantile(x, probs = 0.3), my_quantile2(x, probs = 0.3) ) #> # A tibble: 3 x 10 #> expression min mean median max `itr/sec` mem_alloc n_gc n_itr #> <chr> <bch:> <bch:t> <bch:t> <bch:t> <dbl> <bch:byt> <dbl> <int> #> 1 base_quan~ 34.4ms 35.9ms 35.9ms 37.3ms 27.9 19.1MB 8 3 #> 2 my_quanti~ 99.1ms 112.6ms 107.1ms 131.7ms 8.88 11.4MB 2 3 #> 3 my_quanti~ 23.2ms 26.1ms 24.2ms 30.9ms 38.2 11.4MB 7 10 #> # ... with 1 more variable: total_time <bch:tm>
なるほど速くなった。
quantile()についてはこのスライド、
ソートのアルゴリズムについてはこのQiitaがまとまってそうだった。流し読みしかしてないけど。