Manera eficiente de filtrar un dataframe por rangos en otro

Digamos que tengo un dataframe que contiene un montón de datos y una columna de fecha / hora que indica cuándo se recopiló cada punto de datos. Tengo otro dataframe que enumera los intervalos de tiempo, donde una columna “Inicio” indica la fecha / hora en que comienza cada tramo y una columna “Finalizar” que indica la fecha / hora en que termina cada tramo.

He creado un ejemplo ficticio a continuación utilizando datos simplificados:

main_data = data.frame(Day=c(1:30)) spans_to_filter = data.frame(Span_number = c(1:6), Start = c(2,7,1,15,12,23), End = c(5,10,4,18,15,26)) 

Jugué con algunas maneras de resolver este problema y terminé con la siguiente solución:

 require(dplyr) filtered.main_data = main_data %>% rowwise() %>% mutate(present = any(Day >= spans_to_filter$Start & Day % filter(present) %>% data.frame() 

Esto funciona perfectamente bien, pero noté que puede llevar un tiempo procesarlo si tengo una gran cantidad de datos (supongo que porque estoy realizando una comparación de filas). Todavía estoy aprendiendo los pormenores de R y me preguntaba si existe una forma más eficiente de realizar esta operación, preferiblemente usando dplyr / tidyr.

Aquí hay una función que puede ejecutar en dplyr para buscar fechas dentro de un rango determinado usando la función between (de dplyr ). Para cada valor de Day , mapply ejecuta between cada uno de los pares de fechas de Start y rowSums y la función usa rowSums para devolver TRUE si Day encuentra entre al menos uno de ellos. No estoy seguro de si es el enfoque más eficiente, pero da como resultado casi un factor de cuatro mejoras en la velocidad.

 test.overlap = function(vals) { rowSums(mapply(function(a,b) between(vals, a, b), spans_to_filter$Start, spans_to_filter$End)) > 0 } main_data %>% filter(test.overlap(Day)) 

Si está trabajando con fechas (en lugar de fechas), puede ser incluso más eficiente crear un vector de fechas específicas y probar la membresía (este podría ser un mejor enfoque incluso con los horarios):

 filt.vals = as.vector(apply(spans_to_filter, 1, function(a) a["Start"]:a["End"])) main_data %>% filter(Day %in% filt.vals) 

Ahora compare las velocidades de ejecución. Acorté tu código para requerir solo la operación de filtrado:

 library(microbenchmark) microbenchmark( OP=main_data %>% rowwise() %>% filter(any(Day >= spans_to_filter$Start & Day <= spans_to_filter$End)), eipi10 = main_data %>% filter(test.overlap(Day)), eipi10_2 = main_data %>% filter(Day %in% filt.vals) ) Unit: microseconds expr min lq mean median uq max neval cld OP 2496.019 2618.994 2875.0402 2701.8810 2954.774 4741.481 100 c eipi10 658.941 686.933 782.8840 714.4440 770.679 2474.941 100 b eipi10_2 579.338 601.355 655.1451 619.2595 672.535 1032.145 100 a 

ACTUALIZACIÓN: A continuación se muestra una prueba con un dataframe mucho más grande y algunos rangos de fechas adicionales para que coincida (gracias a @Frank por sugerir esto en su comentario ahora eliminado). Resulta que las ganancias de velocidad son mucho mayores en este caso (aproximadamente un factor de 200 para el método mapply/between , y mucho mayor para el segundo método).

 main_data = data.frame(Day=c(1:100000)) spans_to_filter = data.frame(Span_number = c(1:9), Start = c(2,7,1,15,12,23,90,9000,50000), End = c(5,10,4,18,15,26,100,9100,50100)) microbenchmark( OP=main_data %>% rowwise() %>% filter(any(Day >= spans_to_filter$Start & Day <= spans_to_filter$End)), eipi10 = main_data %>% filter(test.overlap(Day)), eipi10_2 = { filt.vals = unlist(apply(spans_to_filter, 1, function(a) a["Start"]:a["End"])) main_data %>% filter(Day %in% filt.vals)}, times=10 ) Unit: milliseconds expr min lq mean median uq max neval cld OP 5130.903866 5137.847177 5201.989501 5216.840039 5246.961077 5276.856648 10 b eipi10 24.209111 25.434856 29.526571 26.455813 32.051920 48.277326 10 a eipi10_2 2.505509 2.618668 4.037414 2.892234 6.222845 8.266612 10 a 

En el paquete data.table a partir de v1.9.8, se ha implementado no equi join. Con esto, he creado una función de contenedor inrange() para exactamente este tipo de operaciones, donde la tarea implica encontrar si un punto se encuentra en cualquiera de los intervalos provistos, y si es así, devolver TRUE , sino FALSE .

 require(data.table) # v>=1.9.8 setDT(main_data)[Day %inrange% spans_to_filter[, 2:3]] # inclusive bounds # Day # 1: 1 # 2: 2 # 3: 3 # 4: 4 # 5: 5 # 6: 7 # 7: 8 # 8: 9 # 9: 10 # 10: 12 # 11: 13 # 12: 14 # 13: 15 # 14: 16 # 15: 17 # 16: 18 # 17: 23 # 18: 24 # 19: 25 # 20: 26 

Ver ?inrange por más.

Usando Base R:

 main_data[unlist(lapply(main_data$Day, function(x) any(x >= spans_to_filter$Start & x <= spans_to_filter$End))),]