How optimizar this code in R (split string to columns)

Viewed 67

I have this R code that works:

df <- data.frame(C1=c('[AU 1] string 1; [AU 2] string 2; [AU 3] string 3.1; string 3.2; [AU 4] string 
4.1; string 4.2; [AU 5] string 5','[AU 1; AU 2] string 1','[AU 1] string 1; [AU 2] string 2'), 
UT=c("A1","A2","A3"))

s <-strsplit(df$C1, split = "(; (?=\\[))",perl=TRUE)

df1<-data.frame(
UT= rep(df$UT, sapply(s, length)),
AU=gsub('] .*', ']', unlist(s)),
C1=gsub('.*\\]', '', unlist(s)))

s1 <-strsplit(df1$C1, split = "; ",perl=TRUE)

df2<-data.frame(
UT= rep(df1$UT, sapply(s1, length)),
AU= rep(df1$AU, sapply(s1, length)),
C1=unlist(s1))

How to make this code more efficient, any ideas?

obs: The base has more than 1 million rows.

3 Answers

We could use separate_rows to split the 'C1' at the ; followed by zero or more spaces (\\s*) followed by the opening square bracket ([) (as there are ; within the [] and we want to avoid splitting that part). Then on the expanded data, it may be better to use str_extract to get the substring of characters from the opening square bracket till the closing ] in 'AU' and then extract all substring that starts with 'string' followed by any space and some digits with .. As str_extract_all returns a list, we do an unnest on the list column to convert it to a normal column

library(dplyr)
library(tidyr)
library(stringr)
df %>% 
   separate_rows(C1, sep=";\\s*(?=\\[)") %>%
   mutate(AU = str_extract(C1, "\\[[^]]+\\]"), 
        C1 = str_extract_all(C1, 'string\\s*[0-9.]+')) %>% 
   unnest(c(C1)) %>%
   select(UT, AU, C1)

-output

# A tibble: 10 x 3
#   UT    AU           C1        
#   <chr> <chr>        <chr>     
# 1 A1    [AU 1]       string 1  
# 2 A1    [AU 2]       string 2  
# 3 A1    [AU 3]       string 3.1
# 4 A1    [AU 3]       string 3.2
# 5 A1    [AU 4]       string 4.1
# 6 A1    [AU 4]       string 4.2
# 7 A1    [AU 5]       string 5  
# 8 A2    [AU 1; AU 2] string 1  
# 9 A3    [AU 1]       string 1  
#10 A3    [AU 2]       string 2  

Here is a base R solution. It is a simplification of the question's code and it is twice as fast. This increased speed comes from avoiding the creation of a data.frame, df1, followed by extraction operations, which in the case of data.frames are always slow.
In the 2nd strsplit, perl = TRUE was replaced by fixed = TRUE, a further speed-up thanks to akrun's comment.
The three solutions (OP's, this one and akrun's) are rewritten in the form of functions, with timings at the end.

cnauber <- function(X){
  s <- strsplit(X$C1, split = "(; (?=\\[))",perl=TRUE)
  
  df1 <- data.frame(
    UT= rep(X$UT, sapply(s, length)),
    AU=gsub('] .*', ']', unlist(s)),
    C1=gsub('.*\\]', '', unlist(s))
  )
  
  s1 <-strsplit(df1$C1, split = "; ",perl=TRUE)
  
  data.frame(
    UT= rep(df1$UT, sapply(s1, length)),
    AU= rep(df1$AU, sapply(s1, length)),
    C1=unlist(s1)
  )
}

rui <- function(X){
  s <- strsplit(X$C1, split = "(; (?=\\[))", perl = TRUE)
  ut <- rep(X$UT, lengths(s))
  au <- sub('] .*', ']', unlist(s))
  c1 <- sub('.*\\]', '', unlist(s))
  s1 <- strsplit(c1, split = "; ", fixed = TRUE)
  data.frame(
    UT = rep(ut, lengths(s1)),
    AU = rep(au, lengths(s1)),
    C1 = unlist(s1)
  )
}

library(dplyr)
library(tidyr)
library(stringr)

akrun <- function(X){
  X %>% 
    separate_rows(C1, sep=";\\s*(?=\\[)") %>%
    mutate(AU = str_extract(C1, "\\[[^]]+\\]"), 
           C1 = str_extract_all(C1, 'string\\s*[0-9.]+')) %>% 
    unnest(c(C1)) %>%
    select(UT, AU, C1)
}

The first benchmark is with the original 3 rows data.

mb <- microbenchmark(
  cnauber = cnauber(df),
  rui = rui(df),
  akrun = akrun(df)
)
print(mb, order = "median")
#Unit: microseconds
#    expr       min         lq      mean     median        uq       max neval cld
#     rui   522.003   793.2365  1019.390   832.8795   993.800  18186.66   100  a 
# cnauber  1062.187  1603.5655  1858.867  1643.8895  1816.374  20068.48   100  a 
#   akrun 19931.267 29093.6995 31774.619 29780.3750 31086.222 175779.06   100   b

The second benchmark is with a larger data set.

for(i in 1:10) df <- rbind(df, df)
mb <- microbenchmark(
  cnauber = cnauber(df),
  rui = rui(df),
  akrun = akrun(df)
)
print(mb, order = "median")
#Unit: milliseconds
#    expr       min        lq      mean    median        uq       max neval cld
#     rui  32.79471  45.00103  45.73348  45.80887  47.14947  70.10428   100 a  
# cnauber  57.42609  70.17960  75.93042  73.73240  80.90393 105.26087   100  b 
#   akrun 483.04227 528.21140 566.06477 558.65710 601.27015 785.15709   100   c

A data.table option

setDT(df)[
  ,
  strsplit(C1, ";\\s+(?=\\[)", perl = TRUE), UT
][
  ,
  setNames(tstrsplit(V1, "(?<=\\])\\s+", perl = TRUE), c("AU", "C1")), UT
][
  ,
  .(C1 = unlist(strsplit(C1, ";\\s+"))),
  .(UT, AU)
]

gives

    UT           AU         C1
 1: A1       [AU 1]   string 1
 2: A1       [AU 2]   string 2
 3: A1       [AU 3] string 3.1
 4: A1       [AU 3] string 3.2
 5: A1       [AU 4] string 4.1
 6: A1       [AU 4] string 4.2
 7: A1       [AU 5]   string 5
 8: A2 [AU 1; AU 2]   string 1
 9: A3       [AU 1]   string 1
10: A3       [AU 2]   string 2
Related