6.8 Implementación
A continuación se presenta un resumen de los principales paquetes de R y Python que se pueden utilizar para la vinculación probabilística de registros:
Lenguaje | Paquete | Características principales |
---|---|---|
R | RecordLinkage |
Implementa Fellegi-Sunter, Soundex, Jaro-Winkler, Levenshtein. Permite bloques, clasificación supervisada o no. |
R | fastLink |
Modelo bayesiano de Fellegi-Sunter. Maneja datos faltantes. Permite estimación de probabilidades y escalabilidad. |
R | fuzzyjoin |
Permite uniones por coincidencias parciales como stringdist , regex y se integra con dplyr . |
R | stringdist |
Ofrece múltiples métricas de distancia (Levenshtein, Jaccard, Jaro, Hamming). Útil para comparaciones de texto. |
Python | recordlinkage |
Implementa Fellegi-Sunter, SVM, Random Forests. Permite bloques y evaluación de desempeño. |
Python | Dedupe |
Usa aprendizaje supervisado y semi-supervisado. Permite bloques y métodos de clúster. |
Python | splink |
Basado en Fellegi-Sunter, escalable con Spark, DuckDB o SQL. Visualización interactiva. Soporta paralelización. |
6.8.1 Deduplicación de registros
Una etapa clave en el cálculo de la omisión censal, es asegurar que la base de enumeración de la encuesta de cobertura no tiene duplicados. Poder identificar si una persona ha sido enumerada más de una vez en el censo, se conoce como proceso de deduplicación.
Ejemplo usando el paquete RecodrLinkage
Para ilustrar este procedimiento se implementará un análisis supervisado utilizando los datos simulados RLdata500
incluidos en el paquete RecordLinkage
. El conjunto de datos contiene 500 registros simulados, incluyendo nombres, apellidos, fechas de nacimiento y un identificador de la persona real (identity.RLdata500
). Suponga que este es un conjunto de entrenamiento que fue seleccionado con unos registros del censo, y en el cual se realizó un proceso de identificación y revisión clerical para identificar con certeza si un registro es duplicado o no, de esta manera es posible entrenar un modelo, realizar evaluaciones de precisión y entender mejor las decisiones del algoritmo.
## fname_c1 fname_c2 lname_c1 lname_c2 by bm bd
## 1 CARSTEN <NA> MEIER <NA> 1949 7 22
## 2 GERD <NA> BAUER <NA> 1968 7 27
## 3 ROBERT <NA> HARTMANN <NA> 1930 4 30
## 4 STEFAN <NA> WOLFF <NA> 1957 9 2
## 5 RALF <NA> KRUEGER <NA> 1966 1 13
## 6 JUERGEN <NA> FRANKE <NA> 1929 7 4
En caso de realizar todas la comparaciones por pares, serían necesarias 124.750 comparaciones:
\[\binom{500}{2} = 124.750\] Lo anterior es manejable en conjuntos de datos pequeños, pero en los casos de censos o encuestas de cobertura no resulta viable aplicar el total de comparaciones, por lo que será necesario realizar una indexación con unos bloques de comparación.
Como se ha mencionado antes, el bloqueo consiste en agrupar los registros en bloques más pequeños usando una o más variables, de manera que solo se comparan registros dentro del mismo bloque. En este ejemplo se usará la primera letra del apellido como clave de bloqueo.
## inic_apell
## A B D E F G H J K L M N O P R S T V W Z
## 5 56 2 6 38 12 32 8 46 13 76 8 4 6 7 115 2 7 52 5
Lo anterior genera 20 bloques, donde el número de registros por bloque puede ser diferente. Como ahora el número de comparaciones se realiza dentro de cada bloque, esto reduce drásticamente el número total de comparaciones que se tienen que realizar. Sin embargo, es recomendable evitar una alta variación en el número de registros por bloque, esto debido a que algunos bloques con un alto número de registros puede incremetar fuertemente el costo computacional. En este caso el número de registros por bloque varía entre 2 y 115.
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 2.00 5.75 8.00 25.00 40.00 115.00
A pesar de lo anterior, el número de pares posibles tras aplicar el bloqueo baja de 124750 a 14805 pares. Esta reducción es crucial para el rendimiento computacional del algoritmo. A continuación se observa el número de comparaciones por bloque.
## A B D E F G H J K L M N O P R S
## 10 1540 1 15 703 66 496 28 1035 78 2850 28 6 15 21 6555
## T V W Z
## 1 21 1326 10
Para entrenar el modelo, se agrega a la tabla de datos el id de cada persona y se quita la información redundante:
Ahora es sencillo filtrar los duplicados reales, esto permite examinar cómo se presentan las inconsistencias reales en los datos y elegir los métodos más apropiados en el entrenamiento del modelo.
## # A tibble: 6 × 6
## fname_c1 lname_c1 by bm bd id
## <chr> <chr> <int> <int> <int> <dbl>
## 1 RENATE SCHUTE 1940 12 29 436
## 2 RENATE SCHULTE 1940 12 29 436
## 3 CHRISTINE PETERS 1993 2 5 442
## 4 CHRISTINE PETERS 1993 2 6 442
## 5 CHRISTA SCHWARZ 1965 7 13 444
## 6 CHRISTAH SCHWARZ 1965 7 13 444
Al calcular la distancia de Levenshtein observamos que la similaridad aún está lejana de 1, mientras que la métrica de Jaro y Winkler produce un mejor resultado de la similaridad.
## [1] 0.2857143 0.2500000
## [1] 0.9714286 0.9750000
El algoritmo de Jaro-Winkler tiende a funcionar mejor cuando los errores son de tipeo o diferencias leves. También se puede aplicar codificación fonética como soundex()
o cualquiera de las presentadas en este capítulo.
En el siguiente paso se realiza la comparación de pares con bloqueo supervisado, toda vez que el resultado que identifica si es un duplicado o no es observado. La función compare.dedup()
crea un objeto con la comparación entre pares de registros dentro de cada bloque. En este ejemplo se generan pares comparables según los bloques definidos por la primera letra del nombre y año de nacimiento (posiciones 1 y 3). Cada par se compara por igualdad de campos y similitud textual cuando se especifica.
entrenamiento <- compare.dedup(RLdata500c[,-6],
blockfld = list(1, 3),
identity = identity.RLdata500)
Ahora se calculan pesos probabilísticos para cada par comparado. Este paso estima la probabilidad de que cada par sea un match verdadero, usando un modelo probabilístico basado en la teoría de Fellegi-Sunter.
A partir de las probabilidades se clasifica automáticamente los pares en tres categorías:
- P: Positivo (match verdadero).
- N: Negativo (no match).
- L: Incertidumbre (requiere revisión clerical).
En este caso se especifica un umbral de 0.7, es decir, los pares con probabilidad superior a ese valor se clasifican como positivos.
##
## Deduplication Data Set
##
## 500 records
## 2709 record pairs
##
## 50 matches
## 2659 non-matches
## 0 pairs with unknown status
##
##
## Weight distribution:
##
## [0.2,0.25] (0.25,0.3] (0.3,0.35] (0.35,0.4] (0.4,0.45] (0.45,0.5] (0.5,0.55]
## 2318 0 114 131 30 50 8
## (0.55,0.6] (0.6,0.65] (0.65,0.7] (0.7,0.75] (0.75,0.8] (0.8,0.85] (0.85,0.9]
## 2 10 0 0 35 8 3
##
## 46 links detected
## 0 possible links detected
## 2663 non-links detected
##
## alpha error: 0.080000
## beta error: 0.000000
## accuracy: 0.998523
##
##
## Classification table:
##
## classification
## true status N P L
## FALSE 2659 0 0
## TRUE 4 0 46
Se observa que la gran mayoría de los pares comparados presentan baja evidencia de coincidencia, con 2318 pares concentrados en el intervalo de peso [0.2, 0.25]. Por otra parte, solo 46 pares alcanzan un peso mayor a 0.7, lo que sugiere una alta probabilidad de ser duplicados.
Según la matriz de clasificación, de los 50 pares realmente duplicados, el modelo identificó correctamente a 46, mientras que 4 no fueron detectados, lo que corresponde a una tasa de falsos negativos de \(\alpha = 8\)%. Por otro lado, la tasa de falsos positivos es cero, ya que ningún par no duplicado fue clasificado erróneamente como duplicado.
En conjunto, el modelo alcanzó una exactitud del 99.85%, lo que indica un alto rendimiento en la tarea de deduplicación.
Una vez se ha entrenado el modelo, se puede aplicar una comparación difusa (fuzzy) a todos los datos, para ampliar las posibilidades del ejemplo se usará con la métrica de Jaro-Winkler para todas las variables de cadena (strcmp = TRUE
). Se omite el uso de funciones fonéticas (phonetic = FALSE
), lo cual es útil cuando queremos detectar errores ortográficos leves y los bloques se arman solo por año de nacimiento. Aunque en la práctica se debe especificar el modelo entrenado.
modelo <- compare.dedup(RLdata500c[,-6],
phonetic = FALSE,
blockfld = list(1, 3),
strcmp = TRUE,
strcmpfun = jarowinkler)
En conclusión, el uso de bloqueo combinado con comparaciones textuales permite reducir significativamente el esfuerzo computacional, en este caso, más del 90%, al evitar comparaciones innecesarias entre todos los registros. Además, este enfoque es efectivo para detectar duplicados incluso cuando existen errores de tipeo o inconsistencias en los datos, logrando una clasificación precisa de los pares potencialmente duplicados.
Se recomienda ajustar adecuadamente el argumento blockfld para optimizar la eficiencia del proceso, y seleccionar el método de comparación textual (por ejemplo, Jaro-Winkler o Levenshtein) de acuerdo con la calidad y naturaleza de los nombres en los datos.
Finalmente, es importante validar los resultados obtenidos, ya sea mediante revisión clerical o a través de otras reglas, para asegurar la confiabilidad del proceso de deduplicación.
Ejemplo usando el paquete fastLink
Para explorar otras opciones, en este ejemplo se usará el conjunto de datos RLdata10000
del paquete RecordLinkage
, el cual contiene 10.000 registros con 1.000 duplicados y 8.000 no duplicados.
library(pacman)
p_load(tidyverse, janitor, fastLink, RecordLinkage, parallel)
data("RLdata10000")
head(RLdata10000)
## fname_c1 fname_c2 lname_c1 lname_c2 by bm bd
## 1 FRANK <NA> MUELLER <NA> 1967 9 27
## 2 MARTIN <NA> SCHWARZ <NA> 1967 2 17
## 3 HERBERT <NA> ZIMMERMANN <NA> 1961 11 6
## 4 HANS <NA> SCHMITT <NA> 1945 8 14
## 5 UWE <NA> KELLER <NA> 2000 7 5
## 6 DANIEL <NA> HEINRICH <NA> 1967 5 6
Al igual que en el ejemplo anterior, suponga que un subconjunto de los datos de la muestra E fue revisado de forma manual para establecer la coincidencia con la muestra P, y que ha conservado un id único que permite realizar el emparejamiento exacto.
En el caso de RLdata10000
se cuenta con el vector identity.RLdata10000
que conserva el id único de cada registro, esto con fines de entrenamiento de un modelo o como en este caso, para mostrar el uso de los procedimientos. Note que solo hay 9.000 identificadores únicos, por lo que 1.000 son duplicados, el desafío es que los métodos de emparejamiento los identifique con el menor error.
## [1] 9000
Se define el vector var
con todas las variables que se hará el emparejamiento, en el vector char_vars
se conservan las variables de cadena donde es posible hacer cálculos con métricas de similaridad, cal_simil
especifica para cuales de las variables de char_vars
no se exige coincidencias exactas. La métrica que se usa por defecto es Jaro-Winkler, pero hay otras opciones que se pueden implementar.
vars <- c("fname_c1", "lname_c1", "by", "bm", "bd")
char_vars <- c("fname_c1", "lname_c1")
cal_simil <- c("fname_c1", "lname_c1")
La función fastLink
permite identificar los duplicados usando los mismos datos en los argumentos de los dfA
y dfB
, y cuenta con un argumento para distribuir en varios cores el procesamiento. cut.a
es el umbral mínimo de probabilidad posterior para aceptar un emparejamiento y cut.p
es el umbral inferior para considerar un registro como emparejamiento potencial (que pase a revisión clerical), es decir, si la probabilidad está entre cut.p
y cut.a
, el par se considera un emparejamiento potencial que requiere revisión manual. Si la probabilidad es menor que cut.p
, el registro se considera como no emparejado. Se debe tener en cuenta que un valor muy alto de cut.a
puede originar más precisión pero menos emparejamientos, pero si cut.a
es bajo entonces se espera un mayor recall y un mayor riesgo de falsos positivos.
nCores <- detectCores()
res <- fastLink(dfA = RLdata10000, dfB = RLdata10000,
varnames = vars,
stringdist.match = char_vars,
stringdist.method = "jw",
partial.match = cal_simil,
cut.a = 0.94,
cut.p = 0.84,
dedupe = FALSE,
n.cores = nCores - 1)
##
## ====================
## 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.
El procedimiento genera la variable dedupe.ids
para todo el conjunto de datos. La función getMatches
permite extraer el conjunto de datos con la variable de identificación.
index_dup <- getMatches(dfA = RLdata10000, dfB = RLdata10000, fl.out = res)
index_dup <- index_dup |> bind_cols(data.frame(id = identity.RLdata10000))
duplicados <- get_dupes(index_dup, dedupe.ids)
head(duplicados)
## dedupe.ids dupe_count fname_c1 fname_c2 lname_c1 lname_c2 by bm bd id
## 1 420 3 GUENTHER <NA> ZIMMERMWANN <NA> 1971 6 23 1794
## 2 420 3 GUENTHER <NA> ZIMMERMANN <NA> 1992 6 23 1864
## 3 420 3 GUENTHER <NA> ZIMMERMANN <NA> 1971 6 23 1794
## 4 3969 3 GERTRUD <NA> MUELLER <NA> 1964 7 27 8970
## 5 3969 3 GERTRUD <NA> MUELOER <NA> 1964 7 11 7616
## 6 3969 3 GERTRUD <NA> MUELLER <NA> 1964 7 11 7616
El desempeño del modelo se puede evaluar mediante una matriz de confusión que compara las predicciones del modelo con los valores reales. En este caso, el modelo identificó correctamente 982 verdaderos positivos, es decir, observaciones que efectivamente eran duplicados. Sin embargo, también generó 18 falsos negativos, que son casos verdaderos que el modelo no logró identificar correctamente. Además, el modelo produjo 63 falsos positivos, es decir, casos que fueron clasificados como verdaderos por el modelo, pero en realidad no eran duplicados.
ids_duplicados <- names(table(identity.RLdata10000))[table(identity.RLdata10000) > 1]
todos_ids <- union(unique(duplicados$id), unique(ids_duplicados))
dupes_model <- todos_ids %in% unique(duplicados$id)
dupes_real <- todos_ids %in% ids_duplicados
matriz <- table(Real = dupes_real, Modelo = dupes_model)
print(matriz)
## Modelo
## Real FALSE TRUE
## FALSE 0 63
## TRUE 18 982