data.table: Выполнить эффективную построчную операцию на большом data.table со столбцами в качестве входных данных - PullRequest
2 голосов
/ 06 апреля 2020

У меня очень большая таблица данных с размером 1,6x10 ^ 8 строк, и я хочу выполнить построчную операцию между столбцами exposure и exposure.before.index, как показано в моем примере ниже.

Я создал столбец TI (т. Е. «Интенсивность лечения»), который показывает, является ли в настоящее время не идентификатором наркотик / наркотики, exposure, который отличается от любых лекарств, которые он принимал у каждого идентификатора. соответствующий первый ряд, exposure.before.index. Вы можете просмотреть мой код и заметить, что окончательный результат соответствует описанному.

library(data.table)
DT <- data.table::data.table(ID=c("a","a","a","b","b","c","c"),
                             drugA=c(1,1,1,0,0,0,0),
                             drugB=c(0,1,1,1,0,0,0),
                             drugC=c(0,0,1,0,1,0,0))
DT[, exposure := gsub("NA\\+|\\+NA", "", do.call(paste, 
                                                 c(Map(function(x, y) names(.SD)[(NA^!x) * y], .SD, 
                                                       seq_along(.SD)), sep="+"))), .SDcols = drugA:drugC]
DT[exposure=="NA",exposure:="NONE"]
DT[,exposure.before.index:=c("drugA","drugA","drugA","drugB","drugB","NONE","NONE")]
DT[,CNT:=1:.N]
DT[!(exposure.before.index!="NONE" & exposure=="NONE"),TI:=(any(!unlist(strsplit(exposure, "[+]"))%in%unlist(strsplit(exposure.before.index, "[+]")))),by="CNT"]
DT[is.na(TI),TI:=FALSE]
DT

   ID drugA drugB drugC          exposure exposure.before.index CNT    TI
1:  a     1     0     0             drugA                 drugA   1 FALSE
2:  a     1     1     0       drugA+drugB                 drugA   2  TRUE
3:  a     1     1     1 drugA+drugB+drugC                 drugA   3  TRUE
4:  b     0     1     0             drugB                 drugB   4 FALSE
5:  b     0     0     1             drugC                 drugB   5  TRUE
6:  c     0     0     0              NONE                  NONE   6 FALSE
7:  c     0     0     0              NONE                  NONE   7 FALSE

Я создал CNT, чтобы применить свою функцию any(!unlist(strsplit(exposure, "[+]"))%in%unlist(strsplit(exposure.before.index, "[+]"))) между exposure и exposure.before.index. Из-за 1.6x10 ^ 8 строк, которые у меня есть, этот метод занимает довольно много времени. Я обычно использую этот метод data.table [..., by = "CNT"], когда хочу применить определенную операцию / функцию по строкам, но я нахожу это не надежным для очень больших таблиц data.table. Существуют ли другие методы, которые есть у некоторых из вас, более надежные, чем мой метод?

Я нашел другие вопросы, похожие на мои топи c, но ответы не были обобщены для применения строки. разумная операция над пользовательской функцией надежным способом.

Любая помощь и / или советы приветствуются.

1 Ответ

1 голос
/ 07 апреля 2020

Это сложно. strsplit не будет эффективно использовать память для этого 100-миллионного набора данных - для каждой строки необходимо сделать два списка из strsplit. Я предлагаю использовать функцию и пропустить шаг by = 1:.N.

exposed = function(before, after) {
  out = vector(length = length(before))
  for (i in seq_along(before)) {
    bef = before[i]
    aft = after[i]
    if (bef == "NONE" || aft == "NONE") 
      out[i] = FALSE
    else
      out[i] = any(!unlist(strsplit(aft, "[+]", fixed = TRUE), use.names = FALSE)%chin%unlist(strsplit(bef, "[+]", fixed = TRUE), use.names = FALSE))
  }
  return(out)
}

DT[, TI3 := exposed(exposure.before.index, exposure)]

> DT[, .(exposure.before.index, exposure, TI, TI3)]
   exposure.before.index          exposure    TI   TI3
1:                 drugA             drugA FALSE FALSE
2:                 drugA       drugA+drugB  TRUE  TRUE
3:                 drugA drugA+drugB+drugC  TRUE  TRUE
4:                 drugB             drugB FALSE FALSE
5:                 drugB             drugC  TRUE  TRUE
6:                  NONE              NONE FALSE FALSE
7:                  NONE              NONE FALSE FALSE

Обратите внимание, что здесь есть несколько оптимизаций:

  1. Использование %chin% вместо %in% которая является утилитой , которая работает быстрее на символьных векторах, чем %in%
  2. Использование strsplit(..., fixed = TRUE) для оптимизации - это не регулярное выражение, которое мы используем. Вероятно, самое большое увеличение производительности.
  3. unlist(..., use.names = FALSE)

Следующим шагом будет превращение функции в Rcpp, чего здесь не делается. Строки сложнее, чем числа в Rcpp (по крайней мере, для меня).

Вот производительность этой функции. Для примера с 7 рядами это в 4 раза быстрее. Но по мере увеличения строк разница в скорости становится менее значительной:

## 7 rows
Unit: microseconds
   expr      min       lq     mean   median       uq       max
 use_fx  375.801  395.251  662.582  409.751  431.351 21345.701
     OP 1889.901 2021.601 2211.858 2096.101 2285.201  4042.801

## 700,000 rows
Unit: seconds
   expr       min        lq      mean    median        uq       max
 use_fx  4.409595  4.409595  4.409595  4.409595  4.409595  4.409595
     OP 12.592520 12.592520 12.592520 12.592520 12.592520 12.592520

## 7,000,000 rows
Unit: seconds
   expr       min        lq      mean    median        uq       max
 use_fx  43.90979  43.90979  43.90979  43.90979  43.90979  43.90979
     OP 130.16418 130.16418 130.16418 130.16418 130.16418 130.16418

## code used:
DT_big = DT[rep(seq_len(.N), 1e5)]
microbenchmark(
  use_fx = DT_big[, TI3 := exposed(exposure.before.index, exposure)],
  OP = {
    DT_big[,CNT:=1:.N]
    DT_big[!(exposure.before.index!="NONE" & exposure=="NONE"),TI:=(any(!unlist(strsplit(exposure, "[+]")) %in% unlist(strsplit(exposure.before.index, "[+]")))),by="CNT"]
    DT_big[is.na(TI),TI:=FALSE]
  }
  , times = 1L
)

Если вас интересует Rcpp, это может быть полезно:

https://wckdouglas.github.io/2015/05/string-manipulation

...