v1.8.3: OV-matchfilter weert gewone-woord-botsingen (breng/overweg/sein/opwaarderen)

'NOS | Oldambt weigert Leninbeeld' kwam door het filter via het werkwoord
'breng je het gesprek op gang' (familie vervoerders, gewicht 3). Audit over
30 dagen productiedata toonde dezelfde klasse: 'sein brandmeester'
(brandweer) 3x, 'overwegen' (werkwoord) 2x, idioom 'overweg kunnen' 1x.

- breng/overweg/overwegen/sein/opwaarderen van sterk(3)/medium(2) naar
  zwak(1): dubbelzinnige woorden combineren nu met een ander OV-woord
  (bushalte/trein/spoor/NS blijven gewoon scoren)
- _filter_passes eist sindsdien: alleen zwakke hits -> minimaal twee
  verschillende families (zelfde zwakke familie in titel+samenvatting is
  geen combinatie); voorkomt 'overwegen' in beide velden alsnog doorlaten
- trein-familie matst nu ook samenstellingen (stoptrein/goederentrein/
  sprinttrein) zodat overweg-demotie geen echte spoorincidenten verliest
- blacklist + stationair/stationeren/stationeel (geen OV-waarde, matchen
  wel via \bstation\w*\b)
- docs: module-commentaar en _filter_summary/_filter_passes bijgewerkt

Verificatie: exacte storingscode-herhaling op de gemelde NOS-entry (nu
gefilterd), 12 echte 30-dagen-entries (9 FP's eruit, 3 echte OV-berichten
blijven), 10/10 echte OV-regressiegevallen doorlaten, 11/11 FP-vormen
geweerd.
This commit is contained in:
Eddy de Vink 2026-09-10 12:27:17 +02:00
commit eab249ea57
2 changed files with 47 additions and 21 deletions

View file

@ -55,31 +55,41 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
# Gewicht 3 (sterk, onmiskenbaar OV): één woord in titel óf samenvatting volstaat. # Gewicht 3 (sterk, onmiskenbaar OV): één woord in titel óf samenvatting volstaat.
# Gewicht 2 (medium): één woord in de titel volstaat; in de samenvatting moet er # Gewicht 2 (medium): één woord in de titel volstaat; in de samenvatting moet er
# een tweede OV-woord bijkomen. # een tweede OV-woord bijkomen.
# Gewicht 1 (zwak/dubbelzinnig: los "bus", "kaartje", "rails"): moet met een ander # Gewicht 1 (zwak/dubbelzinnig: los "bus", "breng", "overweg", "overwegen", "sein",
# OV-woord combineren. Weert zo "VW-bus"/"schoolbus", "weer op de rails" en # "kaartje", "opwaarderen", "rails"): moet met een ander OV-woord combineren (een
# 112-berichten met "bussen rijden om" in de lead, zonder eenduidige # tweede, andere familie — dezelfde zwakke familie in titel én samenvatting telt
# bushalte/overweg/NS/trein-berichten te raken. # niet als combinatie). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het
# werkwoord "brengen" ("hoe breng je ... in beeld"), het idioom "goed met elkaar
# overweg kunnen", het werkwoord "overwegen" ("gratieverlening overwegen"), het
# werkwoord "opwaarderen" ("een wijk opwaarderen") en brandweerjargon
# ("sein brandmeester"), zonder eenduidige bushalte/overweg/NS/trein-berichten
# te raken.
# MATCH_FILTER_BLACKLIST: gematchte woorden die tóch geen OV-waarde hebben (o.a. # MATCH_FILTER_BLACKLIST: gematchte woorden die tóch geen OV-waarde hebben (o.a.
# straatnamen rond stations — "de Stationsstraat afsluiten voor een festival" # straatnamen rond stations — "de Stationsstraat afsluiten voor een festival"
# is geen OV-nieuws). Zwartgelijste woorden leveren geen score op, maar # is geen OV-nieuws). Zwartgelijste woorden leveren geen score op, maar
# blokkeren de familie niet: een écht "station"-woord ernaast scoort gewoon. # blokkeren de familie niet: een écht "station"-woord ernaast scoort gewoon.
MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
# sterk (3) # sterk (3)
(r"\b(?:ns|arriva|qbuzz|connexxion|cxx|transdev|keolis|ret|gvb|ebs|htm|syntus|breng|u-ov|allgo|flixbus|blauwnet|r-net|valleilijn|waterbus|eurostar|thalys|randstadrail|gvu|ov)\b", 3), # vervoerders (r"\b(?:ns|arriva|qbuzz|connexxion|cxx|transdev|keolis|ret|gvb|ebs|htm|syntus|u-ov|allgo|flixbus|blauwnet|r-net|valleilijn|waterbus|eurostar|thalys|randstadrail|gvu|ov)\b", 3), # vervoerders ('Breng' staat in de zwakke groep: botst met het werkwoord "brengen")
(r"\b(?:spoorweg|spoorbaan|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|perrons|overweg|overwegen|seinstoring|wisselstoring|sein|ontsporing|ontspoord|spoorstaking)\b", 3), # spoorinfra (r"\b(?:spoorweg|spoorbaan|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|perrons|seinstoring|wisselstoring|ontsporing|ontspoord|spoorstaking)\b", 3), # spoorinfra ('overweg', 'overwegen' en 'sein' in de zwakke groep: botsen met idioom/werkwoord resp. brandweerjargon)
(r"\bstation\w*\b", 3), # station(s) (r"\bstation\w*\b", 3), # station(s)
(r"\b(?:bus|tram|metro)(?:halte|haltes|lijn|lijnen|station|stations|verkeer|net|staking)\b", 3), # voertuig-samenstelling (r"\b(?:bus|tram|metro)(?:halte|haltes|lijn|lijnen|station|stations|verkeer|net|staking)\b", 3), # voertuig-samenstelling
(r"\btrein\w*\b", 3), # trein(verkeer/staking/ongeluk/...) (r"\b\w*trein\w*\b", 3), # trein(verkeer/staking/…), ook samenstellingen: stoptrein, goederentrein, sprinttrein
# medium (2) # medium (2)
(r"\btram\b", 2), (r"\btram\b", 2),
(r"\bmetro\b", 2), (r"\bmetro\b", 2),
(r"\b(?:openbaar vervoer|dienstregeling|inchecken|uitchecken|lightrail|pendelbus|shuttlebus)\b", 2), (r"\b(?:openbaar vervoer|dienstregeling|inchecken|uitchecken|lightrail|pendelbus|shuttlebus)\b", 2),
(r"\b(?:ov-chipkaart|chipkaart|ovpay|ov-pay|reisproduct|studentenreisproduct|vervoersbewijs|vervoerbewijs|opwaarderen)\b", 2), (r"\b(?:ov-chipkaart|chipkaart|ovpay|ov-pay|reisproduct|studentenreisproduct|vervoersbewijs|vervoerbewijs)\b", 2),
(r"\b(?:ov-staking|conducteursstaking)\b", 2), (r"\b(?:ov-staking|conducteursstaking)\b", 2),
(r"\b(?:pont|ponten)\b", 2), # waterbus/veerdienst (r"\b(?:pont|ponten)\b", 2), # waterbus/veerdienst
# zwak (1) — dubbelzinnig, moet combineren # zwak (1) — dubbelzinnig, moet combineren
(r"\bbus(?:sen)?\b", 1), (r"\bbus(?:sen)?\b", 1),
(r"\bbreng\b", 1), # vervoerder Breng vs werkwoord "brengen" ("hoe breng je ... in beeld")
(r"\boverweg\b", 1), # spoorwegovergang vs idioom "goed met elkaar overweg kunnen"
(r"\boverwegen\b", 1), # meervoud van overweg vs werkwoord ("een gratieverlening overwegen")
(r"\bsein\b", 1), # spoorsein vs brandweerjargon ("sein brandmeester") en figuurlijk gebruik
(r"\bkaart(?:je|jes)\b", 1), (r"\bkaart(?:je|jes)\b", 1),
(r"\bopwaarderen\b", 1), # ov-chipkaart opladen vs werkwoord "een wijk/park opwaarderen"
(r"\brails\b", 1), (r"\brails\b", 1),
] ]
@ -98,6 +108,10 @@ MATCH_FILTER_BLACKLIST: frozenset[str] = frozenset({
"stationssteeg", "stationswal", "stationsbuurt", "stationswijk", "stationspark", "stationssteeg", "stationswal", "stationsbuurt", "stationswijk", "stationspark",
# Idem voor "\btrein\w*\b". # Idem voor "\btrein\w*\b".
"treinstraat", "treinweg", "treinstraat", "treinweg",
# Idem voor "\bstation\w*\b": werkwoords-/bijvoeglijke vormen zonder OV-waarde
# ("motor stationair laten draaien", "auto's hier stationeren").
"stationair", "stationeel", "stationeren", "stationeert", "stationeerde",
"stationeerden", "stationerend",
}) })
_compiled_match_families: list[tuple[re.Pattern, int]] = [ _compiled_match_families: list[tuple[re.Pattern, int]] = [
@ -219,20 +233,20 @@ class RSSBot(Plugin):
self.log.exception("Fatal error while polling feeds") self.log.exception("Fatal error while polling feeds")
@staticmethod @staticmethod
def _fam_score(text: str) -> int: def _fam_hits(text: str) -> list[tuple[int, int]]:
"""Som van gewichten over de OV-families die in text voorkomen (1× per familie). """(familie-index, gewicht) voor elke OV-familie die in text voorkomt (1× per familie).
Gematchte woorden uit MATCH_FILTER_BLACKLIST (o.a. 'stationsstraat') Gematchte woorden uit MATCH_FILTER_BLACKLIST (o.a. 'stationsstraat')
tellen niet mee en blokkeren de familie niet: een écht OV-woord in tellen niet mee en blokkeren de familie niet: een écht OV-woord in
dezelfde tekst scoort gewoon verder.""" dezelfde tekst scoort gewoon verder."""
total = 0 hits: list[tuple[int, int]] = []
for family, weight in _compiled_match_families: for idx, (family, weight) in enumerate(_compiled_match_families):
for m in family.finditer(text): for m in family.finditer(text):
if m.group(0).lower() in MATCH_FILTER_BLACKLIST: if m.group(0).lower() in MATCH_FILTER_BLACKLIST:
continue continue
total += weight hits.append((idx, weight))
break break
return total return hits
@staticmethod @staticmethod
def _filter_passes(title: str, summary: str) -> bool: def _filter_passes(title: str, summary: str) -> bool:
@ -240,22 +254,34 @@ class RSSBot(Plugin):
Sterke families (gewicht 3) halen de drempel al met één woord in titel óf Sterke families (gewicht 3) halen de drempel al met één woord in titel óf
samenvatting. Medium (2) in de titel ook; in de samenvatting moet een tweede samenvatting. Medium (2) in de titel ook; in de samenvatting moet een tweede
OV-woord bijkomen. Zwakke woorden (losse 'bus', 'kaartje', 'rails') moeten OV-woord bijkomen. Zwakke woorden (losse 'bus', 'breng', 'overweg', 'overwegen',
combineren met een ander OV-woord. Weert oa 'VW-bus', 'schoolbus', 'sein', 'opwaarderen', 'kaartje', 'rails') moeten combineren met een ander
'bussen rijden om' in een woningbrand-lead en figuratief 'weer op de rails'. OV-woord; dezelfde zwakke familie in titel én samenvatting is géén combinatie.
Weert oa 'VW-bus', 'schoolbus', 'bussen rijden om' in een woningbrand-lead,
het werkwoord 'brengen', het idioom 'goed met elkaar overweg kunnen',
'een wijk opwaarderen', brandweerjargon 'sein brandmeester' en figuratief
'weer op de rails'.
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' in de titel (medium).""" Bewust doorgelaten: krant 'De Metro'/café 'De Tram' in de titel (medium)."""
return 2 * RSSBot._fam_score(title) + RSSBot._fam_score(summary) >= MATCH_FILTER_THRESHOLD t_hits = RSSBot._fam_hits(title)
s_hits = RSSBot._fam_hits(summary)
score = 2 * sum(w for _, w in t_hits) + sum(w for _, w in s_hits)
if score < MATCH_FILTER_THRESHOLD:
return False
# Alleen zwakke families? Dan moet een tweede (verschillende) familie meetellen.
if max(w for _, w in t_hits + s_hits) == 1 and len({i for i, _ in t_hits + s_hits}) < 2:
return False
return True
@staticmethod @staticmethod
def _filter_summary() -> str: def _filter_summary() -> str:
"""Mensvriendelijke weergave van het vaste gewogen match-filter.""" """Mensvriendelijke weergave van het vaste gewogen match-filter."""
return ( return (
"**OV-matchfilter (vast in code, gewogen families):**\n" "**OV-matchfilter (vast in code, gewogen families):**\n"
"- Sterk (3): vervoerders (NS, Arriva, Qbuzz, ...), spoorinfra (perron, overweg, sein), " "- Sterk (3): vervoerders (NS, Arriva, Qbuzz, ...), spoorinfra (perron, spoorwegovergang, seinstoring), "
"station(s), bushalte/buslijn/treinstel, trein\n" "station(s), bushalte/buslijn/treinstel, trein\n"
"- Medium (2): tram, metro, openbaar vervoer/dienstregeling/inchecken, " "- Medium (2): tram, metro, openbaar vervoer/dienstregeling/inchecken, "
"ov-chipkaart/ovpay/reisproduct, ov-staking, pont\n" "ov-chipkaart/ovpay/reisproduct, ov-staking, pont\n"
"- Zwak (1): losse 'bus', 'kaartje', 'rails' — moet combineren met een ander OV-woord\n\n" "- Zwak (1): losse 'bus', 'breng', 'overweg', 'overwegen', 'sein', 'kaartje', 'opwaarderen', 'rails' — moet combineren met een ander OV-woord\n\n"
"Uitgesloten (blacklist): woorden die wél matchen maar geen OV-waarde hebben, " "Uitgesloten (blacklist): woorden die wél matchen maar geen OV-waarde hebben, "
"o.a. straatnamen rond stations ('stationsstraat', 'stationsweg', ...)\n\n" "o.a. straatnamen rond stations ('stationsstraat', 'stationsweg', ...)\n\n"
"Doorlaten: `2×(titel-score) + 1×(samenvatting-score) ≥ 3`" "Doorlaten: `2×(titel-score) + 1×(samenvatting-score) ≥ 3`"

View file

@ -1,5 +1,5 @@
id: nl.eddydevink.rss id: nl.eddydevink.rss
version: 1.8.2 version: 1.8.3
modules: modules:
- buzzrss - buzzrss
main_class: RSSBot main_class: RSSBot