【问题标题】:Rcpp fast statistical mode function with vector input of any type具有任意类型向量输入的 Rcpp 快速统计模式函数
【发布时间】:2019-03-17 22:52:56
【问题描述】:

我正在尝试为 R 构建一个超快速模式函数,用于聚合大型分类数据集。该函数应采用所有支持的 R 类型的向量输入并返回模式。我已经阅读了This postThis Help-page 和其他人,但我无法让该函数接受所有 R 数据类型。我的代码现在适用于数字向量,我依赖于 Rcpp 糖包装函数:

#include <Rcpp.h>

using namespace Rcpp;

// [[Rcpp::export]]
int Mode(NumericVector x, bool narm = false) 
{
    if (narm) x = x[!is_na(x)];
    NumericVector ux = unique(x);
    int y = ux[which_max(table(match(x, ux)))];
    return y;
}

此外,我想知道是否可以将 'narm' 参数重命名为 'na.rm' 而不会出错,当然,如果有更快的方法用 C++ 编写一个模式函数,我将不胜感激。

【问题讨论】:

  • @JosephWood 回答了您的性能问题。关于对任何类型的向量类型的支持,您做了哪些尝试?
  • 我尝试使用带有基于this post 的类型宏的模板。使用 Joseph 的代码,我会这样开始 template &lt;int RTYPE&gt; Vector&lt;RTYPE&gt; Mode_temp(Vector&lt;RTYPE&gt; x, bool narm = false) { if (narm) x = x[!is_na(x)]; int myMax = 1; &lt;RTYPE&gt; myMode = x[0]; std::unordered_map&lt;&lt;RTYPE&gt;, &lt;RTYPE&gt;&gt; modeMap; ,然后是 // [[Rcpp::export]] SEXP Mode( SEXP x ){ RCPP_RETURN_VECTOR(Mode_temp, x) ; },但不理解语法,如果它与 unordered_map 一起使用。
  • 对我来说更好的选择似乎是this page 上描述的“开关”语法,但在这里我也遇到了向量输入、标量输出和内部处理代码(即创建向量和每个 switch case 中都有一个特定的类型。)

标签: c++ r rcpp


【解决方案1】:

为了使函数适用于任何向量输入,您可以为您想要支持的任何数据类型实现@JosephWood 算法,并从switch(TYPEOF(x)) 调用它。但这将是很多代码重复。相反,最好创建一个可以处理任何Vector&lt;RTYPE&gt; 参数的通用函数。如果我们遵循 R 的范式,即一切都是向量,并让函数也返回一个Vector&lt;RTYPE&gt;,那么我们可以使用RCPP_RETURN_VECTOR。请注意,我们需要 C++11 才能将其他参数传递给 RCPP_RETURN_VECTOR 调用的函数。一件棘手的事情是您需要Vector&lt;RTYPE&gt; 的存储类型才能创建合适的std::unordered_mapRcpp::traits::storage_type&lt;RTYPE&gt;::type 来救援了。但是,std::unordered_map 不知道如何处理来自 R 的复数。为简单起见,我禁用了这种特殊情况。

把它们放在一起:

#include <Rcpp.h>
using namespace Rcpp ;

// [[Rcpp::plugins(cpp11)]]
#include <unordered_map>

template <int RTYPE>
Vector<RTYPE> fastModeImpl(Vector<RTYPE> x, bool narm){
  if (narm) x = x[!is_na(x)];
  int myMax = 1;
  Vector<RTYPE> myMode(1);
  // special case for factors == INTSXP with "class" and "levels" attribute
  if (x.hasAttribute("levels")){
    myMode.attr("class") = x.attr("class");
    myMode.attr("levels") = x.attr("levels");
  }
  std::unordered_map<typename Rcpp::traits::storage_type<RTYPE>::type, int> modeMap;
  modeMap.reserve(x.size());

  for (std::size_t i = 0, len = x.size(); i < len; ++i) {
    auto it = modeMap.find(x[i]);

    if (it != modeMap.end()) {
      ++(it->second);
      if (it->second > myMax) {
        myMax = it->second;
        myMode[0] = x[i];
      }
    } else {
      modeMap.insert({x[i], 1});
    }
  }

  return myMode;
}

template <>
Vector<CPLXSXP> fastModeImpl(Vector<CPLXSXP> x, bool narm) {
  stop("Not supported SEXP type!");
}

// [[Rcpp::export]]
SEXP fastMode( SEXP x, bool narm = false ){
  RCPP_RETURN_VECTOR(fastModeImpl, x, narm);
}

/*** R
set.seed(1234)
s <- sample(1e5, replace = TRUE)
fastMode(s)
fastMode(s + 0.1)
l <- sample(c(TRUE, FALSE), 11, replace = TRUE) 
fastMode(l)
c <- sample(letters, 1e5, replace = TRUE)
fastMode(c)
f <- as.factor(c)
fastMode(f) 
*/

输出:

> set.seed(1234)

> s <- sample(1e5, replace = TRUE)

> fastMode(s)
[1] 85433

> fastMode(s + 0.1)
[1] 85433.1

> l <- sample(c(TRUE, FALSE), 11, replace = TRUE) 

> fastMode(l)
[1] TRUE

> c <- sample(letters, 1e5, replace = TRUE)

> fastMode(c)
[1] "z"

> f <- as.factor(c)

> fastMode(f) 
[1] z
Levels: a b c d e f g h i j k l m n o p q r s t u v w x y z

如上所述,使用的算法来自 Joseph Wood's answer,它已在 CC-BY-SA 和 GPL >= 2 下明确获得双重许可。我正在关注 Joseph,并特此在 @ 下许可此答案中的代码987654323@(版本 2 或更高版本)以及隐式 CC-BY-SA 许可证。

【讨论】:

  • 非常感谢@RalfStubner !!我自己永远也不会做到这一点。在大型分类数据上,您的函数比基本解决方案快约 25 倍,太棒了!如果找不到模式,我唯一要添加到代码中的是默认为第一个值,即初始化 ` Vector myMode(1);我的模式 = x[0];`。感谢所有人,尤其是 JosephWood,我相信很多人都会觉得这很有帮助。
  • @Sebastian 欢迎您。不过,速度要归功于 Joseph Wood。
  • @RalfStubner ,一如既往的出色工作!我实际上在写答案时想到了您,因为我知道您正在为dqrng 中的采样函数实现哈希函数。我迫不及待想看到最终结果!
  • @AndriSignorell 你可以在返回myMode之前添加myMode.attr("freq") = myMax;
  • 最好,感谢您澄清复杂的法律情况。所以我会提到你们都是作者,并附有帖子的链接。
【解决方案2】:

在您的Mode 函数中,由于您主要调用糖包装函数,因此您不会看到比基本R 有太多改进。事实上,只需编写一个忠实的基础 R 翻译,我们就有:

baseMode <- function(x, narm = FALSE) {
    if (narm) x <- x[!is.na(x)]
    ux <- unique(x)
    ux[which.max(table(match(x, ux)))]
}

而基准测试,我们有:

set.seed(1234)
s <- sample(1e5, replace = TRUE)

library(microbenchmark)
microbenchmark(Mode(s), baseMode(s), times = 10, unit = "relative")
Unit: relative
       expr      min       lq     mean   median       uq      max neval
    Mode(s) 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000    10
baseMode(s) 1.490765 1.645367 1.571132 1.616061 1.637181 1.448306    10

通常,当我们努力编写自己的编译代码时,我们会期望获得更大的收益。简单地将这些已经有效的编译函数包装在Rcpp 中不会神奇地让您获得预期的收益。事实上,在更大的例子上,基本解决方案更快。观察:

set.seed(1234)
sBig <- sample(1e6, replace = TRUE)

system.time(Mode(sBig))
 user  system elapsed 
1.410   0.036   1.450 

system.time(baseMode(sBig))
 user  system elapsed 
0.915   0.025   0.943 

为了解决您编写更快模式函数的问题,我们可以使用std::unordered_map,它与底层的table 非常相似(即它们本质上都是哈希表)。此外,由于您返回的是单个整数,我们可以放心地假设我们可以将 NumericVector 替换为 IntegerVector 并且您不关心返回出现次数最多的每个值。 p>

可以修改下面的算法以返回true mode,但我将把它留作练习(提示:您将需要std::vector 以及在it-&gt;second == myMax 时采取某种行动)。注:您还需要在 cpp 文件的顶部为 std::unordered_mapauto 添加 // [[Rcpp::plugins(cpp11)]]

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::plugins(cpp11)]]
#include <unordered_map>

// [[Rcpp::export]]
int fastIntMode(IntegerVector x, bool narm = false) {
    if (narm) x = x[!is_na(x)];
    int myMax = 1;
    int myMode = 0;
    std::unordered_map<int, int> modeMap;
    modeMap.reserve(x.size());

    for (std::size_t i = 0, len = x.size(); i < len; ++i) {
        auto it = modeMap.find(x[i]);

        if (it != modeMap.end()) {
            ++(it->second);
            if (it->second > myMax) {
                myMax = it->second;
                myMode = x[i];
            }
        } else {
            modeMap.insert({x[i], 1});
        }
    }

    return myMode;
}

以及基准测试:

microbenchmark(Mode(s), baseMode(s), fastIntMode(s), times = 15, unit = "relative")
Unit: relative
          expr      min       lq     mean   median       uq      max neval
       Mode(s) 6.428343 6.268131 6.622914 6.134388 6.881746  7.78522    15
   baseMode(s) 9.757491 9.404101 9.454857 9.169315 9.018938 10.16640    15
fastIntMode(s) 1.000000 1.000000 1.000000 1.000000 1.000000  1.00000    15

现在我们正在谈论...大约比原始版本快 6 倍,比基础版本快 9 倍。它们都返回相同的值:

fastIntMode(s)
##[1] 85433

baseMode(s)
##[1] 85433

Mode(s)
##[1] 85433

对于我们更大的例子:

## base R returned in 0.943s
system.time(fastIntMode(s))
 user  system elapsed 
0.217   0.006   0.224

除了隐式 CC-BY-SA 许可之外,我特此在 GPL &gt;= 2 下许可此答案中的代码。

【讨论】:

  • 是否也建议使用boost::unordered_map 而不是std::unordered_map 以提高速度。
  • 谢谢@F.Privé,我会调查的
  • 非常感谢@JosephWood 的扩展回答!我已经认为 Rcpp 糖不会让我走得太远。我认为 true Mode 您的意思是在适用的情况下返回多种模式......我只需要其中一种。如果您还知道如何进行方法分派,即这与因子、字符和逻辑向量一起工作,那将是非常棒的,因为这确实是我花了很多时间试图做的却没有成功的事情..
  • 也感谢@F.Privé 的这个建议,我尝试对其进行编码:#include &lt;Rcpp.h&gt; using namespace Rcpp; // [[Rcpp::plugins(cpp11)]] // [[Rcpp::depends(BH)]] #include &lt;unordered_map&gt; #include &lt;boost/unordered_map.hpp&gt; // [[Rcpp::export]] int fastIntMode2(IntegerVector x, bool narm = false) { if (narm) x = x[!is_na(x)]; int myMax = 1; int myMode = x[0]; boost::unordered_map&lt;int, int&gt; modeMap; // etc ... 但它并不快:比上面的代码慢了大约 5%。
  • 感谢您的反馈。
【解决方案3】:

为了跟进一些无耻的自我推销,我现在在 CRAN 上发布了一个包collapse,其中包括一整套快速统计函数,其中包括通用的函数fmode。该实现基于索引散列,甚至比上述解决方案更快。 fmode 可用于对向量、矩阵、data.frames 和 dplyr 分组小标题执行简单、分组和/或加权模式计算。语法:

fmode(x, g = NULL, w = NULL, ...)

其中x 是向量、矩阵、data.frame 或grouped_df,g 是分组向量或分组向量列表,w 是权重向量。函数collap 进一步提供了分类和混合聚合问题的紧凑解决方案。代码

collap(data, ~ id1 + id2, FUN = fmean, catFUN = fmode)

聚合混合类型 data.frame datafmean 应用于数字和 fmode 应用于分类列。也可以进行更多自定义呼叫。与 快速统计函数 一起,collap 在处理大型数值数据时与 data.table 一样快,并且分类和加权聚合比目前任何可以实现的方法都要快得多用 data.table 完成。

【讨论】:

    猜你喜欢
    • 2020-08-16
    • 1970-01-01
    • 2011-10-20
    • 1970-01-01
    • 2013-08-24
    • 1970-01-01
    • 2021-03-02
    • 2013-04-15
    • 1970-01-01
    相关资源
    最近更新 更多