26 de marzo de 2010

Aumento de calidad de datos patronimicos

En el 2009 estuve trabajando sobre un algoritmo para realizar búsquedas de pacientes en un base de datos. Si bien esto es una tarea más que resuelta en muchas instituciones, el objetivo del algoritmo era no solo obtener una lista de datos de personas que se correspondieran con un criterio dado, si no detectar en cuanto se corresponden, de forma de encontrar los registros que más se parezcan a lo que buscamos.

Si bien se logró un algoritmo junto al Subcomité Técnico de Identificación de Personas (STID) de SUEIIDISS (HL7 Uruguay), del que participé en el 2009, yo me concentré en otro problema, el de la calidad de los datos. Si bien podemos pensar en cómo resolver las búsquedas de la mejor forma, si la calidad de los datos en nuestra base de datos es pobre, el resultado de la búsqueda será de poca utilidad, y si se llevara la calidad a niveles óptimos, cualquier mecanismo simple de búsqueda nos ayudaría a encontrar lo que necesitamos, por eso el cambio de enfoque. Dicho sea de paso, aquí se puede obtener una copia del documento logrado en el STID.

El resultado de mi investigación fue un algoritmo que sirve para comparar dos identidades, formadas por los atributos: nombres, apellidos, fecha de nacimiento y sexo de una persona. Cualquier tipo de identificador de la persona no forma parte de su identidad, por eso no se consideran números de documentos nacionales ni otros tipos de identificadores en las identidades. El resultado del algoritmo es un número entre 0 y 1 donde 0 indica coincidencia nula y 1 coincidencia total. Un resultado de 0.85 indica que las identidades coinciden en un 85%.

Este algoritmo fue probado en bases de datos con miles de registros, y los resultados obtenidos son óptimos. Según estos experimentos, las identidades cuya coincidencia supera el 95% corresponden a la misma persona, y la diferencia se debe a que hay errores en los datos, por ejemplo un nombre mal escrito, que en base a mis experimentos, es uno de los casos más frecuentes en los de fuentes de error, por ejemplo una letra faltante o la transposición de letras son de los errores más comunes.

Detrás del algoritmo hay tres ideas muy intuitivas:
  • Distintos datos de la identidad de una persona pesan distinto en la identificación de la misma. Nuestra identidad es lo que nos diferencia de las demás personas, pero distintas partes de esta identidad puede diferenciarnos en mayor o menor medida. No es lo mismo diferenciarse por los nombres que por la fecha de nacimiento, seguramente hayan muchas más personas que nacieron el mismo día que uno, que personas que se llaman igual. Entonces cada atributo de la identidad tiene un peso asociado.
  • No solo pasa que cada atributo pesa distinto en la identificación de la persona, si no que este peso en realidad debería variar dependiendo del valor de los atributos. Por ejemplo, si un apellido es muy común, el peso debería ser menor, y si es un apellido poco usual, su peso debería ser mayor. El algoritmo que considera esta afirmación se llamará "Algoritmo de Verificación de Identidades Avanzado" o "AVI-a", y no será descrito en este artículo.
  • El algoritmo debe comportarse bien ante casos de errores en los datos, es decir que si se comparan dos identidades, y una tiene errores en los datos, el algoritmo debería igualmente detectar la similitud entre las identidades. Es por esto que condiciones de igualdad o igualdad parcial no son útiles para el cálculo del índice de coincidencia porque consideran a los datos como un todo o lo divide en grandes partes, es necesario llegar a un nivel de comparación con granularidad suficiente para que los errores no afecten tanto a las comparaciones.

Descripción del Algoritmo de Verificación de Identidades Básico (AVI-b):

Aquí voy a bajar a un nivel muy técnico, igualmente voy a ir explicando los detalles para que nadie pierda el hilo.

La identidad mínima de una persona, como mencioné antes, está formada por los atributos: nombres, apellidos, sexo y fecha de nacimiento. Vamos a tomar dos identidades id1 e id2 para compararlas según el AVI-b.

// Tipo que define la Identidad
Identidad {
  String nom1
  String nom2
  String ap1
  String ap2
  CodigoSexo sexo
  Fecha fecha_nac
}

// Tipo que define la el código para el sexo según la
// codificación utilizada en HL7 que se puede ver aquí:
CodigoSexo {
  const String MASCULINO = "M"
  const String FEMENINO = "F"
  const String IDENFINIDO = "UN"

  // Debe ser alguna de las constantes definidas previamente
  String codigo

}

// Identidades a comparar, con respectivos sus valores para cada atributo
Identidad id1 = { nom11, nom12, ap11, ap12, sexo1, fecha_nac1 }
Identidad id2 = { nom21, nom22, ap21, ap22, sexo2, fecha_nac2 }


Como se mencionó, una de las ideas detrás del algoritmo es que, cada atributo de la identidad tiene un peso distinto en cuanto a la identificación de la persona. A continuación se especifica el peso de cada atributo en la identificación, donde la regla que deben cumplir estos valores es que deben sumar 1, porque si se tienen todos los atributos de la identidad, estos sirven para identificar a la persona en un 100%. Esto se traduce a que si el peso del primer nombre es 0.175, quiere decir que el primer nombre identifica a una persona en un 17,5%. Los valores aquí propuestos son valores encontrados empíricamente, para los cuales se obtuvo un buen funcionamiento del algoritmo (AVI-b).

// Los pesos deben sumar 1
pesos[nom1] = 0.175
pesos[nom2] = 0.175
pesos[ap1]  = 0.175
pesos[ap2]  = 0.175
pesos[sexo] = 0.1
pesos[fecha_nac] = 0.2



No está de más comentar que si bien los pesos planteados para el algoritmo básico (AVI-b) son constantes, por lo expresado en las ideas que están detrás del algoritmo, para el algoritmo avanzado (AVI-a), estos pesos serían funciones que dependen del valor de cada atributo.

A continuación se especifican los algoritmos de comparación de atributos simples que se utilizan para calcular el índice de coincidencia, que es el resultado del AVI-b. Se plantean dos algoritmos distintos para la comparación de nombres y apellidos, uno para el sexo y otro para la fecha de nacimiento. El primer algoritmo para comparar nombres hace la cuenta de cuantos caracteres coinciden en valor y posición y retorna esta cantidad dividida el largo de la palabra más larga, por lo que se obtiene un resultado en el rango 0..1, con valor 1 cuando los nombres y apellidos coinciden completamente. El segundo algoritmo de comparación de nombres está basado en la distancia de Levenshtein, que calcula cuantos caracteres se le deben agregar a una palabra para hacerla coincidir con otra, por lo tanto si el resultado de Levenshtein es 0 las palabras son la misma. La el índice basado en Levenshtein va a dar resultados más altos cuando haya errores de transposición u omisión de caracteres en alguno de los nombres que se están comparando, mientras que el primer algoritmo va a dar más bajo cuando uno de los nombres que se compara tiene alguna letra de menos. Luego voy a mostrar un ejemplo de esto, así que si no se entendió, luego lo retomaremos.

// Case Insensitive Char by Char:
// comparacion de palabras utilizando indice basado en
// comparacion caracter a caracter.
coin_ci_cbc( String a, String b)
{
   // Se pasa todo a minusculas para comparar
   a = pasar_a_minusculas( a )
   b = pasar_a_minusculas( b )
   int max_len = maxLargo(a, b) // Largo del string mas largo
   int min_len = minLargo(a, b) // Largo de string mas corto
   int coincidencias = 0

   for (i = 0..min_len)
   {
      if ( a[i] == b[i] ) coincidencias ++
   }

   return coincidencias / max_len
}


// Case Insensitive Levenshtein:
// comparacion de palabras utilizando indice basado en distancia de Levenshtein
// Mas info>
// - http://www.levenshtein.net/
// - http://es.wikipedia.org/wiki/Distancia_de_Levenshtein
coin_ci_levens( String a, String b)
{
   // Se pasa todo a minusculas para comparar
   a = pasar_a_minusculas( a )
   b = pasar_a_minusculas( b )

   // Largo del string mas largo
   int max_len = maxLargo(a, b)

   // Calcula el indice basado en la distancia de levenshtein

   return 1 - ( levenshtein(a, b) / max_len )
}

coinSEXO(CodigoSexo s1, CodigoSexo s2)
{
   if (s1.codigo == s2.codigo) return 1
   return 0
}

coinFECHA (Fecha f1, Fecha f2)
{
   if (f1 == f2) return 1
   return 0
}


Por último, el cálculo del Índice de Coincidencia que será resultado del AVI-b, como vemos usamos los pesos y los algoritmos para calcular la comparación de atributos simples. En el ejemplo se utiliza coin_ci_cbc como comparador de nombres, éste se podría cambiar por coin_ci_levens.

IC( id1, id2 ) = pesos[nom1] * coin_ci_cbc(id1.nom1, id2.nom1) +
          pesos[nom2] * coin_ci_cbc(id1.nom2, id2.nom2) +
          pesos[ap1]  * coin_ci_cbc(id1.ap1,  id2.ap1) +
          pesos[ap2]  * coin_ci_cbc(id1.ap2,  id2.ap2) +
          pesos[sezo] * coinSEXO(id1.sexo, id2.sexo) +
          pesos[fecha_nac] * coinFECHA(id1.fecha_nac, id2.fecha_nac)

En la siguiente tabla se puede ver el comportamiento del algoritmo básico (AVI-b) probando con algunos casos donde se toma una identidad y se varía algún dato para verificar cómo esta modificación afecta al Índice de Coincidencia (IC) que obtengo como resultado. Algunas aclaraciones para poder interpretar correctamente los resultados:
  • La identidad ID1 es siempre la misma, la que varía es ID2 en algunos valores.
  • Se hicieron 3 casos de comparación, probando con los dos algoritmos distintos para la comparación de nombres y apellidos.
  • Las filas de igual color corresponden a comparaciones de las mismas identidades pero utilizando algoritmos distintos para la comparación de nombres, es decir: ci_cbc o ci_levens.
  • En las filas 1 y 4, la variación de ID2 con respecto a ID1 es que en el segundo nombre se sacó una letra, la C.
  • En las filas 2 y 5, la variación de ID2 con respecto a ID1 es en la fecha de nacimiento.
  • En las filas 3 y 6, la variación de ID2 con respecto a ID1 es el primer nombre completo, manteniendo todos los otros datos. Este caso se podría dar en hermanos gemelos o mellizos, y el algoritmo debería ayudar a diferenciar entre ellos, si no es así, se debería utilizar la identidad extendida mediante los datos de "indicador de nacimiento múltiple" y "orden de nacimiento" que se describen en el documento del STID.

# IC Algor. ID1 ID1 fecha ID2 ID2 fecha
1
0.86
ci_cbc
Ana Jacqueline
Gomez Rodriguez
1983-11-22 F Ana Jaqueline
Gomez Rodriguez
1983-11-22 F
2
0.8
ci_cbc
Ana Jacqueline
Gomez Rodriguez
1983-11-22 F Ana Jacqueline
Gomez Rodriguez
1983-11-12 F
3
0.825
ci_cbc
Ana Jacqueline
Gomez Rodriguez
1983-11-22 F Carla Jacqueline
Gomez Rodriguez
1983-11-22 F
4
0.9825
ci_levens
Ana Jacqueline
Gomez Rodriguez
1983-11-22 F Ana Jaqueline
Gomez Rodriguez
1983-11-22 F
5
0.8
ci_levens
Ana Jacqueline
Gomez Rodriguez
1983-11-22 F Ana Jacqueline
Gomez Rodriguez
1983-11-12 F
6
0.895
ci_levens
Ana Jacqueline
Gomez Rodriguez
1983-11-22 F Carla Jacqueline
Gomez Rodriguez
1983-11-22 F
Tabla de resultados


Trabajo futuro:

Si bien se probó empíricamente el buen funcionamiento del Algoritmo de Verificación de Identidades Básico (AVI-b) creo que se podría ajustar aún más el resultado utilizando el algoritmo avanzado, donde los pesos se ajustan según los valores de los atributos de las identidades. Esto sería fácil de hacer si se tienen calculadas las frecuencias de nombres y apellidos de las identidades que se tienen en la base de datos donde están registradas las identidades a verificar, esto quiere decir, calcular cuántas personas tienen cada nombre (Pablo, Pedro, Juan, etc) y dividir cada una de esas cantidades entre el número total de identidades en la base. Luego esa frecuencia sería utilizada para aumentar el peso de un atributo (si es que hay poca frecuencia de ese nombre o apellido), o disminuir el peso de un atributo (si es que hay muchas personas que se llaman igual). Esta técnica ajustaría los valores resultantes de las comparaciones a un porcentaje más ajustado a la realidad. Como puede resultar obvio, los valores de frecuencias se deberían actualizar cada tanto, y con esto logramos un resultado que puede estar oculto a primera vista: el algoritmo se ajusta a las distintas frecuencias de nombres que se dan en distintos momentos. Es sabido que hay épocas donde un nombre se hace muy popular, y en algunos años el nombre más popular cambia, cambiando la frecuencia de personas que podemos encontrar con esos nombres en nuestras bases de datos. Con esto logramos un algoritmo que se adapta automáticamente (algoritmo adaptable) a cambios que se dan en la cultura de la sociedad. ¿Interesante no?


Conclusiones:

Cómo se puede apreciar en las filas 1 y 4 de la tabla de resultados, al cambiar una sola letra de un nombre o un apellido, el IC es mucho más alto usando el algoritmo ci_levens que el ci_cbc para comparar nombres, esto se debe a que el índice que se calcula en base a Levenshtein es más fuerte o se comporta mejor ante pequeños cambios. Este cambio se puede deber a un error humano en el registro, y es bueno tener un algoritmo que lo pueda detectar. Es decir que la detección de duplicados también ayuda a la detección de errores en el registro, y por lo tanto a mejorar la calidad del mismo, que es el objetivo planteado al inicio.

En las filas 2 y 5 no vemos cambio en el resultado del CI porque lo que es distinto entre las identidades es la fecha de nacimiento.

Por último, en las filas 3 y 6 vemos una pequeña diferencia en el CI, esto se debe a que el primer nombre es tan distinto entre ID1 e ID2 que tanto el índice basado en Levenshtein como el índice basado en la cuenta carácter a carácter detectan que ambos nombres son distintos, bajando el valor del CI en cantidades similares.

Espero que se hayan entendido las ideas detrás del algoritmo, cuáles eran los objetivos y que hayan quedado claras las conclusiones. Por cualquier consulta o comentario no duden en agregar un comentario.

4 comentarios:

  1. Pablo, te felicito por la investigación. El tema de la identificación de personas es vital para los sistemas clínicos y centrarse en la "calidad" del registro es parte importante para el éxito.

    Me gustó tu idea de frecuencia de nombres, yo lo agregaría a un "diccionario" validado de nombres bien escritos o de todas sus variantes léxicas, que permita identificar un "error" en el ingreso de un nombre sin que exista duplicación de registro.

    ¿Existen bases secundarias en Uruguay donde puedas hacer una consulta sobre una persona y compararte?

    ¿Probaste con el SOUNDEX para normalizar el string de nombres y apellidos para hacer la comparación fonética de los mismos?. Una de las mayores dificultades que tienen las plataformas que "empadronan" es identificar si es una C una Z, una doble ZZ, una S.... o cosas más lejanas como "Carla" y "Ana"... el soundex te permitiría una comparación más cercana que el score de Levenshtein.


    Saludos

    ResponderEliminar
  2. Alejandro, muchas gracias!

    Hay varias formas de mejorar el algoritmo, una es la frecuencia de nombres, otra es hacer un poco más inteligente la comparación de fechas, por ejemplo verificar que si el año y el mes coinciden dar un valor de 0.5 en lugar de 0.0 como está dando ahora la comparación de fechas. Otra es hacer un análisis estadístico de la base de datos y los frecuencias de cada dato y ajustar los pesos que propongo aquí para esas frecuencias, sería adaptar el algoritmo a la base donde va a correr.

    Los que planteas de la verificación del dato al ingreso, me hace pensar que se podrían tener distintos algoritmos clasificados por la etapa en la que deben ejecutarse, por ejemplo "en el momento del ingreso" o "post ingreso". El AVI que planteo es claramente un "post ingreso", porque el volumen de datos a analizar podría ser mucho y no andaría bien en un entorno de tiempo real (demora mucho). Pensándolo rápido lo de la base con variantes léxicas parece una buena idea a probar y me recuerda a los Servicios Terminológicos para controlar conceptos clínicos. Una traba que puede darse es por ejemplo cuando la transmisión del nombre es oral y digo Michael cuando el nombre de la persona es en realidad Maicol (la españolización de los nombres de origen anglo-sajón es muy frecuente en Uruguay). Sería un lindo trabajo para ver cómo se comporta el algoritmo.

    Sobre las bases de datos, existe la base de la Dirección Nacional de Identificación Civil, que es la que entrega las Cédulas de Identidad (nuestro DNI). Hemos hablado con ellos para acceder a sus servicios. Ellos pueden ofrecer la verificación de un nombre y la verificación de que una cédula corresponde con los nombres dados. Otra base de datos enorme es la de nuestra previsión social pública, que es el principal seguro de salud en Uruguay, aunque no creo que podamos acceder a su MPI.

    Sobre soundex, al inicio estuvo en la mesa, junto con metaphone que es un soundex avanzado (http://php.net/manual/en/function.metaphone.php). Un problema de estos algoritmos es que no dan una medida, si no que dan un código, y yo estaba pensando desde el inicio en encontrar algo que me diera alguna medida de distancia numérica entre dos strings, comportándose "bien" para casos de error en los datos, ahí Levenshtein surgió naturalmente. Otro punto es que estos algoritmos son muy buenos cuando los datos son comunicados de forma oral, ya que quien registra registra lo que entiende en base a la pronunciación de la otra persona, ahí soundex puede ayudar a decirme si lo que ingresé es algo parecido a otra cosa, pero no decirme que lo que ingresé está mal. También el tema de que estos algoritmos son "idioma dependientes", y hay una gran cantidad de personas con nombres anglo-sajones que son pronunciados de la forma que se pronuncian en inglés, o sea que también depende de cómo lo pronuncie. Por otro lado, el índice basado en Levenshtein, e incluso el índice basado en comparación carácter a carácter, cuando dan resultados mayores a 0.9 he visto que es un buen indicador de un error en el registro, por ejemplo lo que pasa con Jacqueline y Jaqueline con la C.

    Obviamente este es un problema abierto y pueden haber muchos enfoques a considerar y probar. Estaría bueno pensar en un buen algoritmo para detectar errores en el momento del ingreso de los datos, las ideas que plateas son un buen punto de partida!

    ResponderEliminar
  3. Estimado Pablo,

    Respecto al articulo es interesante y te consulto si tenes referencia de documentación donde dentro de los atributos patronimicos de la persona este el documento.

    Trabajo en una intitución de salud en la cual utilizamos 5(7) datos para identificar la persona:

    Apellidos(ambos), Nombres(los que posea), Fecha de Nac, Sexo, y documento.

    En base estos 5 ú 7 datos me gustaría actualizar nuestras búsquedas de pacientes sugiriendo candidatos con ponderación.

    Agradecido desde ya por tu recomendación.

    saludos

    Esteban Aliaga

    ResponderEliminar
  4. Hola Esteban,

    Este trabajo se basó en una experiencia personal dentro de una federación de 23 instituciones sanitarias a nivel nacional.

    Las referencias que tuve son las del curso 10x10 del HIBA, y algunas experiencias de colegas en Argentina y Chile.

    La decisión de si se incluye o no el identificador en el algoritmo de búsqueda/detección de duplicados es meramente conceptual, mi argumento es: el identificador no es parte de la identidad de una persona, y la identidad es lo que lo define y lo diferencia del resto. Los identificadores son meramente una asignación arbitraria de un número o código a una persona por parte de alguna institución, que sirve a un propósito en dicha institución pero pueden no tener valor afuera.

    Dicho eso, creo que el identificador te sirve para confirmar una identidad duplicada, pero no para encontrarla. Conceptualmente, te serviría para encontrar a una persona solo si están 100% de que: 1. el identificador fue correctamente registrado, 2. no hay identificadores duplicados, 3. la asignación de identificador a una identidad es correcta (esto es lo más difícil de saber, confirmar y auditar).

    En definitiva, no usaría el id para detección de duplicados, pero si para confirmar su resultado. Para la búsqueda, si la hacés por id y no estás 100% serguro de los 3 puntos anteriores podés encontrar una persona que coincide con ese id pero no sabés si esa misma persona la tenés con otro id, o si ese el el id correcto para esa persona (lo que mencionaba en el punto 3).

    En general estos problemas que te comento son menores, pero si querés tender a cero error, deberían ser considerados en el diseño.


    Espero que te ayude, y cualquier cosa escribime de nuevo.

    Saludos!

    ResponderEliminar