--- name: deduplicate_entities kind: function lang: py domain: datascience version: "1.0.0" purity: pure signature: "def deduplicate_entities(candidates: list[EntityCandidate], name_threshold: float = 0.85, same_type_only: bool = True) -> DeduplicationResult" description: "Agrupa entidades candidatas que refieren a la misma entidad real usando fuzzy matching de nombres (Levenshtein + Jaccard) y Union-Find para clusters transitivos. Retorna entidades mergeadas con mapas de resolucion de IDs y log de merges." tags: [deduplication, entity, fuzzy, levenshtein, jaccard, union-find, knowledge-graph, nlp, fuzzygraph, datascience] uses_functions: - normalize_entity_name_py_core - merge_entity_attributes_py_core uses_types: - entity_candidate_py_datascience - deduplication_result_py_datascience returns: [deduplication_result_py_datascience] returns_optional: false error_type: "" imports: - uuid params: - name: candidates desc: "lista de EntityCandidate a deduplicar. Cada uno tiene name, type_ref, confidence." - name: name_threshold desc: "umbral de similitud para fuzzy matching de nombres en rango [0, 1] (tipico: 0.85). Mayor = mas estricto." - name: same_type_only desc: "si True, solo mergea candidatos con el mismo type_ref. Si False, mergea por similitud ignoring tipo." output: "DeduplicationResult con entidades mergeadas, mapas de resolucion de IDs (name_to_id) y log de merges realizados" tested: true tests: - "John Smith y Smith, John se mergean" - "Google y Google LLC se mergean" - "192.168.1.1 y 192.168.1.1 se mergean por matching exacto" - "John Smith (person) y John Smith (organization) NO se mergean" - "Clusters transitivos: A~B, B~C -> {A, B, C} en un solo cluster" - "Entidades sin duplicados pasan sin modificacion" - "Confidence toma el max del cluster; atributos se fusionan" - "Lista vacia retorna resultado vacio" - "name_to_id contiene todos los nombres originales del cluster" test_file_path: "python/functions/datascience/deduplicate_entities_test.py" file_path: "python/functions/datascience/deduplicate_entities.py" --- ## Ejemplo ```python from python.types.datascience.entity_candidate import EntityCandidate from python.functions.datascience.deduplicate_entities import deduplicate_entities candidates = [ EntityCandidate(name="John Smith", type_ref="person", confidence=0.9), EntityCandidate(name="Smith, John", type_ref="person", confidence=0.85), EntityCandidate(name="Google", type_ref="organization", confidence=0.95), EntityCandidate(name="Google LLC", type_ref="organization", confidence=0.88), ] result = deduplicate_entities(candidates, name_threshold=0.85, same_type_only=True) # result.total_before = 4 # result.total_after = 2 # result.merge_log = [ # {"canonical": "John Smith", "merged": ["Smith, John"], "score": 0.91, "reason": "fuzzy_name"}, # {"canonical": "Google", "merged": ["Google LLC"], "score": 0.89, "reason": "fuzzy_name"}, # ] ``` ## Algoritmo 1. **Normalizar nombres** usando `normalize_entity_name()` sobre cada candidato segun su `type_ref` 2. **Comparacion pairwise** dentro del mismo tipo (si `same_type_only=True`): - Para tipos tecnicos (ip, email, domain, crypto_wallet, phone): matching exacto normalizado - Para el resto: `score = max(levenshtein_sim, jaccard_sim)` + bonus por contencion (+0.3) y acronimos (+0.3) 3. **Union-Find** para clusters transitivos: si A~B y B~C, entonces {A, B, C} forman un cluster 4. **Merge por cluster:** - Nombre canonico: candidato con mayor `confidence` - Atributos: `merge_entity_attributes()` sobre todos los candidatos del cluster - Confidence: `max` del cluster - Source chunks: union de todos los candidatos - `merged_from`: union de todos los nombres originales ## Heuristicas de similitud de nombres | Heuristica | Efecto | |---|---| | Levenshtein | `1 - (edit_distance / max_len)` | | Jaccard sobre tokens | `\|A ∩ B\| / \|A ∪ B\|` | | Score base | `max(lev_sim, jaccard_sim)` | | Contencion (a in b o b in a) | `+0.3` hasta max 1.0 | | Acronimo ("FBI" ~ "Federal Bureau of Investigation") | `+0.3` hasta max 1.0 | | Tipos exactos (ip/email/domain) | solo matching exacto, ignora umbral | ## Complejidad - Pairwise: O(N^2) — aceptable para <1000 entidades (tipico por documento) - Union-Find con path compression: O(α(N)) amortizado por operacion - Para escalar a >1000: pre-filtrar por primera letra o n-gram index antes de comparar ## Notas Funcion pura. Implementa Levenshtein y Jaccard internamente para evitar dependencias externas a este modulo. Las funciones del registry `levenshtein_distance_py_cybersecurity` y `jaccard_similarity_py_cybersecurity` son equivalentes pero requieren imports adicionales — la implementacion inline mantiene la funcion sin dependencias de stdlib. El `name_to_id` del resultado es el mapa de resolucion principal para la fase de deduplicacion de relaciones: permite resolver cualquier variante de nombre de una entidad a su ID canonico.