Files
veye-lalwa/pipeline/guadeloupe.py
T

163 lines
5.3 KiB
Python
Raw Permalink Normal View History

"""Détection de la pertinence guadeloupéenne et ultramarine d'un texte.
Le tableau A de l'annexe cote lui-même les 27 lois promulguées ; le tableau B
ne cote pas les textes en cours. Pour ces derniers, la cotation est déduite du
vocabulaire employé par le rapport, avec des poids déclarés dans
`pipeline/config.yaml`.
Toute cotation obtenue ainsi est signalée : le texte porte `a_verifier = 1` et
un motif explicite. L'interface l'affiche comme telle. Une cotation déduite
n'est pas une cotation établie.
"""
from __future__ import annotations
import functools
import re
from dataclasses import dataclass, field
import yaml
from pipeline import chemins
from pipeline.modeles import Pertinence
from pipeline.parseurs.dates_fr import sans_accents
# Tournures par lesquelles le corpus **écarte** un territoire qu'il vient de
# nommer : « PPL montagne — sans portée pour la Guadeloupe », « PJL logement —
# pas de volet outre-mer à ce stade ». Compter la mention sans lire la négation
# donnerait à ces textes la cotation la plus forte, à l'exact inverse de ce que
# la source affirme.
NEGATIONS = (
"sans portee",
"sans objet",
"sans incidence",
"sans specificite",
"sans volet",
"sans consequence",
"ne concerne pas",
"n'est pas concerne",
"pas concernee",
"pas de volet",
"pas d'incidence",
"non concerne",
"hors champ",
"exclue",
"exclus",
)
# Fenêtre de voisinage, en caractères, autour d'une mention de territoire.
_AVANT = 90
_APRES = 60
@dataclass(slots=True)
class Cotation:
"""Résultat d'une détection, avec ce qui l'a motivée."""
pertinence: Pertinence | None
score: int
expressions: list[str]
exclusions: list[str] = field(default_factory=list)
@property
def note(self) -> str | None:
if self.exclusions and not self.expressions:
return (
"Le rapport écarte explicitement ce territoire : "
+ ", ".join(f{e} »" for e in self.exclusions[:3])
)
if not self.expressions:
return None
note = "Détecté d'après : " + ", ".join(self.expressions[:6])
if self.exclusions:
note += " — mais le rapport écarte par ailleurs : " + ", ".join(
f{e} »" for e in self.exclusions[:2]
)
return note
@functools.lru_cache(maxsize=1)
def _configuration() -> dict:
return yaml.safe_load(chemins.CONFIG.read_text(encoding="utf-8"))
@functools.lru_cache(maxsize=1)
def _motifs() -> list[tuple[re.Pattern[str], str, int]]:
"""Compile un motif par expression, ancré sur des frontières de mot."""
expressions = _configuration()["guadeloupe"]["expressions"]
compiles: list[tuple[re.Pattern[str], str, int]] = []
for expression, poids in expressions.items():
plat = sans_accents(str(expression)).lower()
compiles.append((re.compile(rf"(?<!\w){re.escape(plat)}\w{{0,3}}(?!\w)"), plat, int(poids)))
# Les expressions les plus lourdes d'abord : le compte-rendu cite en
# priorité ce qui a réellement décidé de la cotation.
compiles.sort(key=lambda triplet: -triplet[2])
return compiles
def coter(*fragments: str | None) -> Cotation:
"""Cote la pertinence guadeloupéenne d'un ensemble de fragments de texte.
Une mention niée ne compte pas. « Sans portée pour la Guadeloupe » nomme le
territoire pour l'écarter : la lire comme une pertinence forte inverserait
le sens de la source.
"""
plat = sans_accents(" ".join(f for f in fragments if f)).lower()
if not plat.strip():
return Cotation(pertinence=None, score=0, expressions=[])
score = 0
trouvees: list[str] = []
exclusions: list[str] = []
for motif, expression, poids in _motifs():
positions = [m.start() for m in motif.finditer(plat)]
if not positions:
continue
retenues = [p for p in positions if not _est_niee(plat, p)]
if retenues:
score += poids
trouvees.append(expression)
else:
exclusions.append(_extrait_autour(plat, positions[0]))
seuils = _configuration()["guadeloupe"]["seuils"]
if score >= seuils["forte"]:
pertinence = Pertinence.FORTE
elif score >= seuils["moyenne"]:
pertinence = Pertinence.MOYENNE
elif score >= seuils["faible"]:
pertinence = Pertinence.FAIBLE
elif exclusions:
# Le territoire est nommé, mais pour être écarté : la pertinence est
# faible, et le motif dit pourquoi.
pertinence = Pertinence.FAIBLE
else:
pertinence = None
return Cotation(
pertinence=pertinence, score=score, expressions=trouvees, exclusions=exclusions
)
def _est_niee(plat: str, position: int) -> bool:
"""Vrai si une tournure d'exclusion entoure la mention trouvée."""
voisinage = plat[max(0, position - _AVANT) : position + _APRES]
return any(negation in voisinage for negation in NEGATIONS)
def _extrait_autour(plat: str, position: int, largeur: int = 70) -> str:
debut = max(0, position - largeur)
fin = min(len(plat), position + largeur)
return " ".join(plat[debut:fin].split())
MOTIF_VERIFICATION = (
"Pertinence Guadeloupe déduite automatiquement : l'annexe du rapport ne "
"cote que les lois promulguées, pas les textes encore en discussion."
)