Identificando intervalos de tempo sobrepostos com mais dois critérios em R?

10

Eu tenho que verificar as observações das aves feitas durante um período mais longo para entradas duplicadas / sobrepostas.

Observadores de diferentes pontos (A, B, C) fizeram observações e as marcaram em mapas em papel. Essas linhas foram trazidas para uma linha com dados adicionais para as espécies, o ponto de observação e os intervalos de tempo em que foram vistos.

Normalmente, os observadores se comunicam por telefone enquanto observam, mas às vezes esquecem, então eu recebo essas linhas duplicadas.

Eu já reduzi os dados para as linhas que tocam o círculo, então não preciso fazer uma análise espacial, mas apenas comparar os intervalos de tempo para cada espécie e ter certeza de que é o mesmo indivíduo encontrado pela comparação .

Agora estou procurando uma maneira em R para identificar as entradas que:

  • são feitas no mesmo dia com um intervalo de sobreposição
  • e onde é a mesma espécie
  • e que foram feitas a partir de diferentes pontos de observação (A ou B ou C ou ...))

insira a descrição da imagem aqui

Neste exemplo, encontrei manualmente entradas possivelmente duplicadas do mesmo indivíduo. O ponto de observação é diferente (A <-> B), a espécie é a mesma (Sst) e o intervalo dos horários de início e término se sobrepõe.

insira a descrição da imagem aqui

Agora, eu criaria um novo campo "duplicado" no meu data.frame, fornecendo às duas linhas um ID comum para poder exportá-las e depois decidir o que fazer.

Procurei muitas soluções já disponíveis, mas não encontrei nenhuma sobre o fato de que eu tenho que sub-definir o processo para as espécies (de preferência sem loop) e tenho que comparar as linhas para 2 + x pontos de observação.

Alguns dados para brincar:

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")

Encontrei uma solução parcial com os foverlaps da função data.table mencionados, por exemplo, aqui 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)

Claro, isso de alguma forma "funciona", mas realmente não é o que eu gosto de alcançar no final.

Primeiro, eu tenho que codificar os pontos de observação. Eu preferiria encontrar uma solução com um número arbitrário de pontos.

Segundo, o resultado não está em um formato com o qual eu possa realmente continuar trabalhando com facilidade. As linhas correspondentes são na verdade colocadas na mesma linha, enquanto meu objetivo é que as linhas sejam colocadas por baixo e, em uma nova coluna, elas teriam um identificador comum.

Terceiro, tenho que verificar manualmente novamente, se um intervalo se sobrepuser aos três pontos (o que não é o caso dos meus dados, mas geralmente poderia)

No final, eu gostaria de receber um novo data.frame com todos os candidatos identificáveis ​​por um ID de grupo que eu possa associar de volta às linhas e exportar o resultado como uma camada para uma análise mais aprofundada.

Então, alguém mais ideias de como fazer isso?

Bernd V.
fonte
Não sei se entendi completamente, mas parece uma tarefa bastante simples no PostgreSQL. Existem funções para intervalos de tempo. Como eu entendi que deveria ser fácil de compartilhar dados entre PostgreSQL e R.
Nicklas Aven
Eu tenho que admitir que não tenho nenhum conhecimento sobre o Postgres, mas, na verdade, ao beber uma cerveja hoje à noite, também tive a ideia de que algumas coisas de sql podem estar disponíveis para isso. No restante das minhas operações, tenho a ver com o conjunto de dados, porém, R é a ferramenta, mas sei que as funções sql também podem ser executadas no R através de alguns pacotes. Investigando ....
Bernd V.
Qual é o tamanho do conjunto de dados - centenas, milhares, milhões de linhas? Para funções SQL, você encontrou o sqldf ?
Simbamangu 24/02
Enquanto isso, encontrei uma solução funcional. Que vergonha, eu não publiquei até agora. Terá que torná-lo mais geral para ser útil para outras pessoas, e depois eu o postarei o mais rápido possível.
Bernd V.
Será +1 se tudo estiver vetorizado e não usar forloops!
Simbamangu 25/02

Respostas:

1

Como alguns comentadores aludiram, o SQL é uma boa opção para expressar conjuntos de restrições bastante complicados. O pacote sqldf facilita o uso da energia do SQL no R sem a necessidade de você mesmo configurar um banco de dados relacional.

Aqui está uma solução usando SQL. Antes de executar, tive que renomear as colunas de intervalo dos seus dados para startTimee endTimeporque o nome fromestá reservado no 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 ajudar no entendimento, a resposta SQL dupes_wideacaba assim:

                         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

Junção automática FROM testdata x JOIN testdata y : a localização de pares de linhas em um único conjunto de dados é uma junção automática. Precisamos comparar todas as linhas com todas as outras. A ONexpressão lista as restrições para manter pares.

Intervalo de sobreposição : tenho certeza de que a definição de sobreposição que usei neste SQL ( fonte ) difere do que foverlapsestava fazendo por você. Você usou o tipo "dentro", que requer que a observação no início obsPointseja inteiramente dentro da observação no final obsPoint(mas perde o inverso, por exemplo, se a observação de C estiver inteiramente dentro de B ). Felizmente, é fácil no SQL se você precisar codificar uma definição diferente de sobreposição.

Pontos diferentes : sua restrição de que duplicatas foram feitas a partir de diferentes pontos de observação seria realmente expressa (x.obsPoint <> y.obsPoint). Se eu digitasse isso, o SQL retornaria todos os pares duplicados duas vezes, apenas com os pássaros trocados de ordem em cada linha. Em vez disso, usei a <para manter apenas a metade exclusiva das linhas. (Esta não é a única maneira de fazer isso)

ID duplicado exclusivo : como na solução anterior, o próprio SQL lista as duplicatas na mesma linha. hex(randomblob(16))é uma maneira hacky ( ainda recomendada ) no SQLite para gerar IDs exclusivos para cada par.

Formato de saída : você não gostou das duplicatas na mesma linha, portanto as meltdivide e mergeatribui os IDs duplicados ao seu quadro de dados inicial.

Limitações : Minha solução não lida com o caso em que o mesmo pássaro é capturado em mais de duas faixas . É mais complicado e um pouco mal definido. Por exemplo, se os intervalos de tempo parecerem

    | - Bird1 - |
             | - Bird2 - |
                      | - Bird3 - |

então Bird1 é uma duplicata de Bird2 , que é uma duplicata de Bird3 , mas são duplicatas de Bird1 e Bird3 ?

Jeff G
fonte