Mon objectif était de définir des plages (ou intervalles de temps) uniques sans chevauchement par ID, lorsque chaque ID comportait plusieurs plages d'exposition potentiellement superposées. J'ai trouvé que la fonction "aplatir" du package R "IntervalSurgeon" peut implémenter la tâche. Ma question est: comment implémenter efficacement la même tâche et obtenir le même résultat "tab_out" de manière "data.table"?
library(data.table) library(IntervalSurgeon) set.seed(2019) N <- 3 # number of IDs IDs <- paste0("ID", 1:N) # unique IDs K <- 4 # number of exposures per ID DT <- data.table(IDs = rep(IDs, each = K), starts = sample(1:20, N * K, replace = T))[, ends := starts + sample(1:5, N * K, replace = T)] DT <- DT[order(IDs, starts),] tab_out <- DT[, as.list(data.table( flatten(as.matrix(cbind(starts, ends))))), by = IDs] DT IDs starts ends 1: ID1 7 11 2: ID1 13 17 3: ID1 15 16 4: ID1 16 18 5: ID2 1 5 6: ID2 1 4 7: ID2 2 3 8: ID2 17 19 9: ID3 3 6 10: ID3 13 16 11: ID3 14 15 12: ID3 16 21 tab_out IDs V1 V2 1: ID1 7 11 2: ID1 13 18 3: ID2 1 5 4: ID2 17 19 5: ID3 3 6 6: ID3 13 21
3 Réponses :
Voici une solution utilisant le intervals
-package..
exemple de données
myfun <- function( y ) { data.table::as.data.table( intervals::interval_union( intervals::Intervals( as.matrix( y ) ), check_valid = TRUE ) ) } DT[, myfun( .SD ), by = .(IDs)] # IDs V1 V2 # 1: ID1 7 11 # 2: ID1 13 18 # 3: ID2 1 5 # 4: ID2 17 19 # 5: ID3 3 6 # 6: ID3 13 21
code
library( data.table ) library( intervals ) DT <- fread(" IDs starts ends ID1 7 11 ID1 13 17 ID1 15 16 ID1 16 18 ID2 1 5 ID2 1 4 ID2 2 3 ID2 17 19 ID3 3 6 ID3 13 16 ID3 14 15 ID3 16 21")
Cela semble inefficace. Je m'attendrais à ce qu'il copie en profondeur les données.
@Roland Je suis ouvert à toute suggestion ;-)
Empruntant l'idée de la solution de David Aurenburg ici a > sortie: g IDs V1 V2
1: 0 ID1 7 11
2: 1 ID1 13 18
3: 0 ID2 1 5
4: 1 ID2 17 19
5: 0 ID3 3 6
6: 1 ID3 13 21
DT[, g := c(0L, cumsum(shift(starts, -1L) > cummax(ends))[-.N]), IDs][,
.(min(starts), max(ends)), .(g, IDs)]
Voici les temps de calcul des approches "IntervalSurgeon", "intervalles" et "data.table". Les heures correspondent à l'ensemble de données contenant un million d'identifiants, avec 10 expositions par identifiant, soit 10 millions de lignes. J'ai fait une seule exécution en raison de l'approche «intervalles» qui prenait trop de temps.
La machine était un MacBook Pro (15 pouces, 2018), avec un processeur Intel Core i9 à 2,9 GHz, une mémoire DDR4 de 32 Go à 2400 MHz, fonctionnant sur MacOC Mojave v. 10.14.6, max 12 threads.
L'approche "data.table" était clairement gagnante, la même que l'approche "intervalles" était clairement perdante:
TEMPS DE CALCUL
sessionInfo() R version 3.5.2 (2018-12-20) Platform: x86_64-apple-darwin15.6.0 (64-bit) Running under: macOS Mojave 10.14.6 Matrix products: default BLAS: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRblas.0.dylib LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib locale: [1] en_AU.UTF-8/en_AU.UTF-8/en_AU.UTF-8/C/en_AU.UTF-8/en_AU.UTF-8 attached base packages: [1] parallel stats graphics grDevices utils datasets methods base other attached packages: [1] intervals_0.15.1 IntervalSurgeon_1.0 fst_0.9.0 data.table_1.12.2 loaded via a namespace (and not attached): [1] compiler_3.5.2 Rcpp_1.0.1
Il est intéressant de noter qu'aucune des approches n'a bénéficié de la définition d'un plus grand nombre de threads data.table via setDTthreads (). Le fractionnement du jeu de données en 100 parties égales et le passage à l'approche "data.table" via parallel :: mclapply (mc.cores = 10) amène le temps de calcul en dessous de 5 secondes (le code est également fourni ci-dessous).
L'approche combinée "data.table + parallel :: mclapply" a également fonctionné avec un ensemble de données beaucoup plus grand de 50 millions d'ID (généré de la même manière que l'ensemble de données d'ID 1M mais avec MM = 50). Cet ensemble de données comprend 500 millions de lignes, divisées en 1000 sous-ensembles pour mclapply. J'ai mis mc.cores = 4 pour ne pas épuiser la RAM. Le temps de calcul d'une seule exécution était de 451,485 s, ce qui n'est pas un mauvais résultat pour un ordinateur portable! Ce serait formidable que ce type d'analyse soit un jour incorporé dans le package data.table, avec d'autres types d'analyses d'intervalle similaires comme dans le IntervalSurgeon package.
GÉNÉRATION DU JEU DE DONNÉES
library(data.table) library(IntervalSurgeon) library(intervals) library(fst) library(parallel) rm(list = ls()) gc() mc_cores <- 10 threads_no <- 2L setDTthreads(threads_no) print(getDTthreads()) DT <- read_fst("fst_DT_1Mx3.fst", as.data.table = T) # split the dataset into G equal parts ##################### G <- 100 nn <- nrow(DT) step_c <- nn/G # collect the indexes in the list list_inx <- vector('list', G) ii_low <- 1 ii_up <- step_c for (i2 in 1:G) { list_inx[[i2]] <- ii_low:ii_up ii_low <- ii_up+1 ii_up <- step_c*(i2+1) } ##################### ptm3 <- proc.time() # approach implementation ##################### list_out <- mclapply(1:G, function(i) { DT_tmp <- DT[list_inx[[i]],] return(DT_tmp[, g := c(0L, cumsum(shift(starts, -1L) > cummax(ends))[-.N]), IDs][, .(min(starts), max(ends)), .(g, IDs)][,g := NULL]) }, mc.cores = mc_cores ) ##################### exec_time3 <- proc.time() - ptm3 print("data.table + parallel::mclapply:") print(exec_time3)