0026 — Deduplicación fuzzy determinista con rapidfuzz (splink diferido)¶
Supersedida en parte por 0031 (2026-06-18, #88): el algoritmo de este ADR (token_sort_ratio + Union-Find + canónico más-frecuente, determinista) sigue vigente, pero su invocación cambia: el dedup deja de ser "función de librería sin subcomando CLI" y pasa a ejecutarse automáticamente en la ingesta (sobre el corpus completo mergeado, cross-biblioteca), y
rapidfuzzdeja el extra[dedup]para ir al núcleo. El cuerpo de abajo queda como historia inmutable del corte del Hito 7. Ver 0031.
- Estado: Aceptada · AS-BUILT (2026-06-16) (Hito 7) · supersedida en parte por
0031 (2026-06-18): invocación del dedup
(automático en ingesta) y
rapidfuzzal núcleo; el algoritmo sigue vigente - Fecha: 2026-06-16
- Decidido por: IA (Claude Opus 4.8), validado por el Product Owner proxy
(ver
registro-ia.md) - Relacionada con: 0011 (thesaurus determinista; el dedup fuzzy
es el complemento que su enmienda dejaba abierto solo si es determinista),
0017 (reproducibilidad por historia auditable; el
dedup registra un
PreprocRef), 0022 (el producto no usa IA generativa: el dedup es determinista por similitud de cadenas, no semántico/LLM/embeddings), 0005 (núcleo liviano + extras con import perezoso; el extra[dedup]pasa arapidfuzz, ver su enmienda)
Contexto¶
El Hito 7 (ROADMAP/04) materializa la deduplicación fuzzy de
authors_id y keywords_id: el complemento aproximado de la normalización conservadora del
Preprocessor (ADR 0011, Hito 5), que solo colapsa variantes triviales (lowercase, acentos,
espacios) y matchea keywords contra el thesaurus curado. Lo que queda son variantes casi-iguales
que ni la normalización ni el thesaurus capturan ("J. Smith" vs "John Smith"; "ecological exchange"
vs "ecologic exchange").
El [dedup] original (ADR 0005, v0) listaba fuzzywuzzy + python-levenshtein. fuzzywuzzy está
abandonado (sin releases, licencia GPL); su sucesor mantenido y permisivo (MIT) es rapidfuzz.
La enmienda de 2026-06-15 del ADR 0011 (raíz del retiro de IA, ADR 0022) ya dejó dicho que el único
fallback aceptado para ampliar cobertura es fuzzy determinista (rapidfuzz), nunca
semántico/LLM. Faltaba decidir el contrato y el algoritmo concretos, y descartar formalmente
splink.
Decisión¶
El dedup fuzzy es determinista y vive en el extra [dedup] = rapidfuzz>=3,<4 (import
perezoso). Se expone como función de librería (no subcomando CLI — decisión del PO), exportada
desde bib2graph.preprocessors:
def deduplicate_authors(corpus: Corpus, *, threshold: float = 0.92) -> Corpus: ...
def deduplicate_keywords(corpus: Corpus, *, threshold: float = 0.90) -> Corpus: ...
thresholdpor-campo (autores 0.92 / keywords 0.90; ambas reciben el parámetro): los nombres de autor toleran menos ruido que las keywords.- Operan sobre la columna
_id(authors_id/keywords_id), nunca sobre_raw(el crudo es historia inmutable). Corren después denormalizeyapply_thesaurus(normalize → thesaurus → dedup): el fuzzy refina lo que lo determinístico no colapsó. rapidfuzz, nosplink.rapidfuzzes liviano, MIT, sin descarga de modelos y determinista (mismo input → mismo score).splinkes probabilístico (EM / Fellegi-Sunter), introduce no-determinismo (estimación de parámetros) y arrastra dependencias pesadas (Spark/DuckDB ML); queda diferido a post-V1 en un ADR aparte, si aparece un caso de record-linkage entre fuentes que lo justifique. El dedup de V1 no es record-linkage probabilístico: es colapso de variantes de cadena con un umbral fijo.
Algoritmo determinista (AS-BUILT)¶
- Scorer:
rapidfuzz.fuzz.token_sort_ratio(0–100), comparado contrathreshold * 100. - Clustering: los pares con score ≥ umbral forman aristas; los clusters son las componentes
conexas vía Union-Find (raíz lexicográfica, iteración en orden → determinista, insensible a
PYTHONHASHSEED). - Canónico del cluster: la variante más frecuente (en nº de papers distintos), con
desempate por
idascendente (min(key=(-freq, v))). - Remapeo: solo la columna
_id; preserva el orden de primera aparición de cada lista; nunca toca_raw.
Esto garantiza determinismo (mismo corpus + threshold + versión de rapidfuzz → mismo
resultado; verificado cross-PYTHONHASHSEED) e idempotencia (converge en una pasada). El import
del extra es perezoso: sin [dedup], un ImportError accionable apunta a uv sync --extra dedup.
Reproducibilidad (ADR 0017)¶
Cada función registra un PreprocRef en el Manifest con
{library, rapidfuzz_version, scorer, threshold, n_clusters_collapsed} — la versión de rapidfuzz
forma parte de la procedencia, porque el resultado es reproducible a igual versión del scorer.
Campos en V1¶
Autores + keywords. Las instituciones quedan diferidas: institutions_id no está
normalizada determinísticamente hoy (no hay un normalize de instituciones que el fuzzy pueda
refinar sin riesgo); se difiere a un hito posterior.
Consecuencias¶
- (+) Redes de autor y keyword limpias de duplicados aproximados sin abandonar el determinismo ni la reproducibilidad (ADR 0017/0022): el dedup es estructura, no IA.
- (+) Núcleo liviano intacto:
rapidfuzzsolo se instala con[dedup]; el import perezoso evita acoplar el núcleo (ADR 0005). - (+)
_rawpreservado → el colapso es auditable y reversible re-derivando desde el crudo. - (−) Cobertura por umbral, no exhaustiva: un threshold conservador deja pasar variantes muy
divergentes; subirlo arriesga falsos positivos. El
thresholdpor-campo es la perilla. - (−)
splink(record-linkage probabilístico) queda fuera de V1: si más adelante hace falta enlazar identidades entre fuentes heterogéneas, requiere un ADR nuevo (y aceptar el no-determinismo o seedearlo explícitamente).
AS-BUILT (2026-06-16):
deduplicate_authors/deduplicate_keywordsensrc/bib2graph/preprocessors/dedup.py, exportadas desdebib2graph.preprocessors, extra[dedup] = rapidfuzz>=3,<4. Algoritmotoken_sort_ratio+ Union-Find + canónico más-frecuente/desempate-id. 388 tests verdes (mypy/ruff limpios; núcleo sin importarrapidfuzz).