lunes, 6 de junio de 2016

La fiabilidad de algún procedimiento de disociación

No hace mucho, en un debate sobre OSINT mencioné un dato que había leído por ahí. Lo malo era que no recordaba dónde y, como quien haya leído algo escrito por mí seguramente sabrá, me gusta citar mis fuentes.

Así que, tarde o temprano, tenía que corregir los efectos de mi olvido.

El asunto está relacionado con lo que en la LOPD se denomina "procedimiento de disociación" y que se define en su artículo 3, letra f, como "todo tratamiento de datos personales de modo que la información que se obtenga no pueda asociarse a persona identificada o identificable".

Pongamos un ejemplo. Supóngase que se tiene una tabla con datos de una serie de personas. Y sean las columnas de esta tabla las siguientes:
  • DNI.
  • Nombre y apellidos.
  • Calle y número.
  • Código postal.
  • Ingresos brutos anuales.
  • Los antecedentes penales que pudiera tener.
  • Los problemas de salud que pudiera tener.
  • Fecha de nacimiento
  • Sexo. (Quiero decir: si es hombre o mujer)
Publicar esta información, con los datos sensibles que contiene, o darla a conocer a terceros sin el debido consentimiento, constituiría una grave vulneración de la normativa sobre Protección de Datos de Carácter Personal. Pero la cosa cambiaría si antes se la somete a un procedimiento de disociación. Supongamos que quitamos las tres primeras columnas y nos quedamos con:
  • Código postal.
  • Ingresos brutos anuales.
  • Los antecedentes penales que pudiera tener.
  • Los problemas de salud que pudiera tener.
  • Fecha de nacimiento
  • Sexo. (Quiero decir: si es hombre o mujer)
Ahora las personas a las que pertenecen los datos dejarían de ser identificables y, por consiguiente, ya no habría tanto problema en proporcionar a alguien esta información. ¿Verdad?

Pues... lo siento, pero no. La tabla aún contiene un conjunto de datos que es capaz de identificar de forma única a cerca del 90% de la población: la combinación de código postal, fecha de nacimiento y sexo.

Ya encontré el paper donde lo leí por primera vez. Y no es nada nuevo. Data del año 2.000, su autor es Latanya Sweeney  y se denomina "Simple Demographics Often Identify People Uniquely". Quien quiera leerlo, lo tiene disponible en este enlace.

La idea es la siguiente: Supón que dispones de la tabla con los datos pretendidamente disociados. Vas por un barrio, te cruzas con alguno de sus vecinos o alguna de sus vecinas y le preguntas su fecha de nacimiento. Cuando te la diga, le puedes responder algo del tipo: "¡Ah! Y tus ingresos anuales son de X euros y no tienes antecedentes penales, pero sí hay algo de una lesión muscular".

Y lo puedes saber porque conoces el código postal del barrio en el que estás, si la persona es hombre o mujer (normalmente se sabe) y el día en que vino a este mundo.

Para hacernos una idea de hasta qué punto la combinación de estos valores son únicos en una población, podemos usar este programa escrito en Ruby:
# Calcula cúantas repeticiones de datos habrá en promedio
# en un grupo de un número determinado de sujetos
# y un número de posibles valores
def repetidos(numero_de_valores, numero_de_sujetos)
 con_datos_repetidos = 0.0
 con_datos_unicos = 0.0

 (1..numero_de_sujetos).each do |n|
  probabilidad_de_repeticion =  (n - 1) / numero_de_valores.to_f;

  # Si se repite valor, hay que distinguir si el valor ya estaba repetido o no
  # Si no estaba repetido y se repite ahora dato, son dos los que repiten
  # Si ya estaba repetido, los que lo tenían ya han sido contados  
  probabilidad_de_repeticion += probabilidad_de_repeticion * 
     con_datos_unicos /  (n - 1) if n>1
  
  # Calcular cuántos sujetos tienen valores repetidos o únicos
  con_datos_repetidos += probabilidad_de_repeticion
  con_datos_unicos = n - con_datos_repetidos
 end

 print "No Repetidos: #{con_datos_unicos.to_i} (#{con_datos_unicos})\n"
 print "Porcentaje: #{100 * con_datos_unicos / numero_de_sujetos} %\n\n" 
 
 print "Repetidos: #{con_datos_repetidos.to_i} (#{con_datos_repetidos})\n"
 print "Porcentaje: #{100 * con_datos_repetidos / numero_de_sujetos} %\n\n" 
end

Con objeto de simplificar el código, el cáculo de la probabilidad de repetición es bastante conservador. En realidad, la probabilidad de repetición podría ser menor si ya se hubieran encontrado datos repetidos. Ahora, sabiendo que en España, según Wikipedia, hay 11.752 códigos postales, 46.770.000 habitantes y una esperanza de vida de más de 82 años, se podría obtener una idea de cuan única es la combinación de código postal, sexo y fecha de nacimiento con una llamada a la función anterior como

repetidos((82*365.25).to_i * 2 * 11_752, 46_770_000)

Que arrojaría los siguientes resultados
No Repetidos: 43730297 (43730297.615365796)
Porcentaje: 93.50074324431428 %

Repetidos: 3039702 (3039702.384634202)
Porcentaje: 6.499256755685701 %

O sea: los datos son únicos en un 93,5% de los casos. Claro que hemos simplificado mucho. La distribución de edades, por ejemplo, no es homogénea y eso alteraría el resultado. OK. Podemos rebajar un poco el porcentaje y aceptar lo que dicen por ahí: un 87%.

NOTA: Para quienes prefieran las simulaciones, también hice un subprograma con ese enfoque. Ahí va...

# Simula el comporatmiento de una población
def simula(anos, codigos_postales, numero_de_sujetos)
 # Sitios donde almacenar conteos de sujetos con unos determinados datos 
 buzones = Hash.new(0)

 # Días a contemplar
 dias = (anos * 365.25).to_i
 
 # Ir simulando personas
 (1..numero_de_sujetos).each do
  dia = rand(dias)
  sexo = rand(2)
  codigo_postal = rand(codigos_postales)
  
  # Generar la clave a partir de los datos aleatorios
  clave = dia + sexo*dias + codigo_postal*dias*2
  buzones[clave] += 1
 end
 
 # Generar estadística de tamaño de grupo
 conteo = Hash.new(0)
  buzones.each_value do |v|
  conteo[v] += 1
 end
 
 # Mostrar resultados
 conteo.each do |tamano, cuenta|
  print "Grupos de coincidentes de #{tamano} sujetos: #{cuenta}\n"
 end
 
 # Probabilidad de ser identificable
 print "\nProbabilidad de ser identificable: " +
    "#{100.0*conteo[1] / numero_de_sujetos} %"
 print "\nProbabilidad de no ser identificable: " + 
    "#{100.0*(numero_de_sujetos - conteo[1]) / numero_de_sujetos} %"
end

Y la salida que produciría para un par de ejecuciones sería
simula(82, 11_752, 46_770_000)
Grupos de coincidentes de 2 sujetos: 1455587
Grupos de coincidentes de 1 sujetos: 43759933
Grupos de coincidentes de 3 sujetos: 32231
Grupos de coincidentes de 4 sujetos: 540
Grupos de coincidentes de 5 sujetos: 8

Probabilidad de ser identificable: 93.56410733376096 %
Probabilidad de no ser identificable: 6.4358926662390425 %




simula(82, 11_752, 46_770_000)
Grupos de coincidentes de 1 sujetos: 43764169
Grupos de coincidentes de 2 sujetos: 1453453
Grupos de coincidentes de 3 sujetos: 32296
Grupos de coincidentes de 4 sujetos: 503
Grupos de coincidentes de 5 sujetos: 5
Probabilidad de ser identificable: 93.5731644216378 %
Probabilidad de no ser identificable: 6.426835578362198 %

Los datos no se diferencian demasiado de los de antes. Buena señal.

No hay comentarios:

Publicar un comentario