Is there a faster implementation for getting a numeric index of a vector than as.numeric(as.factor(x))?

Viewed 90

Say you have a vector of letters and you want to return a numeric index of that vector e.g.

c("a","b","c","c","d","a","b") would return c(1,2,3,3,4,1,2)

Is there a faster method than the below function 'index'?

x <- sample(letters,100000, replace = T)
index <- function(x)as.numeric(as.factor(x))

microbenchmark::microbenchmark(index((x)))
#Unit: milliseconds
#      expr      min       lq     mean   median       uq      max neval
#index((x)) 3.124058 3.154997 4.117221 3.176013 3.310194 25.19272   100

Thank you

4 Answers

You can use fmatch from fastmatch like below

fastmatch::fmatch(x, sort(unique(x)))

Benchmarking

x <- sample(letters, 1e6, replace = T)
index <- function(x) as.numeric(as.factor(x))
index2 <- function(x) match(x, sort(unique(x)))
index3 <- function(x) chmatch(x, sort(unique(x)))
index4 <- function(x) fmatch(x, sort(unique(x)))

microbenchmark::microbenchmark(
  index(x),
  index2(x),
  index3(x),
  index4(x),
  unit = "relative"
)

which shows

Unit: relative
      expr      min       lq     mean   median       uq      max neval
  index(x) 1.494575 1.553172 1.570421 1.651164 1.449423 3.687941   100
 index2(x) 1.355329 1.403027 1.427653 1.527255 1.315000 3.627557   100
 index3(x) 1.185497 1.186414 1.189662 1.236235 1.167037 1.174816   100
 index4(x) 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000   100

One way is using data.table and fmatch, where data.table could use multiple cores.

library(data.table)
library(fastmatch)
setDTthreads(1)
fmatch(x, data.table(x)[,logical(1), keyby = x]$x)

another using kit::funique and fmatch optionally with kit::psort, where you can use multiple cores.

library(kit)
library(fastmatch)
fmatch(x, sort(kit::funique(x)))
fmatch(x, kit::psort(kit::funique(x), nThread=1L))

or using collapse::funique.

library(collapse)
library(fastmatch)
fmatch(x, sort(collapse::funique(x)))
fmatch(x, collapse::funique(x, sort=TRUE))

Benchmark based on @thomasiscoding:

library(fastmatch)
library(data.table)
setDTthreads(1)

set.seed(42)
x <- sample(letters, 1e6, replace = T)
index <- function(x) as.numeric(as.factor(x))
index2 <- function(x) match(x, sort(unique(x)))
index3 <- function(x) chmatch(x, sort(unique(x)))
index4 <- function(x) fmatch(x, sort(unique(x)))
index5 <- function(x) fmatch(x, data.table(x)[,logical(1), keyby = x]$x)
index6 <- function(x) fmatch(x, sort(collapse::funique(x)))
index7 <- function(x) fmatch(x, collapse::funique(x, TRUE))
index8 <- function(x) fmatch(x, sort(kit::funique(x)))
index9 <- function(x) fmatch(x, kit::psort(kit::funique(x), nThread=1L))

bench::mark(index(x), index2(x), index3(x), index4(x), index5(x), index6(x),
  index7(x), index8(x), index9(x))
#  expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time
#  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm>
#1 index(x)    29.95ms  30.11ms      32.1   30.89MB    88.4      4    11      124ms
#2 index2(x)    28.6ms  28.67ms      34.8   23.26MB    39.1      8     9      230ms
#3 index3(x)   20.46ms  21.13ms      46.1   15.63MB    20.2     16     7      347ms
#4 index4(x)   18.71ms  18.88ms      52.1   15.63MB    23.2     18     8      345ms
#5 index5(x)   20.42ms  20.81ms      46.7   15.46MB    16.5     17     6      364ms
#6 index6(x)   10.22ms  10.33ms      96.1    3.82MB    11.2     43     5      448ms
#7 index7(x)   10.11ms  10.26ms      95.1    3.82MB     6.34    45     3      473ms
#8 index8(x)    8.69ms   9.28ms     107.     3.81MB     6.41    50     3      468ms
#9 index9(x)    8.62ms   9.22ms     108.     3.81MB     6.36    51     3      472ms

Marginally faster

index2 <- function(x) match(x, unique(x))

microbenchmark::microbenchmark(
    index((x)), index2(x))
#Unit: milliseconds
#       expr    min      lq     mean  median      uq    max neval
# index((x)) 2.8291 3.01675 3.198094 3.18080 3.30925 3.9038   100
#  index2(x) 2.6704 2.78735 3.035446 3.01125 3.11835 7.4401   100

Note this will not sort the indices by letters but still create unique indices.

An improvement of @Maurits's answer:

library(data.table)
index3 <- function(x) chmatch(x, sort(unique(x)))
all.equal(index(x), index3(x))
# [1] TRUE

microbenchmark::microbenchmark(
  index((x)), index2(x), index3(x))
#Unit: milliseconds
#       expr      min       lq     mean   median       uq       max neval
# index((x)) 2.952510 4.148041 4.648938 4.373836 4.615765 10.690968   100
#  index2(x) 2.664619 3.542237 3.923278 3.760758 3.940802 10.724467   100
#  index3(x) 2.090956 2.726665 2.959576 2.870833 2.993246  7.997333   100

For long vectors with few unique values the sort call shouldn't matter but you could drop it if you don't need it.

Related