Identificación falsa > Artículos > El bug que daba veintiún años a todos los italianos

El bug que daba veintiún años a todos los italianos

Busque cualquier nombre de pila italiano y la edad devuelta estaba entre 19 y 25 años. No la mayoría de las veces. Siempre. Mediana 21, mínimo 19, máximo 25, en la totalidad de los 3.189 nombres de los datos de cohorte italianos — Giuseppe, Giovanni, Mario, Maria, todos y cada uno de ellos.

Giuseppe no es un nombre de hombre joven. Es el nombre arquetípico de la generación de los abuelos italianos. El sistema lo daba por veintiún años con absoluta confianza, y lo hacía por una razón que no tiene nada que ver con la demografía italiana y sí con una única suposición aritmética: que un cero en los datos significa no había ninguno.

A veces es así. A veces significa la fuente no llega tan atrás. Son dos afirmaciones completamente distintas, se escriben de forma idéntica, y confundirlas constituye una clase de bug propia que ningún validador estructural puede ver.

Lo que el modelo debería hacer

Los nombres de pila llevan información de edad. No es una sutileza, es la señal más fuerte de todo el conjunto de datos: las modas de nombres se renuevan en un ciclo generacional, y un nombre indica la década en la que probablemente nació alguien con mucha más fiabilidad que cualquier otra cosa.

Así que un modelo que deduce la edad a partir del nombre es una idea realmente buena, y cuando los datos subyacentes están completos, funciona. Medido sobre 10.000 extracciones por nombre frente a una ventana de edad de 19 a 57 años:

LocaleNombreEdad 19–2526–3536–4546–57Mediana
no_NOBjørn1,6 %4,3 %8,6 %85,5 %57
no_NOEmma60,3 %29,4 %6,2 %4,1 %24
en_USRobert3,1 %6,2 %11,0 %79,7 %57
en_USEmily28,4 %37,1 %21,6 %12,9 %31

Eso es exactamente correcto, y lo es porque Noruega y los Estados Unidos publican recuentos de nombres de pila por año que se remontan lo bastante atrás como para llenar todas las décadas que el modelo utiliza. El modelo guarda siete columnas de década por nombre, desde la década de 1940 hasta la de 2000, pondera las décadas por sus recuentos, elige una, elige un año dentro de ella y lo convierte en una edad.

Cómo eran los datos en realidad

La cobertura no es uniforme de un país a otro, porque las oficinas nacionales de estadística no empezaron a publicar recuentos de nombres al mismo tiempo. Medido directamente a partir de los ficheros — una década cuenta como cubierta si algún nombre del locale tiene un valor distinto de cero en esa columna —, la mayoría de los locales que llevan datos de cohorte cubren las siete décadas.

Dos no lo hacen. La serie austriaca empieza en 1984, de modo que sus cuatro columnas de década más antiguas están vacías en todas las filas del fichero; la serie italiana cubre de 1999 en adelante, de modo que seis de sus siete lo están. Esas columnas contienen ceros — no porque no naciera nadie en Italia en la década de 1960, sino porque los recuentos publicados no llegan tan atrás.

El bug cabe en una línea de aritmética

La selección es una extracción ponderada corriente sobre las siete décadas:

total = sum(decades)
r     = random(1, total)
acc   = 0
for i in 0..6:
    acc = acc + decades[i]
    if r <= acc:
        chosen = decade[i]
        break

No hay nada erróneo en este código. Es la implementación correcta de «ponderar las décadas por sus recuentos». El problema está por entero en lo que los recuentos significan.

Cuando solo la última columna es distinta de cero, el acumulador se queda en cero durante seis iteraciones. La condición r <= acc no puede satisfacerse hasta que el bucle alcanza la única columna que lleva masa. No es que la séptima década sea muy probable; es que las otras seis son inalcanzables, estructuralmente, para cualquier valor posible de r. El resultado es del 100 % y lo es por construcción, no por azar.

Resimulando ambas variantes sobre 10.000 extracciones cada una, con independencia de la implementación entregada:

LocaleNombreDécadas cubiertasVersión19–2526–3536–4546–57MedianaIntervalo
it_ITGiuseppe1antes100,0 %0,0 %0,0 %0,0 %2119–25
it_ITGiuseppe1después30,3 %22,2 %21,6 %25,9 %3419–57
it_ITGiulia1antes100,0 %0,0 %0,0 %0,0 %2119–25
it_ITGiulia1después29,2 %21,6 %22,8 %26,4 %3519–57
de_ATJohann3antes12,4 %31,1 %56,4 %0,0 %3719–45
de_ATJohann3después15,4 %28,6 %39,0 %17,0 %3719–57
de_ATEmma3antes92,7 %7,1 %0,2 %0,0 %2119–45
de_ATEmma3después50,5 %17,6 %14,6 %17,2 %2519–57
no_NOBjørn7antes1,6 %4,3 %8,6 %85,5 %5719–57
no_NOBjørn7después1,6 %4,3 %8,6 %85,5 %5719–57

Dos cosas merecen leerse en esa tabla.

La columna austriaca es el mismo bug con menor amplitud. Nadie llamado Johann, Lukas, Maria o Emma podía tener más de 45 años — un techo duro, producido por el mismo mecanismo, instalado en los datos desde que se construyeron las filas austriacas. Parecía lo bastante plausible como para que nadie lo cuestionara. Solo cuando el caso italiano degeneró hasta el 100 % se hizo visible la forma, y entonces el caso austriaco era evidentemente lo mismo.

El defecto escala de forma inversa a la calidad de la fuente. Siete décadas cubiertas: correcto. Tres cubiertas: ninguna ficha de más de 45 años. Una cubierta: todo el mundo tiene 21. Cuanto menos sabe la fuente, con más aplomo afirma el modelo. Eso es exactamente al revés, y es la firma de unos datos ausentes leídos como evidencia.

El cero tiene dos significados y el fichero no puede decirle cuál

Vale la pena ponerle nombre a la forma general de esto, porque aparece mucho más allá de los datos de nombres.

Un cero en una columna de recuentos puede significar:

  1. Un cero observado. Miramos, y no había ninguno. Eso es información, y ponderar con ello es correcto.
  2. Una celda no observada. No miramos aquí, o el instrumento no llega hasta ahí. Eso es la ausencia de información, y ponderar con ello afirma algo que los datos nunca dijeron.

Los estadísticos distinguen un cero estructural (una celda que lógicamente no puede ser distinta de cero) de un cero de muestreo (una celda que podría ser distinta de cero pero no fue observada), y todo el aparato de los métodos para datos ausentes existe porque tratar el segundo como el primero sesga todo lo que viene después. Nuestro caso es del segundo tipo, en su forma más pura: las celdas no están simplemente no observadas, están sistemáticamente no observadas, todas en un extremo del intervalo, para todas las filas a la vez.

Si esto sobrevive a la revisión es porque la mayoría de los formatos tabulares no tienen manera de expresar la diferencia. Un fichero de enteros separados por tabuladores no tiene NULL. Tampoco lo tiene una columna binaria de ancho fijo, un uint32 de protobuf o un array de enteros de NumPy. En el momento en que usted serializa «no lo sabemos» en una columna de enteros, se convierte en cero, y cero es un número perfectamente válido que la aritmética consumirá sin pestañear.

Se ve la misma colisión por todas partes en cuanto uno la busca:

DominioCero que significa «ninguno»Cero que significa «sin datos»
Analítica del comercio minoristaEsta tienda no vendió nada hoyEsta tienda aún no había abierto
Telemetría de sensoresEl depósito está vacíoEl sensor estaba desconectado
Analítica webNinguna visita desde este paísEl rastreador estaba bloqueado en este país
EpidemiologíaNingún caso en este distritoEste distrito no notificó
Información financieraEste segmento no tuvo ingresosEste segmento no se desglosó en las cuentas de aquel año
Frecuencia de nombresNadie recibió este nombreEl registro no publica tan atrás

En cada fila, un agregado calculado sobre el significado de la izquierda es correcto, y ese mismo agregado sobre el significado de la derecha es erróneo con aplomo y en una dirección concreta: concentra el resultado sobre aquello que se observó.

Por qué ningún validador lo detectó

Los ficheros de cohorte pasan todas las comprobaciones estructurales que existen. Número de columnas correcto, orden de clasificación correcto, UTF-8 sin marca de orden de bytes, ninguna fila duplicada, sumas dentro de los límites, todos los valores enteros no negativos. Una fila de ceros no tiene nada de malformado — una fila de ceros es exactamente lo que un fichero de recuentos honesto contiene para un nombre raro en una década en la que era raro.

El defecto existe únicamente en la distribución de la salida, y solo cuando se compara con una expectativa que vive en la cabeza de alguien y no en un fichero. «Nadie de más de 45 años» no es una propiedad de la entrada; es una propiedad de lo que el modelo hace con la entrada, y solo se puede ver ejecutando el modelo y mirando lo que sale.

Esa es la misma forma que una comprobación que informa de éxito sin leer su entrada — el fallo es invisible porque el artefacto está bien formado y el proceso llegó hasta el final. Hemos escrito largo y tendido sobre ese patrón en La comprobación que se queda ciega justo donde usted la necesita; esto es su gemelo del lado de los datos. La lección se traslada directamente: valide la distribución de la salida, no el esquema de la entrada. Una comprobación de esquema le dice que el fichero es legible. No puede decirle que el fichero se esté leyendo con el significado que usted cree.

Hay una comprobación que lo habría detectado de inmediato, y cuesta unas cinco líneas:

for each column c in the reference table:
    if sum(c over all rows) == 0:
        warn("column c is empty across the entire dataset")

Un cero en una sola fila es normal. Un cero en una columna entera, en todas las filas, nunca es un hecho sobre el mundo — es un hecho sobre su fuente. Esa distinción constituye por sí sola la corrección.

La distinción que hace difícil la corrección

La reparación ingenua — «tratar los ceros como desconocidos» — destruye justamente aquello para lo que el modelo existe.

En un locale con cobertura completa, los ceros individuales son reales y portan carga. Chloe en Gran Bretaña tiene cuatro décadas no vacías de siete; las tres vacías son auténticas, porque prácticamente nadie en Gran Bretaña se llamaba Chloe en la década de 1940. Ese cero es exactamente la señal que hace que el modelo diga «Chloe es joven», y suavizarlo aplanaría todos los nombres de todos los locales completos de vuelta hacia lo uniforme.

Así que los dos ceros se distinguen no por su valor, ni por su fila, sino por su ámbito:

  • Un cero en una fila, en una columna que otras filas sí rellenan → real. El nombre era raro esa década.
  • Un cero en una fila, en una columna vacía para todas las filas → no es una medición. La fuente se detiene ahí.

La segunda condición solo puede evaluarse a escala del conjunto de datos entero. Ninguna inspección de una fila aislada, por minuciosa que sea, puede decirle ante qué tipo de cero se encuentra, y por eso la comprobación tiene que ser un agregado sobre toda la columna y por eso no se deriva de ninguna validación a nivel de fila.

La corrección, en su forma general

Calcule cuántas de las columnas cubre realmente la fuente y reparta después la masa de probabilidad entre «lo que los datos saben» y «lo que no saben», en proporción a esa cobertura:

La regla necesita un número que la propia fila no puede suministrar: cuántas de las columnas de década cubre la fuente siquiera, contado sobre el locale entero y no sobre la fila. Reparta la masa de probabilidad en proporción a esa cobertura. Las columnas cubiertas conservan la parte que les toca; el resto va a un único compartimento desconocido, que representa las décadas que la fuente no puede ver. Extraiga una sola vez sobre los dos en conjunto. Si cae en el compartimento desconocido, la respuesta es exactamente la que el sistema ya da para un locale sin ningún dato de cohorte — una edad uniforme sobre toda la ventana. Si cae en los datos, es la extracción ponderada corriente sobre las décadas, intacta.

Cuatro propiedades hacen de esta la versión que conservamos:

Es exacta en números enteros. El reparto enfrenta columnas enteras contra columnas enteras, de modo que la masa cubierta y la masa desconocida siempre vuelven a sumar el mismo total, sean cuales sean los recuentos del nombre. Sin redondeo, sin coma flotante, sin «basta con poner un 1 en las celdas vacías».

Degenera en el comportamiento antiguo con cobertura completa. Cuando todas las columnas están cubiertas, el compartimento desconocido está vacío y la aritmética no es meramente equivalente: es literalmente la misma expresión. Lo verificamos sobre 640.000 comparaciones frente a una copia literal de la implementación anterior: ocho locales completos, cero divergencias. Esto importa más de lo que parece — una corrección para dos locales que perturbe en silencio los otros ocho no es una corrección.

Se repliega sobre la referencia honesta. El compartimento desconocido no inventa una distribución; produce exactamente el comportamiento que el sistema ya tiene para un locale sin ningún dato de cohorte. Ese repliegue no es un modelo nuevo, es la ausencia de modelo, que es lo correcto que cabe decir de los años que la fuente no puede ver.

Escala con lo que se conoce. Las salidas esperadas se derivan únicamente de la proporción de cobertura. Italia cubre una década de siete, así que seis séptimas partes de la masa caen en el compartimento uniforme y una séptima en los datos; frente a una ventana de edad de 19–57 y una década cubierta que solo puede producir edades de 19 a 25 años, eso predice un 29,7 % de fichas en la banda 19–25, y el valor medido es del 29,1–30,3 %. La misma aritmética predice un 26,4 % en la banda 46–57 frente a un 25,9–26,6 % medido, y para Austria — tres décadas de siete — un 17,6 % frente a un 17,0–17,5 % medido.

Tres alternativas, y por qué pierden

Escribir un 1 en cada celda vacía. La solución evidente, de una sola línea. No hace nada: las masas de cohorte reales se cuentan por centenares de miles, así que un 1 entre ellas no es una corrección pequeña, es ninguna corrección. Vale la pena decirlo porque es lo primero que todo el mundo propone.

Repartir la masa ausente entre las décadas vacías. Esta es la imputación intuitivamente «correcta», y se midió que resultaba activamente perjudicial. Las décadas vacías de Italia van de la de 1940 a la de 1980, y frente a una ventana de edad de 19–57 casi todos esos años de nacimiento caen fuera de la ventana. Tras el acotamiento, aproximadamente el 40 % de las fichas habría salido exactamente con la edad máxima. Un artefacto llamativo cambiado por otro. El compartimento de edad uniforme no tiene, por construcción, ningún comportamiento de borde de ese tipo.

Leer la lista de décadas vacías desde la configuración del validador. Tentador, porque el validador ya lleva una lista elaborada a mano del tipo «vacía conocida, y esta es la razón». Pero ese fichero es un artefacto de tiempo de compilación y no se entrega, y duplicar una configuración dentro del código de ejecución significa que alguien tiene que mantener dos copias sincronizadas para siempre. Contar la cobertura a partir de los propios datos significa que los datos se describen a sí mismos y que no hay nada que sincronizar.

Esa última elección tiene un coste honesto, y conviene dejarlo escrito antes que descubrirlo: como ahora el código de ejecución infiere la cobertura de los datos, un futuro fallo del pipeline que elimine en silencio una década entera quedará suavizado en tiempo de ejecución en lugar de aparecer como una salida rota. La responsabilidad de detectarlo no ha desaparecido — recae en el validador de tiempo de compilación, que sigue avisando de cada columna vacía sin explicación. Mover un fallo de un sitio a otro está bien. Perder la pista de qué sitio es, no.

Lo que la corrección no hace

No crea información. Esta es la parte sobre la que conviene ser rotundo.

Antes del cambio, los ocho nombres de prueba del locale italiano producían secuencias de edades idénticas byte a byte. Con una sola década cubierta, los datos de cohorte no pueden distinguir en absoluto los nombres por edad — no hay ninguna señal que recuperar. La elección nunca fue entre señal y ruido. Fue entre «todos los italianos tienen 21» y «una dispersión plausible sin ninguna señal de edad dentro», y la segunda solo es mejor porque la primera es ostensiblemente falsa.

La señal genuinamente útil en una cohorte de una sola década no es la edad en absoluto — es el género. Un nombre que aparece de forma abrumadora en la columna femenina es femenino con independencia de la década de la que procedan los recuentos, y esa inferencia se conserva íntegra. Antes de que existieran los datos de cohorte, el género de un nombre buscado era un cara o cruz, que es un error mucho más visible que una edad implausible: «Giulia, masculino» salta a la vista de un modo que «Giuseppe, 23» no.

Así que la contabilidad honesta es esta: la inferencia de género lo ganó todo, la inferencia de edad no ganó nada pero dejó de mentir, y los ocho locales con datos completos no cambiaron ni un bit.

La lista de comprobación

  1. Para cada columna de una tabla de referencia, compruebe si está vacía en todo el conjunto de datos. Un cero a nivel de fila es un dato. Un cero a nivel de columna es un hecho sobre su fuente.
  2. Nunca deje que «no observado» se serialice en el mismo valor que «cero observado» sin dejar constancia de la diferencia en alguna parte. Las columnas de enteros no tienen NULL; si el formato no puede llevarla, el mapa de cobertura tiene que vivir junto al fichero.
  3. Pruebe el invariante de que una fuente más pobre produce una salida más ancha, no más estrecha. Si su modelo gana confianza cuando le da menos datos, es que los datos ausentes se están leyendo como evidencia. Es una prueba de propiedad de una sola línea y detecta toda la clase.
  4. Valide la distribución de la salida, no el esquema de la entrada. Todas las comprobaciones estructurales pasaron sobre ficheros que producían una respuesta degenerada para 3.189 nombres.
  5. Busque intervalos degenerados. Un mínimo igual al máximo, o un techo duro que no se corresponde con ninguna frontera del mundo real, es la huella visible. «Ninguna ficha tiene nunca más de 45 años» estuvo en pantalla todo el tiempo que existieron los datos austriacos.
  6. Distinga el ámbito al decidir si un cero es real. El mismo valor es una medición en un locale y un artefacto en otro; solo el agregado sobre toda la columna puede decirle cuál.
  7. Prefiera replegarse sobre la referencia honesta antes que imputar una forma. Repartir la masa entre las celdas vacías parecía riguroso y colocó el 40 % de los resultados sobre un único valor límite.
  8. Demuestre que la corrección es inerte allí donde debe serlo. Dos locales necesitaban cambiar y ocho no; «ocho sin cambios» se estableció por comparación directa con la implementación antigua, no se dio por supuesto.
  9. Cuando el código de ejecución empieza a inferir algo que el pipeline garantizaba antes, deje escrito quién detecta ahora el fallo del pipeline. El suavizado es una virtud para las fuentes malas y una máscara para las compilaciones malas.

El bug tenía una causa de una sola línea y un síntoma visible y absurdo, y aun así sobrevivió porque todo lo que lo rodeaba estaba bien formado. Los ficheros eran válidos, el bucle era correcto, las pruebas estaban en verde, y la respuesta era que todos los italianos de la base de datos tenían veintiún años.

Para el defecto gemelo — donde una lista formalmente ponderada resultó estar medio uniforme, y todas las métricas agregadas coincidían en que estaba bien — véase La mitad de su lista ponderada no está ponderada. Para lo que hace el modelo de edad cuando los datos que hay detrás están completos, véase Los nombres por generación. Para las comprobaciones que informan de éxito sin haber mirado nunca, véase La comprobación que se queda ciega justo donde usted la necesita. Para los datos subyacentes y sus lagunas, véase Capas migratorias en las cohortes de nacimiento.


Datos actualizados a 2026-07-21

Todas las cifras se midieron el 21 de julio de 2026. La cobertura por décadas se calculó directamente a partir de los ficheros de cohorte en disco. Las distribuciones de edad de antes y después se volvieron a derivar para este artículo mediante una reimplementación independiente tanto de la antigua como de la nueva regla de selección, ejecutada sobre los mismos datos con semilla fija, en lugar de citarse del historial de cambios; la reproducen exactamente.

Fuentes y notas:

  • Cobertura — una columna de década cuenta como cubierta si su suma sobre todas las filas del locale es mayor que cero, tomando juntos los dos ficheros de género. Ocho locales cubren 7 de 7; de_AT cubre 3 de 7 (1981, 1991, 2001 rellenas; 1941, 1951, 1961, 1971 vacías); it_IT cubre 1 de 7 (solo 2001). La cobertura no difiere entre el fichero masculino y el femenino en ningún locale.
  • Italia — 3.189 filas en los datos de cohorte, 1.667 masculinas y 1.522 femeninas, que suman 2.425.502 y 2.283.897 recuentos respectivamente, todos en la columna 2001–2010. La serie subyacente son los recuentos de nombres de pila del ISTAT, que empiezan en 1999.
  • Austria — 17.434 filas, 8.159 masculinas y 9.275 femeninas. Sumas por década, masculino: 0, 0, 0, 0, 296.553, 388.347, 346.755. Femenino: 0, 0, 0, 0, 281.455, 368.831, 329.587. La serie nacional empieza en 1984, y por eso las cuatro primeras columnas están vacías.
  • Mediciones de edad — 10.000 extracciones por nombre, semilla fija, ventana de edad 19–57, año base 2026. Las cuatro bandas son 19–25, 26–35, 36–45, 46–57. Nombres italianos antes de la corrección: 100,0 % en 19–25, mediana 21, intervalo 19–25, para todos los nombres probados. Después: 29,1–30,3 % en 19–25, medianas 34–35, intervalo 19–57. Nombres austriacos antes: 0,0 % en 46–57 para todos los nombres, edad máxima 45. Después: 17,0–17,5 % en 46–57, máximo 57.
  • Los locales completos no cambianno_NO y en_US producen proporciones de banda idénticas y medianas idénticas bajo ambas reglas en la resimulación anterior. En la implementación entregada esto se estableció con más solidez, ejecutando una copia literal del código anterior contra el nuevo sobre 400 nombres por locale × 4 ventanas de edad × 40 semillas = 64.000 comparaciones por locale, 640.000 en total, con cero divergencias en los ocho locales completos.
  • La cifra del 40 % — la consecuencia medida de la alternativa rechazada en la que la masa ausente se reparte entre las columnas de década vacías en lugar de verterse en un único compartimento uniforme. Las décadas vacías de Italia son 1941–1990; frente a una ventana 19–57 con año base 2026, esos años de nacimiento corresponden a edades de 36 en adelante, la mayoría por encima de 57, de modo que el acotamiento los amontona sobre el límite superior.
  • Predicción frente a medición — la proporción esperada de una banda es la fracción no cubierta de las décadas multiplicada por la proporción que la banda ocupa en la ventana de edad, más la fracción cubierta multiplicada por la proporción que producen los propios datos. Para Italia esto da un 29,7 % para 19–25 y un 26,4 % para 46–57 frente a mediciones del 29,1–30,3 % y del 25,9–26,6 %. Para Austria da un 17,6 % para 46–57 frente a un 17,0–17,5 %.
  • La afirmación sobre el género — antes de que existieran datos de cohorte para un locale, el género asociado a un nombre buscado se extraía al azar. Con datos de cohorte de una sola década se infiere de qué fichero lleva la mayor masa, lo que es correcto para los 3.189 nombres italianos con independencia de la degeneración de la edad. Esto se enuncia como una propiedad de diseño, no como una cifra de precisión medida.
  • Ceros estructurales frente a ceros de muestreo — la terminología es estándar en el análisis de datos categóricos y se emplea aquí por su precisión, no como afirmación de que nuestro caso sea una tabla de contingencia de manual. Nuestros ceros son celdas sistemáticamente no observadas, que es la variante más difícil, puesto que la ausencia de observación está perfectamente correlacionada con la variable de interés.

← Artículos