Rcpp fast statistical mode function with vector input of any type

时间:2019-03-17 22:52:56

标签: c++ r rcpp

I'm trying to build a super fast mode function for R to use for aggregating large categorical datasets. The function should take vector input of all supported R types and return the mode. I have read This post, This Help-page and others, but I was not able to make the function take in all R data types. My code now works for numeric vectors, I am relying on Rcpp sugar wrapper functions:

#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;
}

In addition I was wondering if the 'narm' argument can be renamed 'na.rm' without giving errors, and of course if there is a faster way to code a mode function in C++, I would be grateful to know about it.

3 个答案:

答案 0 :(得分:5)

在您的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,并且您不必担心返回出现以下情况的每个值大多数。

可以修改下面的算法以返回true mode,但我将保留它作为练习(提示:您需要std::vector以及在it->second == myMax时采取某种措施)。 N.B.您还需要在// [[Rcpp::plugins(cpp11)]]std::unordered_map的cpp文件顶部添加auto

#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

答案 1 :(得分:3)

为了使该函数适用于任何矢量输入,可以对要支持的任何数据类型实现@JosephWood的算法,并从switch(TYPEOF(x))调用它。但这将是很多代码重复。相反,最好使通用函数可以在任何Vector<RTYPE>参数上使用。如果我们遵循R的范例,即一切都是向量,并且让函数也返回Vector<RTYPE>,则可以使用RCPP_RETURN_VECTOR。请注意,我们需要C ++ 11才能将其他参数传递给RCPP_RETURN_VECTOR调用的函数。一件棘手的事情是,您需要Vector<RTYPE>的存储类型才能创建合适的std::unordered_mapRcpp::traits::storage_type<RTYPE>::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

答案 2 :(得分:1)

为了跟进一些无耻的自我提升,我现在在CRAN上发布了一个软件包 collapse ,其中包括一整套的快速统计功能,以防它们泛泛函数fmode。该实现基于索引哈希,甚至比上述解决方案更快。 fmode可用于对矢量,矩阵,data.frame和dplyr分组的小标题执行简单的,分组的和/或加权的模式计算。语法:

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

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

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

聚合混合类型的数据。框架datafmean应用于数字,将fmode应用于分类列。也可以进行更多定制的呼叫。结合快速统计功能collap在大型数字数据上的速度与 data.table 一样快,分类和加权聚合的速度明显快于任何目前可以使用 data.table 完成。