检查一行中的所有值是否相同或缺失的最有效方法

The most efficient way to check if all values in a row are the same or are missing

我最初在 data.table 论坛上问过这个问题,并收到了一些很好的答案,但我希望找到一个不到一秒的解决方案(实际数据暗淡 3M x 303)

我在 R 社区的 post 和 julia 的 MWE(使用 DataFrames.jl 如果我使用了错误的包请告诉我)

julia> using DataFrames

julia> df = DataFrame(v1 = [1.0,2.1,3.0], v2 = [1,3,3], v3 = [missing,2.1,3.0])
3×3 DataFrame
 Row │ v1       v2     v3
     │ Float64  Int64  Float64?
─────┼───────────────────────────
   1 │     1.0      1  missing
   2 │     2.1      3        2.1
   3 │     3.0      3        3.0

julia> desired = [true,false,true]
3-element Vector{Bool}:
 1
 0
 1

结构很重要。这是 R 中的矩阵方法,使用 matrixStats 包 (source),它提供了在 C 中实现的优化矩阵函数。

x = sample.int(3, size = 303*3e6, replace = T)            
m = matrix(x, ncol = 303, byrow = T)
bench::mark(
  var = matrixStats::rowVars(m, na.rm = T) == 0
)

在我的(当然不是高性能的)机器上,对于 300 万行矩阵,这大约需要 3.5 秒。

在 Julia 中使用矩阵进行此类操作,与 R 类似,也是很自然的:

julia> using BenchmarkTools

julia> function helper(x)
           nonempty = false
           local fv
           for v in x
               if !ismissing(v)
                   if nonempty
                       v == fv || return false
                   else
                       fv = v
                       nonempty = true
                   end
               end
           end
           return true
       end
helper (generic function with 1 method)

julia> mat = rand([1, 2, missing], 3_000_000, 303);

julia> @benchmark helper.(eachrow($mat))
BenchmarkTools.Trial: 34 samples with 1 evaluation.
 Range (min … max):  139.440 ms … 154.628 ms  ┊ GC (min … max): 5.74% … 5.15%
 Time  (median):     147.890 ms               ┊ GC (median):    5.27%        
 Time  (mean ± σ):   147.876 ms ±   3.114 ms  ┊ GC (mean ± σ):  5.23% ± 0.95%

                    ▃    ▃   ▃  ▃  ██ ▃
  ▇▁▁▁▁▁▁▁▁▁▇▁▁▁▁▁▁▁█▁▁▁▁█▇▇▇█▁▁█▇▁██▇█▁▇▁▇▇▁▇▇▁▇▁▇▇▇▁▁▁▁▇▁▁▁▁▇ ▁
  139 ms           Histogram: frequency by time          155 ms <

 Memory estimate: 114.80 MiB, allocs estimate: 6.

该操作也可以在DataFrames.jl中完成,这里有一个例子:

julia> function helper2(x, i)
           nonempty = false
           local fv
           for v in x
               vv = v[i]
               if !ismissing(vv)
                   if nonempty
                       vv == fv || return false
                   else
                       fv = vv
                       nonempty = true
                   end
               end
           end
           return true
       end
helper2 (generic function with 1 method)

julia> df = DataFrame(mat, :auto, copycols=false); # copycols to avoid copying as data is large

julia> @benchmark helper2.(Ref(identity.(eachcol($df))), 1:nrow($df))
BenchmarkTools.Trial: 46 samples with 1 evaluation.
 Range (min … max):  105.265 ms … 123.345 ms  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     110.682 ms               ┊ GC (median):    0.00%
 Time  (mean ± σ):   110.581 ms ±   2.692 ms  ┊ GC (mean ± σ):  0.00% ± 0.00%

                ▄ ▂ ▄█▂
  ▄▁▁▄▄▁▁▆▄▁▁▁▆▆█▄█▆███▄▄▁█▄▁▁▁▁▁▁▁▁▁▁▁▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄ ▁
  105 ms           Histogram: frequency by time          123 ms <

 Memory estimate: 385.28 KiB, allocs estimate: 15.

(如果代码中有任何不清楚的地方请告诉我,我可以解释)


编辑

如果你是一个独特的小联盟 eltypes 做:

helper2.(Ref(collect(Union{unique(typeof.(eachcol(df)))...}, eachcol(df))), 1:nrow(df))

如果 Union{unique(typeof.(eachcol(df)))...} 不是一个小集合,那么另一个解决方案会更好,如果这对您来说足够好,请发表评论。

你有大约 350MB 的 RAM 可用吗?如果是这样,你可以试试这个功能

rowequal <- function(x) {
  undetermined <- function(x, can_del) {
    if (length(can_del) < 1L)
      return(x)
    x[-can_del]
  }
  if (ncol(x) < 1L)
    return(logical())
  out <- logical(nrow(x))
  if (ncol(x) < 2L)
    return(!out)
  x1 <- x[[1L]]
  need_compare <- undetermined(seq_len(nrow(x)), which(x1 != x[[2L]]))
  x1[nas] <- x[[2L]][nas <- which(is.na(x1))]
  if (ncol(x) > 2L) {
    for (j in 3:ncol(x)) {
      need_compare <- undetermined(
        need_compare, which(x1[need_compare] != x[[j]][need_compare])
      )
      x1[nas] <- x[[j]][nas <- which(is.na(x1))]
      if (length(need_compare) < 1L)
        return(out)
    }
  }
  `[<-`(out, need_compare, TRUE)
}

以下是基准

> dim(d)
[1] 3000000     300
> bench::mark(f(d), rowequal(d), iterations = 10)
# A tibble: 2 x 13
  expression       min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result memory time  gc   
  <bch:expr>  <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list> <list> <lis> <lis>
1 f(d)           2.97s    2.98s     0.335    34.4MB        0    10     0      29.8s <lgl ~ <Rpro~ <ben~ <tib~
2 rowequal(d)  88.52ms  93.34ms    10.7     352.2MB        0    10     0    932.5ms <lgl ~ <Rpro~ <ben~ <tib~

,其中 f(我从 this post 得到的)和 d

f <- function(x) {
  v1 = do.call(pmin, c(x, na.rm = TRUE))
  v2 = do.call(pmax, c(x, na.rm = TRUE))
  v1 == v2
}

mkDT <- function(rows, cols, type = as.integer) {
  data.table::setDT(
    replicate(cols, sample(type(c(1:10, NA)), rows, replace = TRUE), simplify = FALSE)
  )
}

d <- mkDT(3e6, 300)

代码的 Rcpp 版本。如果您可以确保数据框中的所有列都是 numeric 类型,则它可以实现最佳性能(在内存使用和速度方面)。我想这是解决您问题的最有效方法(在 R 中)。

rowequalcpp <- function(x) {
  if (ncol(x) < 1L)
    return(logical())
  out <- logical(nrow(x))
  if (ncol(x) < 2L)
    return(!out)
  mark_equal(out, x)
  out
}

Rcpp::sourceCpp(code = '
#include <Rcpp.h>

// [[Rcpp::export]]
void mark_equal(Rcpp::LogicalVector& res, const Rcpp::DataFrame& df) {
  Rcpp::NumericVector x1 = df[0];
  int n = df.nrows();
  int need_compare[n];
  for (int i = 0; i < n; ++i)
    need_compare[i] = i;
  for (int j = 1; j < df.length(); ++j) {
    Rcpp::NumericVector dfj = df[j];
    for (int i = 0; i < n; ) {
      int itmp = need_compare[i];
      if (Rcpp::NumericVector::is_na(x1[itmp]))
        x1[itmp] = dfj[itmp];
      if (!Rcpp::NumericVector::is_na(dfj[itmp]) && x1[itmp] != dfj[itmp]) {
        need_compare[i] = need_compare[n - 1];
        need_compare[n-- - 1] = itmp;
      } else
        ++i;
    }
    if (n < 1)
      return;
  }
  for (int i = 0; i < n; ++i)
    res[need_compare[i]] = TRUE;
}
')

基准(列的类型对性能至关重要):

> d <- mkDT(3000000, 300, as.integer)
> bench::mark(rowequal(d), rowequalcpp(d), iterations = 10)
# A tibble: 2 x 13
  expression        min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result memory time  gc   
  <bch:expr>     <bch:> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list> <list> <lis> <lis>
1 rowequal(d)     100ms   147ms      7.07     398MB     3.03     7     3      991ms <lgl ~ <Rpro~ <ben~ <tib~
2 rowequalcpp(d)  101ms   102ms      9.35     309MB     2.34     8     2      855ms <lgl ~ <Rpro~ <ben~ <tib~
> d <- mkDT(3000000, 300, as.numeric)
> bench::mark(rowequal(d), rowequalcpp(d), iterations = 10)
# A tibble: 2 x 13
  expression          min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result  memory   time 
  <bch:expr>     <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>  <list>   <lis>
1 rowequal(d)     103.7ms  110.8ms      8.05   349.3MB    0.895     9     1      1.12s <lgl [~ <Rprofm~ <ben~
2 rowequalcpp(d)   26.3ms   27.3ms     36.3     11.4MB    0        10     0    275.2ms <lgl [~ <Rprofm~ <ben~
# ... with 1 more variable: gc <list>

我建议你使用 python,它有两个很棒的产品,pandas 最流行的数据框架库和 polars 世界上最快的数据框架库! (db-benchmark).

import time
t1=time.time();df.max(axis=1)==df.min(axis=1);t2=time.time()

关于这个问题的快速更新, 我设法使用以下函数将时间减少到大约 2 秒(少于 3 MiB 内存使用量!),尚未使用并行性:(魔法在于使用 @.

julia> f(x,y) = ismissing(x) || ismissing(y) ? true : x == y

julia> function rowequal(df, cols)
           res=ones(Bool, nrow(df))
           for i in 2:length(cols)
               @. res &= ifelse(f(df[!, cols[i-1]], df[!,cols[i]]), true, false)
           end
           res
       end

关于此的另一个更新,现在我可以使用 InMemoryDatasets 将时间减少到不到一秒(实际上不到 0.5 秒),这要归功于 Julia lang 话语中的这个 discussion

julia> using InMemoryDatasets
julia> df = Dataset(df)
julia> f(x,y) = ismissing(x) || ismissing(y) ? true : x == y
julia> byrow(df, isless, [:v1, :v2, :v3], with = byrow(df, coalesce, :), lt = f)

更新

通过话语讨论的新更新,我几乎可以达到之前疯狂记录的一半 :D,即 0.27(Julia 社区真的很喜欢 SPEED

julia> byrow(df, issorted, [:v1, :v2, :v3], lt = !f)