Diagrama del Pipeline NER - Entity Extraction¶
Descripción¶
Diagrama detallado del pipeline de extracción de entidades (NER) que muestra el flujo desde el texto OCR hasta las entidades validadas, incluyendo el EntityExtractorEnsemble y la cadena de validadores.
Diagrama Principal¶
flowchart TB
subgraph Input["📥 Entrada"]
Text[Texto OCR<br/>contenido del documento]
end
subgraph SpaCyAdapter["🏷️ SpaCyAdapter (Orquestador)"]
Load[Cargar Modelo<br/>es_core_news_lg]
Doc[Crear spacy.Doc]
end
subgraph Extractors["🔍 Extractors (Strategy Pattern)"]
Marker[MarkerExtractor<br/>priority=1]
SpaCyNER[SpaCyNERExtractor<br/>priority=2]
Regex[RegexExtractor<br/>priority=3]
Address[AddressExtractor<br/>priority=4<br/>no ensemble vote]
Contact[ContactExtractor<br/>priority=5<br/>no ensemble vote]
end
subgraph Ensemble["⚖️ EntityExtractorEnsemble"]
Collect[Recopilar Candidatos<br/>por tipo de entidad]
Voting[Votación Ponderada<br/>weighted_vote]
Resolve[Resolver Conflictos<br/>highest_confidence]
end
subgraph Validators["✅ Validators (Chain of Responsibility)"]
ValChain[ValidatorChain]
Structural[StructuralValidator<br/>boost por sección]
Temporal[TemporalValidator<br/>consistencia fechas]
RadFormat[RadicadoFormatValidator<br/>formato 23 dígitos]
Dedup[DeduplicationValidator<br/>fuzzy matching]
end
subgraph Utilities["🛠️ Utilities"]
Truncator[EntityTruncator<br/>límite 100 chars]
CtxScorer[ContextScorer<br/>clasificación contextual]
EntLinker[EntityLinker<br/>agrupación fuzzy]
RadVal[RadicadoValidator<br/>códigos DANE]
DocStruct[DocumentStructureAnalyzer<br/>detección secciones]
end
subgraph Output["📤 Salida"]
EntityResult[EntityResult<br/>demandante, demandado<br/>radicado, juzgado, fecha]
end
Text --> Load --> Doc
Doc --> Marker & SpaCyNER & Regex & Address & Contact
Marker --> Collect
SpaCyNER --> Collect
Regex --> Collect
Address --> Collect
Contact --> Collect
Collect --> Voting --> Resolve
Resolve --> ValChain
ValChain --> Structural --> Temporal --> RadFormat --> Dedup
Marker -.uses.-> Truncator
SpaCyNER -.uses.-> CtxScorer
Dedup -.uses.-> EntLinker
RadFormat -.uses.-> RadVal
Structural -.uses.-> DocStruct
Dedup --> EntityResult
style Input fill:#e3f2fd
style Ensemble fill:#fff3e0
style Validators fill:#e8f5e9
style Output fill:#f3e5f5
Extractores (Strategy Pattern)¶
MarkerExtractor (priority=1, weight=0.70)¶
Responsabilidad: Extraer entidades de marcadores explícitos en el documento.
Marcadores soportados:
ACCIONANTE:, DEMANDANTE:, ACTOR:
ACCIONADO:, DEMANDADO:, ENTIDAD DEMANDADA:
RADICACIÓN:, RADICADO No.:, No. RADICADO:
JUZGADO:, CORPORACIÓN:, DESPACHO:
Proceso:
1. Buscar patrón MARCADOR: valor
2. Extraer texto después del marcador
3. Truncar con EntityTruncator
4. Asignar confidence=0.95 (alta confianza)
SpaCyNERExtractor (priority=2)¶
Responsabilidad: Extracción neuronal usando modelo SpaCy.
Labels utilizados:
- PER → demandante/demandado (requiere clasificación)
- ORG → demandado (entidades corporativas)
- LOC → direcciones
Proceso: 1. Ejecutar NER sobre spacy.Doc 2. Clasificar entidades PER como demandante/demandado 3. Aplicar ContextScorer para validar 4. Filtrar por Part-of-Speech (POS)
Clasificación contextual:
# Palabras clave positivas para demandante
PLAINTIFF_KEYWORDS = ["accionante", "demandante", "actor", "solicita"]
# Palabras clave positivas para demandado
DEFENDANT_KEYWORDS = ["accionado", "demandado", "entidad", "contra"]
RegexExtractor (priority=3, weight=0.70)¶
Responsabilidad: Extracción de entidades estructuradas mediante patrones regex.
Patrones:
| Entidad | Patrón | Ejemplo |
|---|---|---|
| Radicado | \d{23} |
11001020300020260001200 |
| Fecha | \d{1,2}/\d{1,2}/\d{4} |
15/02/2026 |
| Juzgado | Juzgado\s+\d+\s+\w+ |
Juzgado 12 Civil |
EntityExtractorEnsemble¶
Recopilación de Candidatos¶
def _collect_candidates(self, text: str) -> Dict[str, List[Entity]]:
"""Recopila candidatos de todos los extractores."""
candidates = defaultdict(list)
for extractor in self._extractors:
entities = extractor.extract(text)
for entity in entities:
candidates[entity.label].append(entity)
return candidates
Votación Ponderada¶
def _weighted_vote(self, candidates: List[Entity]) -> Entity:
"""Selecciona el mejor candidato por votación ponderada."""
scores = {}
for candidate in candidates:
weight = self._weights.get(candidate.source, 0.5)
score = candidate.confidence * weight
scores[candidate.text] = scores.get(candidate.text, 0) + score
best_text = max(scores, key=scores.get)
return next(c for c in candidates if c.text == best_text)
AddressExtractor (priority=4, weight=0.65)¶
Responsabilidad: Extraer direcciones de demandante y demandado.
Proceso: 1. Detectar patrones de dirección colombiana (Calle, Carrera, Avenida, etc.) 2. Clasificar como dirección de demandante o demandado por contexto 3. Asignar confidence según proximidad a marcadores
ContactExtractor (priority=5, weight=0.80)¶
Responsabilidad: Extraer correo electrónico y cédula (CC/CE/NIT).
Patrones:
- Email: regex estándar de correo electrónico
- Cédula: patrones C.C., CC, CE, NIT seguidos de número
Pesos por Extractor (Ensemble Voting)¶
Los pesos varían según el tipo de entidad. Solo 3 extractores participan en el ensemble voting:
| Extractor | Peso (persona) | Peso (estructurado) | Nota |
|---|---|---|---|
| MarkerExtractor | 0.70 | 0.20 | Alta precisión con marcadores explícitos |
| SpaCyNERExtractor | 0.20 | 0.10 | Menor peso por ruido en documentos legales |
| RegexExtractor | 0.10 | 0.70 | Alta precisión para radicados/fechas |
| AddressExtractor | — | — | No participa en voting (extrae direcciones directamente) |
| ContactExtractor | — | — | No participa en voting (extrae cédula/correo directamente) |
- Persona: demandante, demandado (prioriza MarkerExtractor)
- Estructurado: radicado, fecha, juzgado (prioriza RegexExtractor)
Validadores (Chain of Responsibility)¶
StructuralValidator¶
Responsabilidad: Ajustar confianza según la sección del documento.
Boost por sección:
SECTION_BOOSTS = {
"encabezado": {"demandante": 0.15, "demandado": 0.15, "radicado": 0.20},
"partes": {"demandante": 0.20, "demandado": 0.20},
"hechos": {"demandante": 0.05, "demandado": 0.05},
"pretensiones": {"demandante": 0.10},
}
TemporalValidator¶
Responsabilidad: Validar consistencia de fechas.
Validaciones: - Año entre 2000-2030 - Fecha no futura - Consistencia con fecha de radicación
RadicadoFormatValidator¶
Responsabilidad: Validar formato de radicados colombianos.
Formato (23 dígitos):
[DANE 5][AÑO 4][CORP 5][TIPO 2][CONSEC 7]
Ejemplo: 11001020300020260001200
├─────┤├────┤├─────┤├──┤├───────┤
DANE AÑO CORP TIPO CONSEC
Filtrado:
- Elimina radicados con prefijo T- (sentencias, no radicados)
- Elimina radicados con prefijo SU-
DeduplicationValidator¶
Responsabilidad: Eliminar entidades duplicadas o muy similares.
Proceso: 1. Agrupar entidades por tipo 2. Calcular similitud Jaccard 3. Fusionar si similitud > 0.85 4. Mantener la de mayor confianza
Flujo de Datos Detallado¶
sequenceDiagram
participant T as Texto OCR
participant SA as SpaCyAdapter
participant ME as MarkerExtractor
participant SE as SpaCyNERExtractor
participant RE as RegexExtractor
participant AE as AddressExtractor
participant CE as ContactExtractor
participant EN as Ensemble
participant VC as ValidatorChain
participant ER as EntityResult
T->>SA: extract_entities(text)
SA->>SA: _get_nlp() lazy load
SA->>ME: extract(text)
ME-->>EN: [Entity(demandante, 0.95), ...]
SA->>SE: extract(text, doc)
SE-->>EN: [Entity(demandante, 0.70), ...]
SA->>RE: extract(text)
RE-->>EN: [Entity(radicado, 0.90), ...]
SA->>AE: extract(text)
AE-->>EN: [Entity(direccion, 0.80), ...]
SA->>CE: extract(text)
CE-->>EN: [Entity(cedula, 0.95), Entity(correo, 0.95)]
EN->>EN: _collect_candidates()
EN->>EN: _weighted_vote() por tipo
EN->>EN: _resolve_conflicts()
EN->>VC: validate(entities, context)
VC->>VC: StructuralValidator
VC->>VC: TemporalValidator
VC->>VC: RadicadoFormatValidator
VC->>VC: DeduplicationValidator
VC-->>ER: EntityResult validado
Métricas de Precisión¶
| Entidad | Precision | Recall | F1 Score |
|---|---|---|---|
| Demandante | 0.82 | 0.78 | 0.74 |
| Demandado | 0.75 | 0.72 | 0.73 |
| Radicado | 0.95 | 0.90 | 0.92 |
| Juzgado | 0.88 | 0.85 | 0.86 |
| Fecha | 0.85 | 0.78 | 0.81 |
| Correo | 1.00 | 1.00 | 1.00 |
| Cédula | 1.00 | 1.00 | 1.00 |
F1 Global: 85.3%
Configuración¶
@dataclass
class NERConfig:
"""Configuración del pipeline NER."""
model_name: str = "es_core_news_lg"
# Pesos reales del ensemble (solo 3 extractores votan)
person_weights: Dict[str, float] = field(default_factory=lambda: {
"MarkerExtractor": 0.70,
"SpaCyNERExtractor": 0.20,
"RegexExtractor": 0.10,
})
structured_weights: Dict[str, float] = field(default_factory=lambda: {
"MarkerExtractor": 0.20,
"SpaCyNERExtractor": 0.10,
"RegexExtractor": 0.70,
})
validation_enabled: bool = True
dedup_threshold: float = 0.85
max_entity_length: int = 100
Extensibilidad¶
Agregar Nuevo Extractor¶
- Implementar
EntityExtractorProtocol - Definir
name,priority,extract() - Registrar en
SpaCyAdapter._create_default_extractors() - Agregar peso en
NERConfig.ensemble_weights
Agregar Nuevo Validador¶
- Implementar
EntityValidatorProtocol - Definir
name,validate() - Agregar a
ValidatorChainen orden deseado
Leyenda de Estados¶
| Símbolo | Significado |
|---|---|
| ✅ | Implementado y funcional |
| 📅 | Planificado para fase futura |
Última actualización: 2026-03-05 (Fix pesos ensemble: solo 3 extractores votan, pesos diferenciados persona/estructurado)