I have a matrix with many rows and columns, of the nature
x <- matrix(c(1, 1, 3, 3, 55, 55, 1, 3, 3, 1,
1, 1, 3, 3, 55, 55, 1, 3, 9, 1), ncol = 2)
My problem
Within each group of duplicate rows, (i.e. each set of identical rows), I wish to identify the first row index and assign it to all occurences within that group. For example, there are several duplicate rows with 1 in both columns (on rows 1, 2, 7, 10). On each of these rows I want the first row index, i.e. 1.
x
# [,1] [,2]
# [1,] 1 1 # first row of 1-1. Assign its row index, 1, to all 1-1 rows
# [2,] 1 1
# [3,] 3 3 # first row of 3-3. Assign its row index, 3, to all 3-3 rows
# [4,] 3 3
# [5,] 55 55 # first row of 55-55. Assign its row index, 5, to all 55-55 rows
# [6,] 55 55
# [7,] 1 1
# [8,] 3 3
# [9,] 3 9 # first (and only) row of 3-9; row index 9
# [10,] 1 1
Desired result:
1 1 3 3 5 5 1 3 9 1
My attempt
The best I've come up with is a convoluted approach based on duplicated and for loops, that is neither efficient nor elegant. I'm also aware of possible solutions for data.frames; those involving concatenating rows into strings are quite resource-intensive too.
# Identify duplicates
duplicate <- duplicated(x, MARGIN = 1)
# Identify first occurrence of each duplicate
firstDup <- duplicated(x, MARGIN = 1, fromLast = TRUE) & !duplicate
indices <- which(firstDup)
# Initialize index for unique rows
index <- seq_len(dim(x)[1])
cf <- duplicate
for (i in indices) {
# Duplicates must occur after first occurrence
cf[seq_len(i)] <- FALSE
dups <- apply(x[cf, , drop = FALSE], 1L, identical, x[i, ])
index[which(cf)[dups]] <- i
}
index
Is there an elegant solution using base R?
TL;DR
For integer matrices of equal size but different shapes (
5e+06-by-2,5e+05-by-20, 5000-by-2000), containing integers from 1 to 10, the fastestbaseanswer tested wasgrouping/match, suggested in a comment by @alexis_laz. The fastest non-baseanswer wasdata.table::frank/match, thoughgrouping/matchwas comparable in all cases, even outperforming thedata.tableanswer in the 5000-by-2000 case.Note that results may vary for double matrices or integer matrices with greater range, and depending on the number of threads made available to
data.table. [TODO?]Background
@MikaelJagan's
asplit/match(<list>, <list>)answer seems like "an elegant solution using base R". However,?matchwarns:Given that the OP has "a matrix with many rows and columns", we wanted to compare the performance of the
asplit/match(<list>, <list>)answer to that of the otherbaseanswers:paste/match(<chr>, <chr>)answer;interaction/match(<int>, <int>)answer;grouping/match(<int>, <int>)answer.We benchmarked these alongside some non-
baseanswers, which we used as points of reference (recognizing that the OP asked forbaseonly):Rcppanswer;data.tableanswers:which = TRUEandmult = "first"to[.data.table;frank(ties.method = "average")/match(<dbl>, <dbl>),frank(ties.method = "dense")/match(<int>, <int>).Setup
Benchmarking
Many rows, few columns
We first assessed performance using a
5e+06-by-2 integer matrix:f_asplitis two orders of magnitude slower than thebasealternatives.f_groupingis the fastestbaseanswer, butf_frank_denseis faster by a factor of about 2 (and fastest overall).Fewer rows, more columns
The results above do not generalize to all integer matrix inputs. For example,
f_interactionscales very poorly withncol(x): the number of possible interactions isu^ncol(x)if each column ofxhasuunique elements.For this reason, we performed a second benchmark, this time considering a matrix with fewer rows (
5e+05) and more columns (20).An initial test of
f_interactionresulted in a memory allocation error, so it was excluded from the benchmark.f_groupingremains the fastestbaseanswer. Notably, it is now faster thanf_pasteby a full order of magnitude and only marginally slower thanf_frank_dense.Even fewer rows, even more columns
We performed a final benchmark excluding the slowest answers in the last round (
f_asplitandf_rcpp), now considering a 5000-by-2000 integer matrix:Now
f_groupingis fastest overall, and faster thanf_frank_denseby a factor of about 3.