有没有比table()更高效的替代方法?

12

我使用以下命令:

table(factor("list",levels=1:"n")

使用 "list" :(示例)a = c(1,3,4,4,3)levels = 1:5,同时考虑2和5。 对于非常大的数据集,我的代码似乎非常低效。

是否有人知道一个隐藏的库或代码片段可以使它更快?


你的数据集是一个 data.frame 吗? - Martin Gal
最初,它是一个向量列表。实际上,我使用unlist()将它们转换为一个巨大的向量。但是如果有任何机会使用data.frame,数据可以放在数据框中。 - elmoBlue
请提供一个样本数据集和期望的输出。 - GuedesBF
输入:a = c(1,3,4,4,3)输出:result = c(1, 0, 2, 2, 0) [1:1, 2:0, 3:2, 4:2, 5:0] - elmoBlue
3
请在问题编辑中进行修改,而不是在评论部分进行修改。@elmoBlue - GuedesBF
7个回答

13

我们可以使用collapse中的fnobs,这将是高效的。

library(collapse)
fnobs(df, g = df$X1)

base R中,与table相比,tabulate更为高效。

 tabulate(df$X1)
 [1]  9  6 15 13 11  9  7  9 11 10

1
fnobs看起来非常快!非常感谢你。 - elmoBlue

11

简而言之,获胜者是base::tabulate

总的来说,基本目标是性能,所以我准备了所有提供的解决方案的microbenchmark。我使用了小型和大型向量,两种不同的场景。对于我的机器上的collapse包,我必须下载最新的Rcpp包1.0.7(以防止崩溃)。即使是由我添加的Rcpp解决方案也比base::tabulate更慢。

suppressMessages(library(janitor))
suppressMessages(library(collapse))
suppressMessages(library(dplyr))
suppressMessages(library(cpp11))

# source https://dev59.com/A4zda4cB1Zd3GeqPkESz
Rcpp::cppFunction('IntegerVector tabulate_rcpp(const IntegerVector& x, const unsigned max) {
    IntegerVector counts(max);
    for (auto& now : x) {
        if (now > 0 && now <= max)
            counts[now - 1]++;
    }
    return counts;
}')

set.seed(1234)

a = c(1,3,4,4,3)
levels = 1:5
df <- data.frame(X1 = a)


microbenchmark::microbenchmark(tabulate_rcpp = {tabulate_rcpp(df$X1, max(df$X1))},
                               base_table = {base::table(factor(df$X1, 1:max(df$X1)))},
                               stats_aggregate = {stats::aggregate(. ~ X1, cbind(df, n = 1), sum)},
                               graphics_hist = {hist(df$X1, plot = FALSE, right = FALSE)[c("breaks", "counts")]},
                               janitor_tably = {adorn_totals(tabyl(df, X1))},
                               collapse_fnobs = {fnobs(df, df$X1)},
                               base_tabulate = {tabulate(df$X1)},
                               dplyr_count = {count(df, X1)})
#> Unit: microseconds
#>             expr      min        lq       mean    median        uq       max
#>    tabulate_rcpp    2.959    5.9800   17.42326    7.9465    9.5435   883.561
#>       base_table   48.524   59.5490   72.42985   66.3135   78.9320   153.216
#>  stats_aggregate  829.324  891.7340 1069.86510  937.4070 1140.0345  2883.025
#>    graphics_hist  148.561  170.5305  221.05290  188.9570  228.3160   958.619
#>    janitor_tably 6005.490 6439.6870 8137.82606 7497.1985 8283.3670 53352.680
#>   collapse_fnobs   14.591   21.9790   32.63891   27.2530   32.6465   417.987
#>    base_tabulate    1.879    4.3310    5.68916    5.5990    6.6210    16.789
#>      dplyr_count 1832.648 1969.8005 2546.17131 2350.0450 2560.3585  7210.992
#>  neval
#>    100
#>    100
#>    100
#>    100
#>    100
#>    100
#>    100
#>    100


df <- data.frame(X1 = sample(1:5, 1000, replace = TRUE))

microbenchmark::microbenchmark(tabulate_rcpp = {tabulate_rcpp(df$X1, max(df$X1))},
                               base_table = {base::table(factor(df$X1, 1:max(df$X1)))},
                               stats_aggregate = {stats::aggregate(. ~ X1, cbind(df, n = 1), sum)},
                               graphics_hist = {hist(df$X1, plot = FALSE, right = FALSE)[c("breaks", "counts")]},
                               janitor_tably = {adorn_totals(tabyl(df, X1))},
                               collapse_fnobs = {fnobs(df, df$X1)},
                               base_tabulate = {tabulate(df$X1)},
                               dplyr_count = {count(df, X1)})
#> Unit: microseconds
#>             expr      min        lq       mean    median        uq       max
#>    tabulate_rcpp    4.847    8.8465   10.92661   10.3105   12.6785    28.407
#>       base_table   83.736  107.2040  121.77962  118.8450  129.9560   184.427
#>  stats_aggregate 1027.918 1155.9205 1338.27752 1246.6205 1434.8990  2085.821
#>    graphics_hist  209.273  237.8265  274.60654  258.9260  300.3830   523.803
#>    janitor_tably 5988.085 6497.9675 7833.34321 7593.3445 8422.6950 13759.142
#>   collapse_fnobs   26.085   38.6440   51.89459   47.8250   57.3440   333.034
#>    base_tabulate    4.501    6.7360    8.09408    8.2330    9.2170    11.463
#>      dplyr_count 1852.290 2000.5225 2374.28205 2145.9835 2516.7940  4834.544
#>  neval
#>    100
#>    100
#>    100
#>    100
#>    100
#>    100
#>    100
#>    100

本文创建于2021年08月01日,使用reprex包 (v2.0.0)


1
很棒的基准测试!为你出色的工作点赞! - ThomasIsCoding
干的不错。你可以通过两种方式改进你简洁明了的Rcpp函数:“循环展开”(一次跳过多个元素),和/或者通过OpenMP。此外,已经有一个Rcpp的'sugar'函数table()。最后,“为了防止崩溃”:你通常应该更新所有的包。对于编译在它上面的包,我们确实需要安装Rcpp 1.0.7。最后,你在这里还有一个问题:你的数据集太小了,不现实。我切换到a <- sample(1000, 10000, TRUE),现在你的tabulate和基础R table都是最快的。 - Dirk Eddelbuettel
哦,而且你没有使用“levels”,也没有测试“factor”,所以设置略有不同。 - Dirk Eddelbuettel
再试一次:我尝试了OpenMP;但由于结果向量是共享的,它并没有起到帮助作用。 - Dirk Eddelbuettel
非常感谢。我从来没有想过janitor会这么慢! - Anoushiravan R

10

我们也可以使用 janitor::tabyl

library(janitor)

df %>%
  tabyl(X1) %>%
  adorn_totals()

    X1   n percent
     1   9    0.09
     2   6    0.06
     3  15    0.15
     4  13    0.13
     5  11    0.11
     6   9    0.09
     7   7    0.07
     8   9    0.09
     9  11    0.11
    10  10    0.10
 Total 100    1.00

8

这不完全是您要寻找的内容,但或许您可以使用这个:

library(dplyr)
set.seed(8192)

df <- data.frame(X1 = sample(1:10, 100, replace = TRUE))

df %>% 
  count(X1)

返回值

   X1  n
1   1  9
2   2  6
3   3 15
4   4 13
5   5 11
6   6  9
7   7  7
8   8  9
9   9 11
10 10 10

如果您需要计算更多数字(包括丢失的数字),您可以使用
library(tidyr)
library(dplyr)

df2 <- data.frame(X1 = 1:12)

df %>% 
  count(X1) %>% 
  right_join(df2, by="X1") %>% 
  mutate(n = replace_na(n, 0L))

获取

   X1  n
1   1  9
2   2  6
3   3 15
4   4 13
5   5 11
6   6  9
7   7  7
8   8  9
9   9 11
10 10 10
11 11  0
12 12  0

1
我真的可以用它 :) 非常感谢。这让我意识到我应该更经常使用 dplyr。 - elmoBlue

6

这里还有一个: summarytools

来自Martin Gal的数据!非常感谢:

library(summarytools)

set.seed(8192)
df <- data.frame(X1 = sample(1:10, 100, replace = TRUE))

summarytools::freq(df$X1, cumul=FALSE)

输出:

              Freq   % Valid   % Total
----------- ------ --------- ---------
          1      9      9.00      9.00
          2      6      6.00      6.00
          3     15     15.00     15.00
          4     13     13.00     13.00
          5     11     11.00     11.00
          6      9      9.00      9.00
          7      7      7.00      7.00
          8      9      9.00      9.00
          9     11     11.00     11.00
         10     10     10.00     10.00
       <NA>      0                0.00
      Total    100    100.00    100.00

2
强大的工具!酷! - ThomasIsCoding

5

使用 aggregate 的基础 R 选项(从@Martin Gal借用df

> aggregate(. ~ X1, cbind(df, n = 1), sum)
   X1  n
1   1  9
2   2  6
3   3 15
4   4 13
5   5 11
6   6  9
7   7  7
8   8  9
9   9 11
10 10 10

另一个选择是使用 hist

> hist(df$X1, plot = FALSE, right = FALSE)[c("breaks", "counts")]
$breaks
 [1]  1  2  3  4  5  6  7  8  9 10

$counts
[1]  9  6 15 13 11  9  7  9 21

2
如果需要比 table() 更快的替代方案,包括交叉制表,collapse::qtab() 是一个忠实且明显更快的选择,自 v1.8.0(2022年5月)起可用。在单变量情况下也可以使用 fcount(),它会返回一个数据框。
library(collapse) # > v1.8.0, and > 1.9.0 for fcount()
library(microbenchmark)
v = sample(10000, 1e6, TRUE)

microbenchmark(qtab(v, sort = FALSE), fcount(v), tabulate(v), times = 10)
Unit: milliseconds
                  expr      min       lq     mean   median       uq      max neval
 qtab(v, sort = FALSE) 1.911707 1.945245 2.002473 1.963654 2.027942 2.207891    10
             fcount(v) 1.885549 1.906746 1.978894 1.932310 2.103997 2.138027    10
           tabulate(v) 2.321543 2.323716 2.333839 2.328206 2.334499 2.372506    10

v2 = sample(10000, 1e6, TRUE)
microbenchmark(qtab(v, v2), qtab(v, v2, sort = FALSE), table(v, v2), times = 10)
Unit: milliseconds
                      expr       min        lq      mean   median        uq      max neval
               qtab(v, v2)  45.61279  51.14840  74.16168  60.7761  72.86385 157.6501    10
 qtab(v, v2, sort = FALSE)  41.30812  49.66355  57.02565  51.3568  54.69859 118.1289    10
              table(v, v2) 281.60079 282.85273 292.48119 286.0535 288.19253 349.5513    10


话虽如此,就 C 代码而言,tabulate() 几乎是最快的。但它有一个明显的缺点,即它根本不对值进行哈希处理,而是确定最大值并分配一个结果向量,使用它作为表来计算值。请考虑以下内容:
v[10] = 1e7L # Adding a random large value here
length(tabulate(v))
[1] 10000000
length(table(v))
[1] 10001
length(qtab(v))
[1] 10001

所以你得到一个结果向量,其中包含 6.99 百万个零,并且你的性能会下降。

microbenchmark(qtab(v, sort = FALSE), fcount(v), tabulate(v), times = 10)
Unit: milliseconds
                  expr      min       lq     mean   median       uq       max neval
 qtab(v, sort = FALSE) 1.873249 1.900473 1.966721 1.923064 2.064186  2.126588    10
             fcount(v) 1.829338 1.850330 1.926676 1.880199 2.021013  2.057667    10
           tabulate(v) 4.207789 4.357439 5.066296 4.417012 4.558216 10.347744    10

鉴于这一点,qtab() 实际上对每个值进行哈希并实现了这种性能,这是相当了不起的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接