Select matrix rows that are permutations of a given vector

Viewed 551

I have a matrix X:

     one two three four
 [1,]  1   3   2   4
 [2,]  2   0   1   5
 [3,]  3   2   1   4
 [4,]  4   9   11  19
 [5,]  4   3   2   1

I want to get a new matrix Y which only contains rows that are permutations of "1", "2", "3", "4". That is:

     one two three four
 [1,]  1   3   2   4
 [3,]  3   2   1   4
 [5,]  4   3   2   1

What function or command should I use?

8 Answers
mat <- rbind(
    c(1, 3, 2, 4),
    c(2, 0, 1, 5),
    c(3, 2, 1, 4)
)

ok <- apply(mat, 1L, function(x) setequal(x, c(1, 2, 3, 4)))
mat[ok, ]

Your example matrix and target vector:

X <- structure(c(1, 2, 3, 4, 4, 3, 0, 2, 9, 3, 2, 1, 1, 11, 2, 4, 5, 4, 19, 1),
               dim = 5:4)
v <- 1:4

But let's construct a more challenging one (thanks to user harre):

X <- rbind(X, 1, c(1, 2, 1, 2))

A fully vectorized approach (using package matrixStats)

rk <- matrix(match(X, v, nomatch = 0L), nrow(X), ncol(X))
ct <- matrixStats::rowTabulates(rk, values = 1:length(v))
zo <- matrixStats::rowCounts(ct, value = 0L)

## all rows that are permutations of 'v'
X[zo == 0L, ]
#     [,1] [,2] [,3] [,4]
#[1,]    1    3    2    4
#[2,]    3    2    1    4
#[3,]    4    3    2    1

## remove rows that are permutations of 'v'
X[zo > 0L, ]

Another fully vectorized method (base R)

This is a mathematical solution. For a nonlinear and asymmetric weight function w(x), the following weighted sum:

1 x w(1) + 2 x w(2) + 3 x w(3) + 4 x w(4)

is a unique score or identifier and is invariant to permutations. So for example, the following gives the same value:

2 x w(2) + 1 x w(1) + 3 x w(3) + 4 x w(4)

But anything else will give different values, like:

1 x w(1) + 3 x w(1) + 3 x w(3) + 4 x w(4)

0 x w(0) + 3 x w(1) + 0 x w(0) + 4 x w(4)

Here is an implementation using cosine weights. It works even if X and v are floating point numbers or characters.

## method = "tab" for tabulation method
## method = "cos" for cosine weights method
FindPerm <- function (X, v, method) {
  ## number of elements
  n <- length(v)
  if (ncol(X) != n) stop("inconformable 'X' and 'v'!")
  if (anyDuplicated(v)) stop("'v' should not contain duplicated values!")
  ## convert everything to integers 0, 1, 2, ..., n
  Xi <- matrix(match(X, v, nomatch = 0L), nrow(X), ncol(X))
  vi <- 1:n
  ## branches
  if (method == "tab") {
    ## row-wise tabulating
    rtab <- matrixStats::rowTabulates(Xi, values = vi)
    ## the i-th value is TRUE if X[i, ] is a permutation of v
    matrixStats::rowCounts(rtab, value = 0L) == 0L
  } else if (method == "cos") {
    ## evaluate cosine weights for Xi and vi
    w <- pi / (n + 1)
    cos.Xi <- cos(w * Xi)
    cos.vi <- cos(w * vi)
    ## weighted sum for Xi
    wtsum.Xi <- rowSums(Xi * cos.Xi)
    ## weighted sum for vi
    wtsum.vi <- sum(vi * cos.vi)
    ## the i-th value is TRUE if X[i, ] is a permutation of v
    wtsum.Xi == wtsum.vi
  } else {
    stop("unknown method!")
  }
}
X[FindPerm(X, v, "cos"), ]
#     [,1] [,2] [,3] [,4]
#[1,]    1    3    2    4
#[2,]    3    2    1    4
#[3,]    4    3    2    1

Benchmark

Performance depends on the number of values in v. The tabulation method will slow down as v becomes long.

## a benchmark function, relying on package "microbenchmark"
## nr: number of matrix rows
## nc: number of elements in 'v'
bm <- function (nr, nc) {
  X <- matrix(sample.int(nc + 1L, nr * nc, replace = TRUE), nr)
  v <- 1:nc
  microbenchmark::microbenchmark("tab" = FindPerm(X, v, "tab"),
                                 "cos" = FindPerm(X, v, "cos"),
                                 check = "identical")
}

bm(2e+4, 4)
#Unit: milliseconds
# expr      min       lq     mean   median       uq      max
#  tab 4.302674 4.324236 4.536260 4.336955 4.359814 7.039699 
#  cos 4.846893 4.872361 5.163209 4.882942 4.901288 7.837580

bm(2e+4, 20)
#Unit: milliseconds
# expr      min       lq     mean   median       uq       max
#  tab 30.63438 30.70217 32.73508 30.77588 33.08046 135.64322
#  cos 21.16669 21.26161 22.28298 21.37563 23.60574  26.31775 

Update since there's so much interest in this question, here's a method using indexing to give a speed boost on Zheyuan Li's excellent generalization of my original answer.

The idea is to index on a length(v)-dimensional array for small v, or to index on v*sin(w*v) using the results of match instead of calculating X*sin(W*X) when v is large:

library(RcppAlgos)

# simplified version of Zheyuan Li's function
f1 <- function(X, v) {
  n <- length(v)
  Xi <- matrix(match(X, v, nomatch = 0L), nrow(X), ncol(X))
  vi <- 1:n
  w <- pi/(n + 1)
  rowSums(Xi*sin(Xi*w)) == sum(vi*sin(vi*w))
}

f2 <- function(X, v) {
  n <- length(v)
  
  if (n < 6) {
    # index an n-dimensional array
    m <- array(FALSE, rep(n + 1L, n))
    m[permuteGeneral(n)] <- TRUE
    X[] <- match(X, v, nomatch = length(v) + 1L)
    m[X]
  } else {
    nn <- 1:n
    u <- c(nn*sin(pi*nn/(n + 1L)), 0)
    X[] <- u[match(X, v, nomatch = n + 1L)]
    rowSums(X) == sum(u)
  }
}

set.seed(123)
# using Zheyuan Li's test dataset
nr <- 2000; nc <- 4
X <- matrix(sample.int(nc + 1L, nr * nc, replace = TRUE), nr)
v <- 1:nc

microbenchmark::microbenchmark(f1 = f1(X, v),
                               f2 = f2(X, v),
                               check = "identical")
#> Unit: microseconds
#>  expr   min     lq    mean median     uq    max neval
#>    f1 344.4 367.25 438.932 374.05 386.75 5960.6   100
#>    f2  81.9  85.00 163.332  88.90  98.50 6924.4   100

# Zheyuan Li's larger test dataset
set.seed(123)
nr <- 2000; nc <- 20
X <- matrix(sample.int(nc + 1L, nr * nc, replace = TRUE), nr)
v <- 1:nc

microbenchmark::microbenchmark(f1 = f1(X, v),
                               f2 = f2(X, v),
                               check = "identical")
#> Unit: microseconds
#>  expr    min      lq     mean  median     uq    max neval
#>    f1 1569.2 1575.45 1653.510 1601.30 1683.6 3972.6   100
#>    f2  355.2  359.90  431.705  366.85  408.6 2253.8   100

Original answer edited to use X + exp(1/X) (see comments).

This should work with positive integers:

Y <- X[rowSums(X + exp(1/X)) == sum(1:4 + exp(1/(1:4))),]

Benchmarking against the apply solution:

f1 <- function(x) x[apply(x, 1L, function(x) setequal(x, 1:4)),]
f2 <- function(x) x[rowSums(x + exp(1/x)) == sum(1:4 + exp(1/(1:4))),]

X <- matrix(sample(10, 4e5, TRUE), 1e5)
microbenchmark::microbenchmark(f1 = f1(X),
                               f2 = f2(X),
                               times = 10,
                               check = "equal")
#> Unit: milliseconds
#>  expr      min       lq      mean    median       uq      max neval
#>    f1 448.2680 450.8778 468.55179 461.62620 472.0022 542.0455    10
#>    f2  28.5362  28.6889  31.50941  29.44845  30.2693  50.4402    10

This question is HOT. I am learning so I take this as a good opportunity to learn. It is really difficult for me to come up with new solutions, but I found two things missing here:

  1. there is no serious validation of these answers;
  2. there is no benchmark for all of them.

I would like to convert each answer to a function that returns a TRUE/FALSE vector for flagging rows. I also want this function to work with any matrix and any vector.

  • Stéphane Laurent's answer, Zheyuan Li's answer and ThomasIsCoding's answer need minimal adaption.

  • Mohamed Desouky's answer is also easy to adapt, by taking out the function applied in Filter() and apply() it over matrix rows.

  • jblood94's answer is challenging. It was commented that for other matrices and vectors, conversion is needed using match. I don't know what is the appropriate way, but I saw match in Zheyuan Li's answer, so I borrowed that part.

  • TarJae's answer is awful (sorry; don't take this as an insult). None of them seems to work. I don't see any comparison between matrix rows and vectors in the base R solution. For other tidyverse codes, I don't know what df_matrix is. I have requested TarJae to please revise the answer.

  • harre's answer uses tidyverse and dose not return TRUE/FALSE. So I have to exclude it from the benchmark (sorry).

Here are the functions for the benchmark.

S.Laurent <- function (X, v) apply(X, 1L, function(x) setequal(x, v))

Z.Li <- function (X, v, method) {
  ## number of elements
  n <- length(v)
  if (ncol(X) != n) stop("inconformable 'X' and 'v'!")
  if (anyDuplicated(v)) stop("'v' should not contain duplicated values!")
  ## convert everything to integers 0, 1, 2, ..., n
  Xi <- matrix(match(X, v, nomatch = 0L), nrow(X), ncol(X))
  vi <- 1:n
  ## branches
  if (method == "tab") {
    ## row-wise tabulating
    rtab <- matrixStats::rowTabulates(Xi, values = vi)
    ## the i-th value is TRUE if X[i, ] is a permutation of v
    matrixStats::rowCounts(rtab, value = 0L) == 0L
  } else if (method == "cos") {
    ## evaluate cosine weights for Xi and vi
    w <- pi / (n + 1)
    cos.Xi <- cos(w * Xi)
    cos.vi <- cos(w * vi)
    ## weighted sum for Xi
    wtsum.Xi <- rowSums(Xi * cos.Xi)
    ## weighted sum for vi
    wtsum.vi <- sum(vi * cos.vi)
    ## the i-th value is TRUE if X[i, ] is a permutation of v
    wtsum.Xi == wtsum.vi
  } else {
    stop("unknown method!")
  }
}

Thomas <- function (X, v) colSums(mapply(`%in%`, list(v), asplit(X, 1))) == ncol(X)

M.Desouky <- function (X, v) apply(X, 1, function (x) all((x %in% v) & length(unique(x)) == length(v)))

jblood94 <- function (X, v) {
  Xi <- matrix(match(X, v, nomatch = 0L), nrow(X), ncol(X))
  vi <- 1:length(v)
  rowSums(Xi + exp(1/Xi)) == sum(vi + exp(1/vi))
}

For benchmark, I followed the setup in Zheyuan Li's answer.

library(matrixStats)
library(microbenchmark); library(ggplot2)

nr <- 2000; nc <- 4
X <- matrix(sample.int(nc + 1L, nr * nc, replace = TRUE), nr)
v <- 1:nc
microbenchmark("S.Laurent" = S.Laurent(X, v),
               "Z.Li.tab" = Z.Li(X, v, "tab"),
               "Z.Li.cos" = Z.Li(X, v, "cos"),
               "Thomas" = Thomas(X, v),
               "M.Desouky" = M.Desouky(X, v),
               "jblood94" = jblood94(X, v),
               check = "identical") -> bm1
autoplot(bm1)

"bm1.png"

nr <- 2000; nc <- 20
X <- matrix(sample.int(nc + 1L, nr * nc, replace = TRUE), nr)
v <- 1:nc
microbenchmark("S.Laurent" = S.Laurent(X, v),
               "Z.Li.tab" = Z.Li(X, v, "tab"),
               "Z.Li.cos" = Z.Li(X, v, "cos"),
               "Thomas" = Thomas(X, v),
               "M.Desouky" = M.Desouky(X, v),
               "jblood94" = jblood94(X, v),
               check = "identical") -> bm2
autoplot(bm2)

"bm2.png"

I don't know how time is transformed for plotting, but clearly, they are not on the usual scale. Those to the left are far faster than it is suggested from the plot!

Conclusion: Zheyuan Li's "cos" method is the winner.

Another option is using Filter function

t(Filter(\(x) all((x %in% 1:4) & length(unique(x)) == 4) ,
data.frame(t(X))))

Just for fun

Who can give me 4 distinct digits whose sum equal 6 ?

  • there is just {0,1,2,3}

then we can use the module of 4 using %%

X[apply(X , 1 , \(x) sum(unique(x %% 4)) == 6 & length(unique(x)) == 4) , ]

OR

with Using pure for loop

ans <- data.frame(matrix(NA , ncol = ncol(X)))
r <- 1
for(i in 1:nrow(X)){
    if(all((X[i,] %in% 1:4) & length(unique(X[i,])) == 4)){
        ans[r,] <- X[i,]
        r <- r + 1
    }
}

ans <- as.matrix(ans)
  • output
  X1 X2 X3 X4
1  1  3  2  4
2  3  2  1  4
3  4  3  2  1

  • data
X <- matrix(c(1, 2, 3, 4, 4, 3, 0, 2, 9, 3, 2, 1, 1, 11, 2, 4, 
              5, 4, 19, 1) , ncol = 4)

We can try this

> mat[colSums(mapply(`%in%`, list(1:4), asplit(mat, 1))) == ncol(mat), ]
     [,1] [,2] [,3] [,4]
[1,]    1    3    2    4
[2,]    3    2    1    4
[3,]    4    3    2    1

For the fun of tidyverse-solutions, even if I think we'd rather work on the matrices directly. However, we could use rowwise() and c_across():

With set-operations (inspired by @Stéphane Laurent):

library(dplyr)

mat %>% 
  as_tibble() |>
  rowwise() |>
    filter(setequal(c_across(), c(1, 2, 3, 4))) |>
  ungroup() |>
  as.matrix()

Or without set-operations:

library(dplyr)

mat %>% 
  as_tibble() |>
  rowwise() |>
  filter(1 %in% c_across(everything()) &
         2 %in% c_across(everything()) &
         3 %in% c_across(everything()) &
         4 %in% c_across(everything())
         ) |>
  ungroup() |>
  as.matrix()

Or inspired by @Mohamed Desouky:

mat %>% 
    as_tibble() |>
    rowwise() |>
      filter(all(c_across() %in% 1:4) & n_distinct(c_across()) == 4) |>
    ungroup() |>
    as.matrix()

And so on..

The algorithm library in C++ offers a function called std::is_permutation that does just the trick.

The workhorse function below uses Rcpp and is fairly straightforward.

#include <Rcpp.h>

// [[Rcpp::export]]
SEXP perm_idx_cpp(Rcpp::IntegerMatrix mat, const std::vector<int> &v) {
  
    const int nRows = mat.nrow();
    const int nCols = mat.ncol();
    
    std::vector<int> test(nCols);
    Rcpp::LogicalVector res(nRows);
    
    for (int i = 0; i < nRows; ++i) {
        for (int j = 0; j < nCols; ++j) {
            test[j] = mat(i, j);
        }
        
        res[i] = std::is_permutation(
            test.begin(), test.end(), v.begin()
        );
    }
    
    return res;
}

And calling it in R we have (N.B. We use match to get integer indices as in @Zheyuan Li's answer, which is absolutely brilliant btw!):

get_perm_idx <- function(X, v) {
    Xi <- matrix(match(X, v, nomatch = 0L), ncol = ncol(X))
    perm_idx_cpp(Xi, seq_along(v))
}

It is very efficient as well. Here is a simple benchmark:

nr <- 2e4
nc <- 20
X <- matrix(sample.int(nc + 1L, nr * nc, replace = TRUE), nr)
v <- 1:nc
microbenchmark("tab" = FindPerm(X, v, "tab"),
               "cos" = FindPerm(X, v, "cos"),
               "is_perm_cpp" = get_perm_idx(X, v),
               check = "identical")
Unit: milliseconds
        expr       min        lq     mean    median        uq       max neval
         tab 33.641345 36.479660 39.00994 37.402306 39.560015  54.88057   100
         cos  9.496309 12.887493 15.30122 13.306302 14.053643 132.24079   100
 is_perm_cpp  3.232093  4.819553  6.08687  4.993367  5.248818  19.56919   100

You could probably squeeze out some extra efficiency, but it is tough to beat the simplicity here.

Related