【问题标题】:Identify the original row numbers of filtered rows识别过滤行的原始行号
【发布时间】:2017-07-26 16:03:46
【问题描述】:

我想过滤行并将(源数据的)原始行号存储在data.table 列中。

我知道.I 有一个语法变体(请参阅https://stackoverflow.com/a/23586059/4468078),但这需要我过滤两次我想要避免的内容:

DT <- mtcars
setDT(DT)
row.numbers <- DT[, .I[ gear > 4]]
# > row.numbers
# [1] 27 28 29 30 31
DT[row.numbers, .(row.numbers, gear)]
#    row.numbers gear
# 1:          27    5
# 2:          28    5
# 3:          29    5
# 4:          30    5
# 5:          31    5

如果我使用普通的.I 语法,它会返回子集的行号,而不是来自原始数据:

DT[gear > 4, .(.I, gear)]

   I gear
1: 1    5
2: 2    5
3: 3    5
4: 4    5
5: 5    5

对于更简单/优化的解决方案有什么想法吗?

【问题讨论】:

  • 这可能是可能的,但据我了解,在j 参数中的任何计算之前,data.table 是i 参数中的子集。

标签: r data.table


【解决方案1】:

编辑 2:添加了 @Frank 的 w2 变体...

为了补充@UweBlock 接受的答案,我做了一些基准测试,我想在这里展示以分享结果:

library(data.table)
library(microbenchmark)
# size: about 800 MB
DT <- data.table(x = sample(1000, 1E8, replace = TRUE), y = sample(1000, 1E8, replace = TRUE))
LIMIT <- 500

microbenchmark(row.filter = {
  row.numbers <- DT[, .I[x > LIMIT]]
  res <- DT[row.numbers, .(row.numbers, x, y)]
},
chaining = {
  res <- DT[, row.number := .I][x > LIMIT, .(row.number, x, y)]
},
w2 = {
  w = DT[x > LIMIT, which = TRUE ]
  DT[w, c("x","y")][, w := w ]
},
times = 20)

结果是:

Unit: seconds
       expr      min       lq     mean   median       uq      max neval cld
 row.filter 2.039627 2.152165 2.290105 2.284775 2.381632 2.652203    20   b
   chaining 2.032791 2.272493 2.369759 2.359630 2.472028 2.777191    20   b
         w2 1.104414 1.194826 1.274428 1.257893 1.311050 1.557225    20  a 

编辑1:比较过滤器选择性的影响:

对于LIMIT &lt;- 100

Unit: seconds
       expr      min       lq     mean   median       uq      max neval cld
 row.filter 3.254134 3.638193 4.053991 3.865599 4.432278 5.337939    20   b
   chaining 3.005504 3.874443 4.116179 4.069974 4.391666 4.994020    20   b
         w2 1.289617 1.588608 1.965523 1.962185 2.294457 2.859689    20  a 

对于LIMIT &lt;- 900

Unit: milliseconds
       expr      min       lq     mean   median       uq       max neval cld
 row.filter 900.9504 905.0694 914.9406 907.5211 916.2071  964.6856    20  b 
   chaining 927.1630 932.0981 965.8222 970.9336 981.5885 1030.6396    20   c
         w2 607.0091 609.8028 620.5582 612.0490 615.2337  669.9706    20 a  

【讨论】:

  • 很好,特别是检查过滤器的选择性。看来,对于较少的行(较短的 row.number 向量,使用 x &gt; 900 过滤),row.filter 方法的速度大约快 1.5%,而对于较长的 row.number 向量,chaining 的速度大约快 0.5%。跨度>
  • 这是另一个基准测试,虽然我没有耐心将它运行超过 3 次:chat.stackoverflow.com/transcript/message/38334338#38334338
  • 而且...w2 = { w = DT[x &gt; th, which = TRUE ]; DT[w, c("x","y")][, w := w ] } 对我来说是最快的。 (如果您首先需要行号,可以在结果中使用setcolorder。)
  • 我正在更新基准并试图了解它为什么更快(使用which,不同的链接顺序?)
  • @Frank 如果你发布答案,我会改变我的投票(抱歉 Uwe,我太快了)
【解决方案2】:

为什么要进行另一个基准测试?

  1. Frank 在他的 comment 中提到,从 .(rn, gear) 切换到 c("rn", "gear") 可能会加快速度,但没有单独进行基准测试。
  2. R yoda's benchmark 中,样本数据的类型为integer,但LIMIT &lt;- 500 的类型为doubledata.table 偶尔会警告类型转换,所以我想知道在这种情况下类型转换可能会对性能产生什么影响。

以什么为基准?

到目前为止,已经提供了 3 个答案,构成了五个代码变体:

很遗憾,我无法让 row.filter 在 SE 版本中工作。

使用了哪些参数?

  • 问题大小(行数):102、103、...、108
  • LIMIT 的不同值:100、500、900
  • LIMIT的类型:integerdouble来测试类型转换的效果

重复次数是根据问题大小计算得出的,最少运行 3 次,最多运行 100 次。

结果

类型转换确实会花费大约 4%(中位数)到 9%(平均)的性能。所以确实很重要,如果你写LIMIT &lt;- 500LIMIT &lt;- 500L 使用L 来表示一个整数常量。

使用非标准评估的性能损失要高得多:对于这两种方法,NSE 平均需要比 SE 多 50% 以上的时间。
(请注意,下面的图表仅显示类型 integer 的结果)

下面的限制 500 和类型 integer 的图表表明,对于所有问题大小,SE 变体都比 NSE 变体更快。有趣的是,对于高达 5000 行的较小问题,chaining_se 似乎比 which_se 略有优势,而对于超过 5 M 行的问题,which_se 是明显更快。

根据要求,下表显示了上图的时间(以毫秒为单位):

dcast(bm_med[limit == 500L & type == "int"][
  , expr := forcats::fct_reorder(factor(expr), -time)],
  expr ~ n_rows, fun.aggregate = function(x) max(x/1E6), value.var = "time")
           expr       100      1000     10000    1e+05    1e+06    1e+07    1e+08
1: chaining_nse 0.8189745 0.8493695 1.0115405 2.870750 22.34469 441.1621 2671.179
2:   row.filter 0.7693225 0.7972635 0.9622665 2.677807 21.30861 247.3984 2677.495
3:    which_nse 0.8486145 0.8690035 1.0117295 2.620980 18.39406 219.0794 2341.990
4:  chaining_se 0.5299360 0.5582545 0.6454755 1.700626 12.48982 166.0164 2049.904
5:     which_se 0.5894045 0.6114935 0.7040005 1.624166 13.00125 130.0718 1289.050

基准代码

library(data.table)
library(microbenchmark)
run_bm <- function(n_rows, limit = 500L, type = "int") {
  set.seed(1234L)
  DT <- data.table(x = sample(1000, n_rows, replace = TRUE), 
                   y = sample(1000, n_rows, replace = TRUE))
  LIMIT <- switch(type,
                  int = as.integer(limit),
                  dbl = as.double(limit))
  times <- round(scales::squish(sqrt(1E8 / n_rows) , c(3L, 100L)))
  cat("Start run:", n_rows, limit, type, times, "\n")
  microbenchmark(row.filter = {
    row.numbers <- DT[, .I[x > LIMIT]]
    DT[row.numbers, .(row.numbers, x, y)]
  },
  chaining_nse = {
    DT[, row.number := .I][x > LIMIT, .(row.number, x, y)]
  },
  chaining_se = {
    DT[, row.number := .I][x > LIMIT, c("row.number", "x", "y")]
  },
  which_nse = {
    row.numbers <- DT[x > LIMIT, which = TRUE ]
    DT[row.numbers, .(x, y)][, row.numbers := row.numbers ][]
  },
  which_se = {
    row.numbers <- DT[x > LIMIT, which = TRUE ]
    DT[row.numbers, c("x", "y")][, row.numbers := row.numbers][]
  },
  times = times)
}
# parameter
bm_par <- CJ(n_rows = 10^seq(2L, 8L, 1L), 
             limit = seq(100L, 900L, 400L), 
             type = c("int", "dbl"))
# run the benchmarks
bm_raw <- bm_par[, run_bm(n_rows, limit, type), by = .(n_rows, limit, type)]
# aggregate results
bm_med <- bm_raw[, .(time = median(time)), by = .(n_rows, limit, type, expr)]

图形代码

library(ggplot2)

# chart 1
ggplot(
  dcast(bm_med, n_rows + limit + expr ~ type, value.var = "time")[
    , ratio := dbl / int - 1.0] #[limit == 500L]
) + 
  aes(n_rows, ratio, colour = expr) +
  geom_point() + 
  geom_line() + 
  facet_grid(limit ~ expr) + 
  scale_x_log10(labels = function(x) scales::math_format()(log10(x))) +
  scale_y_continuous(labels = scales::percent) + 
  coord_cartesian(ylim = c(-0.1, 0.5)) +
  geom_hline(yintercept = 0) +
  theme_bw() +
  ggtitle("Performance loss due to type conversion") +
  ylab("Relative computing time dbl vs int") + 
  xlab("Number of rows (log scale)")
ggsave("p2.png")

# chart 2
ggplot(
  dcast(bm_med[, c("code", "eval") := tstrsplit(expr, "_")][!is.na(eval)], 
        n_rows + limit + type + code ~ eval, value.var = "time")[
          , ratio := nse / se - 1.0][type == "int"]
) + 
  aes(n_rows, ratio, colour = code) +
  geom_point() + 
  geom_line() + 
  facet_grid(limit  + type ~ code) + 
  scale_x_log10(labels = function(x) scales::math_format()(log10(x))) +
  scale_y_continuous(labels = scales::percent) + 
  geom_hline(yintercept = 0) +
  theme_bw() +
  ggtitle("Performance loss due to non standard evaluation") +
  ylab("Relative computing time NSE vs SE") + 
  xlab("Number of rows (log scale)")
ggsave("p3.png")

# chart 3
ggplot(bm_med[limit == 500L][type == "int"]) + 
  aes(n_rows, time/1E6, colour = expr) +
  geom_point() + 
  geom_smooth(se = FALSE) + 
  facet_grid(limit ~ type) +
  facet_grid(type ~ limit) +
  scale_x_log10(labels = function(x) scales::math_format()(log10(x))) +
  scale_y_log10(labels = function(x) scales::math_format()(log10(x))) +
  theme_bw() +
  ggtitle("Benchmark results (log-log scale)") +
  ylab("Computing time in ms (log scale)") + 
  xlab("Number of rows (log scale)")
ggsave("p1.png")

【讨论】:

【解决方案3】:

@RYoda 回答中的示例要快一点:

w = DT[x > LIMIT, which = TRUE ]
DT[w, c("x","y")][, w := w ]

要更改结果中列的顺序,setcolorder 应该可以工作,几乎不需要时间。

【讨论】:

    【解决方案4】:

    可以在过滤前添加一列行号:

    library(data.table)
    data.table(mtcars)[, rn := .I][gear > 4, .(rn, gear)]
    
       rn gear
    1: 27    5
    2: 28    5
    3: 29    5
    4: 30    5
    5: 31    5
    

    基准测试

    只是对mtcars 数据集(32 行)的快速基准测试,它非常小,但这里的重点是开销。

    microbenchmark::microbenchmark(
      copy = DT <- data.table(mtcars),
      ryoda = {
        DT <- data.table(mtcars)
        row.numbers <- DT[, .I[ gear > 4]]
        DT[row.numbers, .(row.numbers, gear)]
      },
      uwe = {
        DT <- data.table(mtcars)
        DT[, rn := .I][gear > 4, .(rn, gear)]
      },
      times = 1000L
    )
    
    Unit: microseconds
      expr      min       lq     mean   median       uq       max neval cld
      copy  691.710  727.192  803.235  749.385  785.428 15989.293  1000 a  
     ryoda 1821.869 1883.479 2001.653 1930.213 2011.124  6650.497  1000  b 
       uwe 1860.288 1934.191 2053.004 1987.927 2077.370  5908.892  1000   c
    

    请注意,每次基准测试运行都是从 DT 的新副本开始的,因为其中一个代码正在修改 DT(使用 :=)。

    在这里,对于 32 行的微小样本数据集,链接似乎有 50 到 60 微秒的损失。 R Yoda's benchmark results 具有 800 M 行的大型数据集,根据过滤的行数,即 row.numbers 的长度,两个方向上的差异约为 1%。

    【讨论】:

    • 是的,链接是一种选择,但也可能会降低性能(尽管它看起来很小,因为只附加了一个新的列/向量)。但是过滤只进行一次,很好的答案!
    • 哦,这很有趣。我知道管道(%&gt;%)可能会降低性能,但我不知道链接也可能会降低性能(我相信data.table 的人非常关注效率)。你有这方面的例子吗?
    • 幸运一击,我又找到了:r.789695.n4.nabble.com/…(与内存使用有关,但内存使用可能会消耗性能)
    • 非常感谢您提供的链接。我想知道这将如何在今天,2 年半和很多 data.table 之后的优化中得到回答。
    • 绝对正确,值得提出一个新的 SO 问题,因为 @arun 参与了链接讨论。我认为已经有一个 SO 问题、答案或评论将我带到此链接,但我找不到它 ATM。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-05
    • 2016-09-14
    • 2013-05-11
    • 1970-01-01
    相关资源
    最近更新 更多