14 Juntando dados (Joins)
Acima: um exemplo animado de uma operação de left join (image source)
Essa página descreve maneiras de combinar diferentes data frames com operações do tipo “join”, “match”, “link” e “bind”.
Raramente nosso fluxo de análise epidemiológica não envolverá múltiplas fontes de dados e relações entre múltiplas bases. Talvez você precise conectar dados laboratoriais com desfechos clínicos dos pacientes, ou dados de mobilidade do Google às tendências de doenças infecciosas, ou ainda, uma base de dados em um estágio da análise com uma versão transormada dessa mesma base.
Nessa página vamos demonstrar como escrever código para:
- Conduzir joins de dois dataframes de forma que as linhas sejam combinadas com base nos valores comuns das colunas de identificadores
- Juntar dois dataframes baseados na combinação probabilística (verossímil) entre valores
- Expandir um dataframe através da ligação/anexação direta (binding ou “appending”) de linhas ou colunas de outro dataframe
14.1 Preparação
Carregue os pacotes R
O código abaixo realiza o carregamento dos pacotes necessários para a análise dos dados. Neste manual, enfatizamos o uso da função p_load()
, do pacman, que instala os pacotes, caso não estejam instalados, e os carrega no R para utilização. Também é possível carregar pacotes instalados utilizando a função library()
, do R base. Para mais informações sobre os pacotes do R, veja a página Introdução ao R.
::p_load(
pacman# importar and exportar
rio, # localizar arquivos
here, # gerenciamento e visualização de dados
tidyverse, # combinações probabilisticas
RecordLinkage, # combinações probabilisticas
fastLink )
Importe dados
Para iniciar, importaremos a versão limpa da linelist de casos de uma epidemia simulada de Ebola. Se você quiser acompanhar, clique para baixar a linelist “limpa” (com um arquivo .rds). Importe os dados com a função import()
do pacote rio (ela manipula vários tipos de arquivo, tais como .xlsx, .csv, .rds - veja na página Importando and exportando para detalhes).
# importa a linelist de casos
<- import("linelist_cleaned.rds") linelist
As primeiras 50 linhas da linelist são mostradas abaixo.
Base de dados de exemplo
Para demonstrar um join na seção abaixo, utilizaremos as seguintes bases de dados:
- Uma versão “miniatura” da
linelist
de casos, contendo apenas as colunascase_id
,date_onset
, ehospital
, e apenas as primeiras 10 linhas - Um dataframe separado chamado
hosp_info
, que contém mais detalhes sobre cada hospital
Na seção de combinação probabilística, utilizaremos outras duas bases diferentes. O código para criar essas bases serão mostradas naquela seção.
Linelist de casos “miniatura”
Abaixo está a miniatura da linelist de casos, que contém apenas 10 linhas e só as colunas case_id
, date_onset
, e hospital
.
<- linelist %>% # começa com a linelist original
linelist_mini select(case_id, date_onset, hospital) %>% # seleciona colunas
head(10) # seleciona apenas 10 primeira linhas
Dataframe com informações dos hospitais
Abaixo está o código para criar um dataframe separado com informações adicionais sobre sete hospitais (abrangência populacional e o nível de atenção disponível). Note que o nome “Military Hospital” pertence a dois hospitais diferentes - um no primeiro nível servindo 10000 residentes e outro no nível secundário servindo 50280 residentes.
# Cria o dataframe de informação dos hospitais
<- data.frame(
hosp_info hosp_name = c("central hospital", "military", "military", "port", "St. Mark's", "ignace", "sisters"),
catchment_pop = c(1950280, 40500, 10000, 50280, 12000, 5000, 4200),
level = c("Tertiary", "Secondary", "Primary", "Secondary", "Secondary", "Primary", "Primary")
)
Aqui está esse dataframe:
Pré-limpeza
Joins tradicionais (não-probabilísticos) são sensíveis ao caso (maiúsculos/minúsculos) e necessitam que os caracteres combinem de forma exata (exact match) entre os valores nos dois dataframes. Para demonstrar alguns dos passos de limpeza que você possa precisar executar antes de iniciar um join, vamos limpar e alinhar as bases linelist_mini
e hosp_info
agora.
Identifique as diferenças
Precisamos que os valores da coluna hosp_name
no dataframe hosp_info
combinem com os valores da coluna hospital
no dataframe linelist_mini
.
Aqui estão os valores do dataframe linelist_mini
, impressos com a função unique()
do R base:
unique(linelist_mini$hospital)
[1] "Other"
[2] "Ausente"
[3] "St. Mark's Maternity Hospital (SMMH)"
[4] "Port Hospital"
[5] "Military Hospital"
e aqui estão os valores do dataframe hosp_info
:
unique(hosp_info$hosp_name)
[1] "central hospital" "military" "port" "St. Mark's"
[5] "ignace" "sisters"
Você pode ver que apesar de alguns dos hospitais existirem em ambos dataframes, existem muitas diferenças na forma como os nomes estão escritos.
Alinhando valores
Vamos começar limpando os valores do dataframe hosp_info
. Como explicado na página Limpando dados e funções principais, podemos recodificar valores com critérios lógicos utilizando a função case_when()
do pacote dplyr. Para os quatro hospitais que existem em ambos os dataframes, vamos mudar os valores para alinhar com os valores da linelist_mini
. Para os outros hospitais, deixaremos os valores como estão (TRUE ~ hosp_name
).
CUIDADO: Tipicamente, quando estamos limpando dataframes devemos criar uma nova coluna (ex: hosp_name_clean
), mas para facilitar a demonstração vamos mostrar diretamente a modificação da antiga coluna
<- hosp_info %>%
hosp_info mutate(
hosp_name = case_when(
# critério # novo valor
== "military" ~ "Military Hospital",
hosp_name == "port" ~ "Port Hospital",
hosp_name == "St. Mark's" ~ "St. Mark's Maternity Hospital (SMMH)",
hosp_name == "central hospital" ~ "Central Hospital",
hosp_name TRUE ~ hosp_name
) )
Os nomes dos hospitais que aparecem em ambos os dataframes estão alinhados. Existem dois hospitais em hosp_info
que não estão presentes em linelist_mini
- vamos lidar com estes depois, no join.
unique(hosp_info$hosp_name)
[1] "Central Hospital"
[2] "Military Hospital"
[3] "Port Hospital"
[4] "St. Mark's Maternity Hospital (SMMH)"
[5] "ignace"
[6] "sisters"
Antes de fazer um join, geralmente o mais fácil é converter uma coluna completamente para letras minúsculas (lowercase) ou letras maiúsculas (uppercase). Se você precisar converter todos os valores de uma coluna para MAIÚSCULO ou minúsculo, utilize a função mutate()
e envolva a coluna com alguma dessas funções do pacote stringr, como mostrado na página Caracteres e strings.
str_to_upper()
str_to_upper()
str_to_title()
14.2 Joins com dplyr
O pacote dplyr oferece várias funções diferentes de join. dplyr está incluso no pacote tidyverse. Essas funções de join são descritas abaixo, com casos de uso simples.
Agradecimentos a https://github.com/gadenbuie pelos gifs informativos!
Sintaxe geral
Os comandos de join podem ser executados individualmente para juntar dois dataframes em um novo objeto, ou podem ser usados em uma cadeia de comandos com pipe (%>%
) para mesclar um dataframe em outro à medida que ele vai sendo limpo ou modificado de alguma outra forma.
No exemplo abaixo, a função left_join()
é utilizada como um comando individual para criar um novo dataframe chamado joined_data
. As entradas (inputs) são os dataframes 1 e 2 (df1
e df2
). O primeiro dataframe listado é o dataframe de base e o segundo é o dataframe que será mesclado a (joined to) ele.
O terceiro argumento by =
é onde você especifica as colunas em cada dataframe que serão utilizadas para alinhar as linhas dos dois dataframes. Se os nomes dessas colunas forem diferentes, insira o nome delas com um vetor c()
como mostrado abaixo, onde as linhas são combinadas com base nos valores comuns entre a coluna ID
em df1
e a coluna identifier
em df2
.
# Faz o join baseado nos valores comuns entre a coluna "ID" (primeiro dataframe) e coluna "identifier" (segundo dataframe)
<- left_join(df1, df2, by = c("ID" = "identifier")) joined_data
Se as colunas utilizadas no parâmetro by
tiverem exatamente o mesmo nome em ambos os dataframes, você pode inserir apenas esse nome, entre aspas.
# Join baseado nos valores comuns da coluna "ID" em ambos os dataframes
<- left_join(df1, df2, by = "ID") joined_data
Se você estiver juntando dataframes baseado em valores comuns ao longo de múltiplos campos, liste esses campos em um vetor c()
. Esse exemplo junta linhas se os valores em três colunas em cada base de dado se alinham de forma exata.
# join baseado nos mesmos primeiro nome, último nome e idade
<- left_join(df1, df2, by = c("name" = "firstname", "surname" = "lastname", "Age" = "age")) joined_data
Os comandos de join também podem ser rodados em uma cadeia de comandos com pipe. Isso irá modificar o dataframe que está sendo passado pelo pipe.
No exemplo abaixo, df1
está sendo pessado pelos pipes, df2
será juntado a ele e df1
então será modificado e redefinido.
<- df1 %>%
df1 filter(date_onset < as.Date("2020-03-05")) %>% # miscellaneous cleaning
left_join(df2, by = c("ID" = "identifier")) # join df2 to df1
CUIDADO: Joins são caso-específicos (maiúsculas e minúsculas)! Então, é útil converter todos os valores para minúsculo ou maiúsculo antes de fazer o join. Veja a página sobre caracteres/strings.
Left joins e right joins
Um left join ou right join é normalmente utilizado para adicionar informação a um dataframe - novas informações são adicionadas apenas a linhas que já existem no data frame base. Esses são tipos de joins comuns em trabalhos epidemiológicos pois eles são utilizados para adicionar informações de uma base de dado em outra.
Para utilizar esses joins, a ordem em que os dataframes estão escritos nos comandos é importante*.
- Em um left join, o primeiro dataframe escrito é o de base
- Em um right join, o segundo dataframe escrito é o de base
Todas as linhas do dataframe base são mantidas. As informações no outro dataframe (secundário) será juntada ao dataframe de base apenas se houver a combinação via as colunas dos identificadores. E ainda:
- Linhas no dataframe secundário que não combinam são removidas.
- Se houver muitas linhas no dataframe base que combinam com uma linha no secundário (muitos-para-um), a informação do secundário é adicionada a cada linha do base que combina.
- Se uma linha do base combina com múltiplas linhas no secundário (um-para-muitos), todas as combinações são dadas, o que significa que novas linhas podem ser adicionadas ao dataframe resultante!
Exemplos animados de left e right joins (fonte da imagem)
Exemplo
Abaixo está a saída de um left_join()
do hosp_info
(dataframe secundário, veja aqui) em linelist_mini
(dataframe base, veja aqui). O linelist_mini
tem nrow(linelist_mini)
linhas. O linelist_mini
modificado é mostrado. Note o seguinte:
- Duas novas colunas,
catchment_pop
elevel
foram adicionadas ao lado esquerdo dalinelist_mini
- Todas as linhas originais do dataframe base
linelist_mini
são mantidas - Quaisquer linhas originais do
linelist_mini
para “Military Hospital” foram duplicadas porque combinaram com duas linhas no dataframe secundário, então ambas as combinações são retornadas - A coluna de identificador do join na base secundária (
hosp_name
) desapareceu pois está em redundância com a coluna de identificador da base primária (hospital
)
- Quando a linha da base não combina com nenhuma linha da base secundária (ex: quando
hospital
é “Other” ou “Missing”), as colunas do dataframe secundário serão preenchidas comNA
(em branco) - Linhas no dataframe secundário sem nenhuma combinação no dataframe base (hospitais “sisters” e “ignace”) foram removidas
%>%
linelist_mini left_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in left_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
“Devo fazer um right join, ou um left join?”
Para responder à pergunta acima, pergunte-se “qual dataframe deve manter todas as suas linhas?” - utilize esse como o base. Um left join manterá todas as linhas do primeiro dataframe escrito no comando, enquanto um right join manterá todas as linhas do segundo dataframe.
Os dois comandos abaixo geram a mesma saída - um join de 10 linhas do hosp_info
em linelist_mini
(base), mas eles usam joins diferentes. O resultado é que a ordem das colunas vai diferir se hosp_info
aparecer na direita (em um left join) ou na esquerda (em um right join). A ordem das linhas também pode mudar de posição da mesma forma. No entanto, ambas essas consequências podem ser resolvidas depois utilizando select()
para reordenar as colunas, ou arrange()
para ordenar (sort) as linhas.
# Os dois comandos abaixo geram os memso dados, mas com linhas e colunas em ordem diferentes
left_join(linelist_mini, hosp_info, by = c("hospital" = "hosp_name"))
right_join(hosp_info, linelist_mini, by = c("hosp_name" = "hospital"))
Aqui está o resultado do join de hosp_info
em linelist_mini
por meio de um left join (novas colunas vindas da direita)
Warning in left_join(linelist_mini, hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Aqui, o resultado do join de hosp_info
em linelist_mini
por meio de um right join (novas colunas vindas da esquerda)
Warning in right_join(hosp_info, linelist_mini, by = c(hosp_name = "hospital")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 4 of `x` matches multiple rows in `y`.
ℹ Row 5 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Considere também se seu caso de uso está em meio a uma cadeia de comandos com pipe (%>%
). Se os dados passando através dos pipes for o dataframe base, você irá preferivelmente utilizar um left join para adicionar mais dados a ele.
Full join
Um full join é o mais inclusivo dos joins - ele retorna todas as linhas de ambos os dataframes.
Se houver alguma linha presente em um mas não em outro (onde não foi encontrada nenhuma combinação entre os dados), o dataframe vai incluir essa linha e tornar-se mais longo. Os valores NA
para representar valores ausentes serão utilizados para preencher os espaços vazios criados. À medida que você for executando o join, monitore cuidadosamente o número de colunas e linhas para resolver problemas de sensitividade de caso (maiúsculo/minúsculo) ou combinação exata de caracteres.
O dataframe base será aquele escrito primeiro no comando. Mudar essa posição não vai impactar em quais registros serão retornados pelo join, mas pode impactar na ordem das colunas e linhas resultantes e também em quais colunas de identificadores serão mantidos.
Exemplo animado de um full join (fonte da imagem)
Exemplo
Abaixo está uma saída de um full_join()
do hosp_info
(originalmente nrow(hosp_info)
, veja aqui) em linelist_mini
(originalmente nrow(linelist_mini)
, veja aqui). Note o seguinte:
- Todas as linhas do dataframe base são mantidas (
linelist_mini
)
- Linhas no dataframe secundário que não combinam com o dataframe base são mantidas (“ignace” e “sisters”), com valores nas colunas correspondentes do base (
case_id
eonset
) preenchidas com os valores ausentesNA
- Da mesma forma, linhas no dataframe base que não combinam com o secundário (“Other” e “Missing”) são mantidas, com as colunas
catchment_pop
elevel
do secundário preenchidas comNA
- Nos casos das combinações um-para-muitos ou muitos-para-um (ex: linhas para “Military Hospital”), todas as combinações possíveis são retornadas (aumentando o tamanho do dataframe final)
- Apenas a coluna do identificador do dataframe base foi mantida (
hospital
)
%>%
linelist_mini full_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in full_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Inner join
Um inner join é o mais restritivo dos joins - ele retona apenas as linhas que combinam em ambos os dataframes. Isso significa que o número linhas no dataframe base pode, de fato, reduzir. Ajustes em relação a qual dataframe será o base (escrito primeiro na função) não irá impactar quais linhas serão retornadas, mas vai impactar na ordem das colunas e linhas, e quais colunas de identificadores serão mantidas.
Exemplo animado de um inner join (fonte da imagem)
Exemplo
Abaixo está a saída de um inner_join()
de linelist_mini
(base) com hosp_info
(secundário). Note o seguinte:
- As linhas do dataframe base que não combinam com as do secundário são removidas (linhas onde
hospital
é “Missing” ou “Other”)
- De forma análoga, linhas do secundário que não combinam com o base são removidas (linhas onde
hosp_name
é “sisters” ou “ignace”)
- Apenas a coluna de identificador do base foi mantida (
hospital
)
%>%
linelist_mini inner_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in inner_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Semi join
Um semi join é “join filtro” que utiliza outra base de dados não para adicionar linhas ou colunas, mas para fazer uma filtragem.
Um semi-join mantém todas as observações do dataframe base que combinam com algum valor do dataframe secundário (mas não adiciona novas colunas nem duplica nenhuma linha para o caso de múltiplas combinações). Leia mais sobre esses joins “filtros” aqui.
Exemplo de um semi join animado (fonte da imagem)
Como um exemplo, o código abaixo retorna as linhas do dataframe hosp_info
que tem combinações em linelist_mini
baseados no nome do hospital.
%>%
hosp_info semi_join(linelist_mini, by = c("hosp_name" = "hospital"))
hosp_name catchment_pop level
1 Military Hospital 40500 Secondary
2 Military Hospital 10000 Primary
3 Port Hospital 50280 Secondary
4 St. Mark's Maternity Hospital (SMMH) 12000 Secondary
Anti join
O anti join é um outro tipo de “join filtro” que retorna linhas no dataframe base que não possuem uma combinação no dataframe secundário.
Leia mais sobre esses joins “filtros” aqui.
Cenários comuns para anti-join incluem identificar registros que não estão presentes em outro data frame, checar erros de grafia em um join (revisão de registros que deveriam ter combinado) e examinar registros que foram excluídos depois de outro join.
Assim como com right_join()
e left_join()
, o dataframe base (listado primeiro) é importante. As linhas retornadas serão apenas do dataframe base. Veja no gif abaixo qu e a linha no dataframe secundário (linha roxa 4) não é retornada mesmo que ela não tenha combinado com o base.
Exemplo animado de um anti join (fonte da imagem)
Um exemplo simples de anti_join()
Para um exemplo simples, vamos achar os hospitais de hosp_info
que não possuem nenhum caso presente em linelist_mini
. Nós listamos hosp_info
primeiro, como o dataframe base. Os hospitais que não estão presentes em linelist_mini
são retornados.
%>%
hosp_info anti_join(linelist_mini, by = c("hosp_name" = "hospital"))
Exemplo complexo de anti_join()
Para outro exemplo, vamos supor que rodamos um inner_join()
entre linelist_mini
e hosp_info
. Será retornado apenas um subconjunto dos registros originais de linelist_mini
, visto que alguns não estão presentes em hosp_info
.
%>%
linelist_mini inner_join(hosp_info, by = c("hospital" = "hosp_name"))
Warning in inner_join(., hosp_info, by = c(hospital = "hosp_name")): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 5 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
"many-to-many"` to silence this warning.
Para revisar os registros de linelist_mini
que foram excluídos no inner join, podemos rodar um anti-join com as mesmas configurações (linelist_mini
como o dataframe base).
%>%
linelist_mini anti_join(hosp_info, by = c("hospital" = "hosp_name"))
Para ver os registros de hosp_info
que foram excluídos no inner join, também podemos rodar um anti-join com hosp_info
como o dataframe base.
14.3 Combinação probabilística
Se você não tiver um único identificador comum entre as bases de dados para fazer o join, considere utilizar um algoritmo de combinação probabilística. Ele acharia combinações entre os registros baseados em similaridade (ex: distância de strings de Jaro–Winkler, ou distância numérica). Abaixo temos um exemplo simples utilizando o pacote fastLink .
Carregue o pacote
::p_load(
pacman# manipulação e visualização de dados
tidyverse, # combinação dos registros
fastLink )
Aque estão duas pequenas bases de dados que nós iremos utilizar para demonstrar a combinação probabilística (cases
e test_results
):
Aqui está o código utilizado para construir as bases de dados:
# cria as bases de dados
<- tribble(
cases ~gender, ~first, ~middle, ~last, ~yr, ~mon, ~day, ~district,
"M", "Amir", NA, "Khan", 1989, 11, 22, "River",
"M", "Anthony", "B.", "Smith", 1970, 09, 19, "River",
"F", "Marialisa", "Contreras", "Rodrigues", 1972, 04, 15, "River",
"F", "Elizabeth", "Casteel", "Chase", 1954, 03, 03, "City",
"M", "Jose", "Sanchez", "Lopez", 1996, 01, 06, "City",
"F", "Cassidy", "Jones", "Davis", 1980, 07, 20, "City",
"M", "Michael", "Murphy", "O'Calaghan", 1969, 04, 12, "Rural",
"M", "Oliver", "Laurent", "De Bordow", 1971, 02, 04, "River",
"F", "Blessing", NA, "Adebayo", 1955, 02, 14, "Rural"
)
<- tribble(
results ~gender, ~first, ~middle, ~last, ~yr, ~mon, ~day, ~district, ~result,
"M", "Amir", NA, "Khan", 1989, 11, 22, "River", "positive",
"M", "Tony", "B", "Smith", 1970, 09, 19, "River", "positive",
"F", "Maria", "Contreras", "Rodriguez", 1972, 04, 15, "Cty", "negative",
"F", "Betty", "Castel", "Chase", 1954, 03, 30, "City", "positive",
"F", "Andrea", NA, "Kumaraswamy", 2001, 01, 05, "Rural", "positive",
"F", "Caroline", NA, "Wang", 1988, 12, 11, "Rural", "negative",
"F", "Trang", NA, "Nguyen", 1981, 06, 10, "Rural", "positive",
"M", "Olivier", "Laurent", "De Bordeaux", NA, NA, NA, "River", "positive",
"M", "Mike", "Murphy", "O'Callaghan", 1969, 04, 12, "Rural", "negative",
"F", "Cassidy", "Jones", "Davis", 1980, 07, 02, "City", "positive",
"M", "Mohammad", NA, "Ali", 1942, 01, 17, "City", "negative",
NA, "Jose", "Sanchez", "Lopez", 1995, 01, 06, "City", "negative",
"M", "Abubakar", NA, "Abullahi", 1960, 01, 01, "River", "positive",
"F", "Maria", "Salinas", "Contreras", 1955, 03, 03, "River", "positive"
)
A base de dados cases
possui 9 registros de pacientes que estão esperando para testar resultados.
A base de dados test_results
possui 14 registros e contém a coluna result
, que nós queremos adicionar aos registros cases
baseados nas combinações probabilísticas dos registros.
Combinação Probabilística (probabilistic matching)
A função fastlink()
do pacote fastlink pode ser utilizada para aplicar um algoritmo que busca as combinações entre os valores. Aqui estão as informações básicas. Você pode ler mais detalhes digitando ?fastlink
no seu console.
- Defina os dois dataframes que serão comparados nos argumentos
dfA =
edfB =
- Em
varnames =
defina todos os nomes de colunas que serão utilizadas para testar a combinação. Todas devem existir tanto emdfA
quanto emdfB
.
- Em
stringdist.match =
defina o nome das colunas que estão emvarnames
que serão avaliadas pela “distancia” entre strings.
- Em
numeric.match =
defina o nome das colunas que estão emvarnames
que serão avaliadas pela distância numérica.
- Valores ausentes serão ignorados
- Por padrão, cada linha em qualquer dos dataframes será combinada com no máximo uma linha do outro dataframe. Se você quiser ver todas as combinações que foram avaliadas defina o parâmetro
dedupe.matches = FALSE
. A deduplicação é feita utilizando a solução de Winkler para atribuição linear.
Dica: divida uma coluna única de data em 3 colunas numéricas separadas utilizadando day()
, month()
, e year()
do pacote lubridate
O limiar padrão para as combinações é 0.94 (threshold.match =
) mas você pode ajustar para maior ou menor. Se você for ajustar manualmente o limiar, considere que com limiares mais altos você pode gerar mais falsos negativos (linhas que não combinam quando na verdade deveriam combinar) e, da mesma forma, um limiar mais baixo pode gerar falsos positivos.
Abaixo, os dados estão sendo combinados pela distância de strings nas colunas de nome e distrito, e na distância numérica para ano, mês e dia do nascimento. Um limiar para combinação de 95% de probabilidade foi configurado.
<- fastLink::fastLink(
fl_output dfA = cases,
dfB = results,
varnames = c("gender", "first", "middle", "last", "yr", "mon", "day", "district"),
stringdist.match = c("first", "middle", "last", "district"),
numeric.match = c("yr", "mon", "day"),
threshold.match = 0.95
)
====================
fastLink(): Fast Probabilistic Record Linkage
====================
If you set return.all to FALSE, you will not be able to calculate a confusion table as a summary statistic.
Calculating matches for each variable.
Getting counts for parameter estimation.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Running the EM algorithm.
Getting the indices of estimated matches.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Deduping the estimated matches.
Getting the match patterns for each estimated match.
Revisando as combinações
Nós definimos o objeto retornado da função fastLink()
como fl_output
. Ele é da classe list
, e de fato contém vários dataframes internamente que detalham os resultados da combinação. Um desses dataframes é matches
, que vai conter as combinações mais prováveis entre cases
e results
. Você pode acessar esse dataframe de combinações com fl_output$matches
. Abaixo, ele está sendo salvo como my_matches
para facilitar o acesso depois.
Quando my_matches
é impresso no console, você vê duas colunas de vetores: os pares de número de linhas/índices (também chamados “rownames”) em cases
(“inds.a”) e em results
(“inds.b”) representando as melhores combinações. Se o número da linha de um dataframe estiver ausente, então nenhuma combinação foi achada no outro dataframe no limiar de combinação especificado.
# print matches
<- fl_output$matches
my_matches my_matches
inds.a inds.b
1 1 1
2 2 2
3 3 3
4 4 4
5 8 8
6 7 9
7 6 10
8 5 12
Algumas coisas para se ter em mente:
- As combinações ocorrem apesar de algumas diferenças sutis na grafia dos nomes ou nas datas de nascimento:
- “Tony B. Smith” combinou com “Anthony B Smith”
- “Maria Rodriguez” combinou com “Marialisa Rodrigues”
- “Betty Chase” combinou com “Elizabeth Chase”
- “Olivier Laurent De Bordeaux” combinou com “Oliver Laurent De Bordow” (data de nascimneto ausente foi ignorada)
- “Tony B. Smith” combinou com “Anthony B Smith”
- Uma linha de
cases
(para “Blessing Adebayo”, linha 9) não teve nenhuma boa combinação emresults
, por isso não está prente emmy_matches
.
Join baseado em combinações probabilísticas
Para utilizar essas combinações para fazer um join de results
e cases
, uma estratégia seria:
- Utilizar
left_join()
para fazer o join demy_matches
emcases
(combinando os nomes das colunas emcases
com “inds.a” emmy_matches
)
- Depois, utilizar outro
left_join()
para fazer o join deresults
emcases
(combinando o recém adquirido “inds.b” emcases
com os nomes das linhas emresults
)
Antes dos joins, devemos limpar os três dataframes:
- Ambos
dfA
edfB
devem ter seus números de linhas (“rowname”) convertidos para uma coluna propriamente dita.
- Ambas as colunas de
my_matches
devem ser convertidas para a classe character, para que o join possa ser feito com os nomes de coluna que são caracteres
# Limpeza dos dados antes do join
#############################
# converte os nomes das linhas de cases em coluna
<- cases %>% rownames_to_column()
cases_clean
# converte os nomes das linhas de test_results em coluna
<- results %>% rownames_to_column()
results_clean
# converte todas as colunas no dataframe de combinações (my_matches) para character, para que o join possa ser feito com os nomes das linhas
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# Faça o join das combinações com dfA, e depois adicione dfB
###################################
# a coluna "inds.b" é adicionada ao dfA
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete
# colunas do dfB são adicionadas
<- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
Como feito no código acima, o dataframe complete
, resultado dos joins, vai conter todas as colunas de cases
e results
. Muitas delas estarão com sufixos “.x” e “.y”, por que, caso contrário, o nome das colunas seriam duplicados.
Como opção, para ter apenas os 9 registros “originais” em cases
com as novaas colunas de results
, utilize select()
em results
antes dos joins, de forma que ela contenha apenas os nomes de linhas e colunas que você queira adicionar a cases
(ex: a coluna result
).
<- cases %>% rownames_to_column()
cases_clean
<- results %>%
results_clean rownames_to_column() %>%
select(rowname, result) # selecion apenas algumas colunas
<- my_matches %>%
matches_clean mutate(across(everything(), as.character))
# joins
<- left_join(cases_clean, matches_clean, by = c("rowname" = "inds.a"))
complete <- left_join(complete, results_clean, by = c("inds.b" = "rowname")) complete
Se você quiser um subconjunto de qualquer um dos dataframes contendo apenas as linhas que combinaram, você pode utilizar o código abaixo:
<- cases[my_matches$inds.a, ] # Linhas em cases que combinaram com uma linha em results
cases_matched <- results[my_matches$inds.b, ] # Linhas em results que combinaram com uma linha em cases results_matched
Ou, para ver apenas as linhas que não combinaram:
<- cases[!rownames(cases) %in% my_matches$inds.a, ] # Linhas em cases que NÃO combinaram com uma linha em results
cases_not_matched <- results[!rownames(results) %in% my_matches$inds.b, ] # Linhas em results que NÃO combinaram com uma linha em cases results_not_matched
Remoção de duplicidades probabilística
A combinação probabilística também pode ser utilizada para remover duplicidades uma base de dados. Veja a página de remoção de duplicidades para outros métodos de deduplicação.
Aqui, vamos começar com a base cases
, mas a partir daqui chamaremos ela de cases_dup
, pois ela tem 2 linhas adicionais que podem ser duplicidades de linhas anteriores: Veja “Tony” com “Anthony”, e “Marialisa Rodrigues” com “Maria Rodriguez”.
Execute fastLink()
como anteriormente, mas compare o dataframe cases_dup
com ele mesmo. Quando os dois dataframes passados para a função são idênticos, ela assume que você quer fazer a remoção de duplicidades. Note que nós não especificamos stringdist.match =
ou numeric.match =
como havíamos feito anteriormente.
## Execute fastLink na mesma base de dados
<- fastLink(
dedupe_output dfA = cases_dup,
dfB = cases_dup,
varnames = c("gender", "first", "middle", "last", "yr", "mon", "day", "district")
)
====================
fastLink(): Fast Probabilistic Record Linkage
====================
If you set return.all to FALSE, you will not be able to calculate a confusion table as a summary statistic.
dfA and dfB are identical, assuming deduplication of a single data set.
Setting return.all to FALSE.
Calculating matches for each variable.
Getting counts for parameter estimation.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Running the EM algorithm.
Getting the indices of estimated matches.
Parallelizing calculation using OpenMP. 1 threads out of 8 are used.
Calculating the posterior for each pair of matched observations.
Getting the match patterns for each estimated match.
Agora, você pode revisar as duplicidades em potencial com getMatches()
. Passe o dataframe tanto como dfA =
como dfB =
, e passa a saída da função fastLink()
como fl.out =
. fl.out
deve ser da classe fastLink.dedupe
, ou em outras palavras, o resultado de fastLink()
.
## Execute getMatches()
<- getMatches(
cases_dedupe dfA = cases_dup,
dfB = cases_dup,
fl.out = dedupe_output
)
Veja a coluna da extrema direita, que indica os IDs das duplicatas - as duas últimas linhas estão identificadas como sendo potenciais duplicatadas das linhas 2 e 3.
Para retornar o número de linhas que são potenciais duplicidades, você pode contar o número de linhas por valores únicos na coluna dedupe.ids
, e aí filtrar para manter apenas aquelas com mais de uma linha. Nesse caso, isso deixaria as linhas 2 e 3.
%>%
cases_dedupe count(dedupe.ids) %>%
filter(n > 1)
dedupe.ids n
1 2 2
2 3 2
Para investigar a totalidade das linhas que poderiam ser duplicidades, coloque o número da linha nesse comando:
# mostra a linha 2 e todas as possíveis duplicidades dela
$dedupe.ids == 2, ] cases_dedupe[cases_dedupe
gender first middle last yr mon day district dedupe.ids
2 M Anthony B. Smith 1970 9 19 River 2
10 M Tony B. Smith 1970 9 19 River 2
14.4 Anexando (binding) e alinhando
Um outro método de combinar dois dataframes é anexando (“binding”) um ao outro. Você também pode pensar nisso como incluir (“appending”) ou adicionar (“adding”) linhas ou colunas.
Essa seção também vai discutir como “alinhar” a ordem das linhas de um dataframe à ordem de outro dataframe. Esse tópico é discutido abaixo na seção de Anexar colunas.
Anexar linhas (bind rows)
Para anexar linhas de um dataframe no final de outro dataframe, utilize a função bind_rows()
do pacote dplyr. Essa é uma função bastante inclusiva, então, todas as colunas presentes em cada dataframe será incluída na saída. Algumas notas:
- Diferente da versão
row.bind()
do R base,bind_rows()
do dplyr não exige que a ordem das colunas seja a mesma em ambos os dataframes. Contanto que os nomes das colunas estejam grafados de forma idêntica, a função vai alinhá-las corretamente.
- Você poder, opcionalmente, especificar o argumento
.id =
. Passe um nome de coluna para o argumento. Isso vai produzir uma nova coluna que serve para identificar de qual dataframe cada linha veio originalmente. - Você pode usar
bind_rows()
em umalista
de dataframes que tenham estrutura semelhante para combiná-los em um só. Veja um exemplo na página Iteração, loops e listas que envolve a importação de múltimplas linelists com purrr.
Um exemplo comum de anexação de linhas é anexar uma linha de “total” no final de uma tabela descritiva feita utilizando a função summarise()
do dplyr. Abaixo nós criamos uma tabela de contagem de casos e medianas dos valores de CT por hospital com uma linha total.
A função summarise()
é utilizada em dados agrupados por hospital para retornar um dataframe sumárizado por hospital. Porém, a função summarise()
não gera colunas de “totais” automaticamente, então, nós criamos uma sumarizando os dados novamente, mas com os dados não agrupados por hospital. Isso produz um segundo dataframe de apenas uma linha. Nós podemos anexar (bind) esses dataframes um ao outro para chegar na tabela final.
Veja outros exemplos práticos como esse nas páginas Tabelas descritivas e Tabelas para apresentação.
# Cria a tabela principal
###################
<- linelist %>%
hosp_summary group_by(hospital) %>% # Agrupa os dados por hospital
summarise( # Cria colunas sumário dos indicadores de interesse
cases = n(), # Número de linhas por grupo hospital-desfecho
ct_value_med = median(ct_blood, na.rm = T)
# valores medianos de CT por grupo )
Aqui está o dataframe hosp_summary
:
Cria um dataframe com as estatísticas de “total” (sem o agrupamento por hospital). Isso vai retornar apenas uma linha.
# cria totais
###############
<- linelist %>%
totals summarise(
cases = n(), # Número de linhas da base de dados inteira
ct_value_med = median(ct_blood, na.rm = T)
# CT Mediano para toda a base de dados )
E abaixo está o dataframe totals
. Note como tem apenas duas colunas. Essas colunas também está em hosp_summary
, mas tem uma coluna em hosp_summary
que não está em totals
(hospital
).
Agora podemos anexar as linhas com bind_rows()
.
# Bind data frames together
<- bind_rows(hosp_summary, totals) combined
Agora podemos visualizar o resultado. Veja como na última linha, um valor vazio, NA
, preenche a coluna hospital
que não estava em hosp_summary
. Como explicado na página Tabelas para apresentação, você pode preencher essa célula com “Total” utilizando replace_na()
.
Anexar colunas
Existe uma função similar no dplyr, a bind_cols()
que você pode utilizar para combinar dois dataframes “lateralmente”. Note que as linhas são combinadas umas com as outras por posição (não como no join acima) - por exemplo, a 12ª linha de cada dataframe estarão alinhadas.
Para demonstrar, vamos anexar várias tabelas de sumário juntas. Para fazer isso, também demonstraremos como rearranjar a ordem das linhas de um dataframe para combinar com a ordem do outro, com match()
.
Aqui nós definimos case_info
como um dataframe de sumário dos casos da linelist, por hospital, com o número de casos e o número de mortes.
# Informação dos casos
<- linelist %>%
case_info group_by(hospital) %>%
summarise(
cases = n(),
deaths = sum(outcome == "Death", na.rm = T)
)
Então, digamos que haja um dataframe diferente, contact_fu
(fu -> follow up), contendo informação acerca da porcentagem de contatos expostos que foram investigados e “acompanhados” (follow-up), novamente por hospital.
<- data.frame(
contact_fu hospital = c("St. Mark's Maternity Hospital (SMMH)", "Military Hospital", "Missing", "Central Hospital", "Port Hospital", "Other"),
investigated = c("80%", "82%", NA, "78%", "64%", "55%"),
per_fu = c("60%", "25%", NA, "20%", "75%", "80%") # fu -> follow up
)
Note que os hospitais são os mesmos, mas estão em ordens diferentes em cada dataframe. A solução mais fácil seria utilizar left_join()
na coluna hospital
, mas você poderia também utilizar bind_cols()
com um passo extra.
Utilize match()
para alinhar a ordem
Pelo fato da ordem das linhas ser diferente, um simples bind_cols()
resultaria em um desalinhamento dos dados. Para consertar isso, podemos utilizar match()
do base R para alinhar as linhas de um dataframe na mesma ordem de outro. Vamos assumir, para essa abordagem, que não há valores duplicados em nenhum dos dataframes.
Quando utilizamos match()
, a sintaxe é match(ORDEM DO VETOR ALVO, COLUNA PARA MUDAR)
, onde o primeiro argumento é a ordem desejada (seja um vetor individual, ou nesse caso, uma coluna do dataframe), e o segundo argumento é uma coluna do dataframe que será reordenado. A saída do match()
é um vetor de números representando a posição correta do ordenamento. Você pode ler mais com ?match
.
match(case_info$hospital, contact_fu$hospital)
[1] NA 4 2 6 5 1
Você pode utilizar esse vetor numérico para reordenar o dataframe - coloque ele entre colchetes [ ]
antes da vírgula. Leia mais sobre a sintaxe de subconjuntos com colchetes no base R na página Introdução ao R. O comando abaixo cria um novo dataframe, definido como o anterior em que as linhas estão ordenadas de acordo com o vetor numérico acima.
<- contact_fu[match(case_info$hospital, contact_fu$hospital), ] contact_fu_aligned
Agora podemos anexar as colunas dos dataframes um no outro, com a ordem de linhas correta. Note que algumas colunas estão duplicadas e vão precisar de uma limpeza utilizando rename()
. Leia mais sobre bind_rows()
aqui.
bind_cols(case_info, contact_fu)
New names:
• `hospital` -> `hospital...1`
• `hospital` -> `hospital...4`
# A tibble: 6 × 6
hospital...1 cases deaths hospital...4 investigated per_fu
<chr> <int> <int> <chr> <chr> <chr>
1 Ausente 1469 611 St. Mark's … 80% 60%
2 Central Hospital 454 193 Military Ho… 82% 25%
3 Military Hospital 896 399 Missing <NA> <NA>
4 Other 885 395 Central Hos… 78% 20%
5 Port Hospital 1762 785 Port Hospit… 64% 75%
6 St. Mark's Maternity Hospital (… 422 199 Other 55% 80%
Uma alternativo do R base a bind_cols
é a função cbind()
, que faz a mesma operação.
14.5 Recursos
A página do tidyverse sobre joins
A página do R for Data Science sobre dados relacionais
A página do tidyverse no dplyr sobre binding (ligação)
Uma vignette sobre fastLink na página do Github do pacote
Publicação descrevendo a metodologia do fastLink
Publicação descrevendo o pacote RecordLinkage