¿Identificando intervalos de tiempo superpuestos con dos criterios más en R?

10

Tengo que verificar las observaciones de aves realizadas durante un período más largo para entradas duplicadas / superpuestas.

Los observadores de diferentes puntos (A, B, C) hicieron observaciones y las marcaron en mapas de papel. Esas líneas se incorporaron a una característica de línea con datos adicionales para la especie, el punto de observación y los intervalos de tiempo en que se vieron.

Normalmente, los observadores se comunican entre sí por teléfono mientras observan, pero a veces se olvidan, así que obtengo esas líneas duplicadas.

Ya reduje los datos a esas líneas que tocan el círculo, por lo que no tengo que hacer un análisis espacial, solo comparo los intervalos de tiempo para cada especie y puedo estar bastante seguro de que es la misma persona que se encuentra en la comparación .

Ahora estoy buscando una forma en R para identificar aquellas entradas que:

  • se hacen el mismo día con un intervalo superpuesto
  • y donde es la misma especie
  • y que se hicieron desde diferentes puntos de observación (A o B o C o ...))

ingrese la descripción de la imagen aquí

En este ejemplo, encontré manualmente entradas posiblemente duplicadas de la misma persona. El punto de observación es diferente (A <-> B), la especie es la misma (Sst) y el intervalo de los tiempos de inicio y finalización se superpone.

ingrese la descripción de la imagen aquí

Ahora crearía un nuevo campo "duplicado" en mi data.frame, dando a ambas filas una identificación común para poder exportarlas y luego decidir qué hacer.

Busqué muchas soluciones disponibles, pero no encontré ninguna sobre el hecho de que tengo que subconjuntar el proceso para la especie (preferiblemente sin bucle) y comparar las filas para 2 + x puntos de observación.

Algunos datos para jugar:

testdata <- structure(list(bird_id = c("20150712_0810_1410_A_1", "20150712_0810_1410_A_2", 
"20150712_0810_1410_A_4", "20150712_0810_1410_A_7", "20150727_1115_1430_C_1", 
"20150727_1120_1430_B_1", "20150727_1120_1430_B_2", "20150727_1120_1430_B_3", 
"20150727_1120_1430_B_4", "20150727_1120_1430_B_5", "20150727_1130_1430_A_2", 
"20150727_1130_1430_A_4", "20150727_1130_1430_A_5", "20150812_0900_1225_B_3", 
"20150812_0900_1225_B_6", "20150812_0900_1225_B_7", "20150812_0907_1208_A_2", 
"20150812_0907_1208_A_3", "20150812_0907_1208_A_5", "20150812_0907_1208_A_6"
), obsPoint = c("A", "A", "A", "A", "C", "B", "B", "B", "B", 
"B", "A", "A", "A", "B", "B", "B", "A", "A", "A", "A"), species = structure(c(11L, 
11L, 11L, 11L, 10L, 11L, 10L, 11L, 11L, 11L, 11L, 10L, 11L, 11L, 
11L, 11L, 11L, 11L, 11L, 11L), .Label = c("Bf", "Fia", "Grr", 
"Kch", "Ko", "Lm", "Rm", "Row", "Sea", "Sst", "Wsb"), class = "factor"), 
    from = structure(c(1436687150, 1436689710, 1436691420, 1436694850, 
    1437992160, 1437991500, 1437995580, 1437992360, 1437995960, 
    1437998360, 1437992100, 1437994000, 1437995340, 1439366410, 
    1439369600, 1439374980, 1439367240, 1439367540, 1439369760, 
    1439370720), class = c("POSIXct", "POSIXt"), tzone = ""), 
    to = structure(c(1436687690, 1436690230, 1436691690, 1436694970, 
    1437992320, 1437992200, 1437995600, 1437992400, 1437996070, 
    1437998750, 1437992230, 1437994220, 1437996780, 1439366570, 
    1439370070, 1439375070, 1439367410, 1439367820, 1439369930, 
    1439370830), class = c("POSIXct", "POSIXt"), tzone = "")), .Names = c("bird_id", 
"obsPoint", "species", "from", "to"), row.names = c("20150712_0810_1410_A_1", 
"20150712_0810_1410_A_2", "20150712_0810_1410_A_4", "20150712_0810_1410_A_7", 
"20150727_1115_1430_C_1", "20150727_1120_1430_B_1", "20150727_1120_1430_B_2", 
"20150727_1120_1430_B_3", "20150727_1120_1430_B_4", "20150727_1120_1430_B_5", 
"20150727_1130_1430_A_2", "20150727_1130_1430_A_4", "20150727_1130_1430_A_5", 
"20150812_0900_1225_B_3", "20150812_0900_1225_B_6", "20150812_0900_1225_B_7", 
"20150812_0907_1208_A_2", "20150812_0907_1208_A_3", "20150812_0907_1208_A_5", 
"20150812_0907_1208_A_6"), class = "data.frame")

Encontré una solución parcial con la función data.table foverlaps mencionada, por ejemplo, aquí https://stackoverflow.com/q/25815032

library(data.table)
#Subsetting the data for each observation point and converting them into data.tables
A <- setDT(testdata[testdata$obsPoint=="A",])
B <- setDT(testdata[testdata$obsPoint=="B",])
C <- setDT(testdata[testdata$obsPoint=="C",])

#Set a key for these subsets (whatever key exactly means. Don't care as long as it works ;) )
setkey(A,species,from,to)    
setkey(B,species,from,to)
setkey(C,species,from,to)

#Generate the match results for each obsPoint/species combination with an overlapping interval
matchesAB <- foverlaps(A,B,type="within",nomatch=0L) #nomatch=0L -> remove NA
matchesAC <- foverlaps(A,C,type="within",nomatch=0L) 
matchesBC <- foverlaps(B,C,type="within",nomatch=0L)

Por supuesto, esto de alguna manera "funciona", pero en realidad no es lo que me gusta lograr al final.

Primero, tengo que codificar los puntos de observación. Preferiría encontrar una solución con un número arbitrario de puntos.

En segundo lugar, el resultado no está en un formato con el que realmente pueda continuar trabajando fácilmente. Las filas coincidentes se colocan en la misma fila, mientras que mi objetivo es que las filas se coloquen debajo, y en una nueva columna, tendrían un identificador común.

Tercero, tengo que verificar manualmente nuevamente, si un intervalo se superpone desde los tres puntos (que no es el caso con mis datos, pero generalmente podría)

Al final, me gustaría recibir un nuevo data.frame con todos los candidatos identificables por una identificación de grupo que pueda unir de nuevo a las líneas y exportar el resultado como una capa para un examen más detallado.

¿Alguien más ideas de cómo hacer esto?

Bernd V.
fuente
No estoy seguro de entender completamente, pero parece una tarea bastante sencilla en PostgreSQL. Hay funciones para rangos de tiempo. Como he entendido, debería ser fácil compartir datos entre PostgreSQL y R.
Nicklas Avén
Tengo que admitir que no tengo conocimiento cero de Postgres, pero en realidad, cuando tomé una cerveza esta noche, también tuve la idea de que algunas cosas sql podrían estar disponibles para esto. Para el resto de mis operaciones que tengo que ver con el conjunto de datos, R es LA herramienta, pero sé que las funciones sql también se pueden realizar dentro de R a través de algunos paquetes. Investigando ....
Bernd V.
¿Qué tan grande es el conjunto de datos: cientos, miles, millones de filas? Para las funciones de SQL, ¿encontró sqldf ?
Simbamangu
Mientras tanto, encontré una solución de trabajo. Es una pena que no lo haya publicado hasta ahora. Tendré que hacerlo más general para que sea de utilidad para otros, y luego lo publicaré lo antes posible.
Bernd V.
¡Lo hará +1 si está todo vectorizado y no usa forbucles!
Simbamangu

Respuestas:

1

Como algunos comentaristas aludieron, SQL es una buena opción para expresar conjuntos de restricciones bastante complicados. El paquete sqldf facilita el uso de la potencia de SQL en R sin necesidad de configurar usted mismo una base de datos relacional.

Aquí hay una solución usando SQL. Antes de ejecutar, tuve que cambiar el nombre de las columnas de intervalo de datos a startTimey endTimeporque el nombre fromestá reservado en SQL.

library(reshape2)
library(sqldf)

dupes_wide <- sqldf("SELECT hex(randomblob(16)) dupe_id, x.bird_id x_bird_id, y.bird_id y_bird_id
                     FROM testdata x JOIN testdata y
                          ON (x.startTime <= y.endTime)
                         AND (x.endTime >= y.startTime)
                         AND (x.species = y.species)
                         AND (x.obsPoint < y.obsPoint)")
dupes_long <- melt(dupes_wide, id.vars='dupe_id', value.name='bird_id')
merge(testdata, dupes_long[, c('dupe_id', 'bird_id')], by='bird_id', all.x=TRUE)

Para facilitar la comprensión, la respuesta SQL dupes_widetermina luciendo así:

                         dupe_id x_bird_id y_bird_id
253FCC7A58FD8401960FC5D95153356C 20150727_1130_1430_A_2 20150727_1120_1430_B_1
9C1C1A13306ECC2DF78004D421F70CE6 20150727_1130_1430_A_5 20150727_1120_1430_B_4
1E8316DBF631BBF6D2CCBD15A85E6EF3 20150812_0907_1208_A_5 20150812_0900_1225_B_6

Autounión FROM testdata x JOIN testdata y : encontrar pares de filas de un único conjunto de datos es una autounión. Necesitamos comparar cada fila con todas las demás. La ONexpresión enumera las restricciones para mantener pares.

Intervalo de superposición : estoy bastante seguro de que la definición de superposición que utilicé en este SQL ( fuente ) difiere de lo que foverlapsestaba haciendo por usted. Usó el tipo "dentro", que requiere que la observación anterior obsPointesté completamente dentro de la observación posterior obsPoint(pero pierde lo contrario, por ejemplo, si la observación de C está completamente dentro de B ). Afortunadamente, es fácil en SQL si necesita codificar una definición diferente de superposición.

Diferentes puntos : su restricción de que se hicieron duplicados a partir de diferentes puntos de observación realmente se expresaría (x.obsPoint <> y.obsPoint). Si hubiera escrito eso, SQL devolvería cada par duplicado dos veces, solo con el orden cambiado de los pájaros en cada fila. En cambio, utilicé una <para mantener solo la mitad única de las filas. (Esta no es la única forma de hacer esto)

ID duplicado único : al igual que con su solución anterior, el propio SQL enumera los duplicados en la misma fila. hex(randomblob(16))es una forma hacky ( pero recomendada ) en SQLite para generar ID únicos para cada par.

Formato de salida : no le gustaron los duplicados en la misma fila, por lo que los meltdivide y mergeasigna los ID duplicados a su marco de datos inicial.

Limitaciones : Mi solución no maneja el caso donde el mismo pájaro es capturado en más de dos pistas . Es más complicado y algo mal definido. Por ejemplo, si sus rangos de tiempo se ven así

    | - Pájaro1 - |
             | - Pájaro2 - |
                      | - Pájaro3 - |

entonces Bird1 es un duplicado de Bird2 , que es un duplicado de Bird3 , pero ¿son duplicados Bird1 y Bird3 ?

Jeff G
fuente