v1.9.6: pendelbus/shuttlebus naar zwak + zwak-alleen moet titel-anker hebben

Aanleiding: Omroep Brabant-golfbericht (Solheim Cup Cromvoirt, 11-09-2026) kwam door
het filter. 'pendelbus' stond op de whitelist (evenementpendel vanaf station Den Bosch),
en 'station'+'bus'+'pendelbus' in de samenvatting was samen al drempel 3.

- pendelbus/shuttlebus: whitelist + medium -> zwak (1), één familie (synoniemen tellen
  niet als twee onafhankelijke signalen).
- _filter_passes: bij uitsluitend zwakke hits moet minstens één zwak woord in de TITEL
  staan; zwakke woorden alleen in de samenvatting zijn geen bewijs.
- validate_filter.py: invariant 1 uitgebreid + 3 regressiecases (golfbericht, zwak-alleen
  samenvatting, positieve controle met titel-anker).
- _filter_summary()/docstrings gelijkgetrokken met de werkelijke gewichten.
This commit is contained in:
Eddy de Vink 2026-09-11 07:49:05 +02:00
commit 1f168b2f54
3 changed files with 59 additions and 27 deletions

View file

@ -56,9 +56,12 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
# 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", "breng", "overweg", "overwegen", "sein", # Gewicht 1 (zwak/dubbelzinnig: los "bus", "breng", "overweg", "overwegen", "sein",
# "kaartje", "opwaarderen", "rails"): moet met een ander OV-woord combineren (een # "kaartje", "opwaarderen", "rails", "pendelbus"): moet met een ander OV-woord
# tweede, andere familie — dezelfde zwakke familie in titel én samenvatting telt # combineren (een tweede, andere familie — dezelfde zwakke familie in titel én
# niet als combinatie). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het # samenvatting telt niet als combinatie) EN minstens één zwak woord moet in de
# TITEL staan: een combinatie van alleen zwakke woorden in de samenvatting is
# geen bewijs (het golfbericht met "station" + "pendelbus" + "bus" in de
# samenvatting is geen OV-nieuws). Weert zo "VW-bus"/"schoolbus", "weer op de rails", het
# werkwoord "brengen" ("hoe breng je ... in beeld"), het idioom "goed met elkaar # werkwoord "brengen" ("hoe breng je ... in beeld"), het idioom "goed met elkaar
# overweg kunnen", het werkwoord "overwegen" ("gratieverlening overwegen"), het # overweg kunnen", het werkwoord "overwegen" ("gratieverlening overwegen"), het
# werkwoord "opwaarderen" ("een wijk opwaarderen") en brandweerjargon # werkwoord "opwaarderen" ("een wijk opwaarderen") en brandweerjargon
@ -101,7 +104,15 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
# woord nooit de doorslag kan geven. Bekende dubbelzinnige: # woord nooit de doorslag kan geven. Bekende dubbelzinnige:
# trein, bus, station(s), sprinter, reiziger, vervoerder, # trein, bus, station(s), sprinter, reiziger, vervoerder,
# inchecken/uitchecken, sein, overweg, breng, kaartje, # inchecken/uitchecken, sein, overweg, breng, kaartje,
# opwaarderen, rails, dienstregeling, chipkaart, ontsporen. # opwaarderen, rails, dienstregeling, chipkaart, ontsporen,
# pendelbus/shuttlebus (evenement-/hotel-/parkeerpendel;
# synoniemen -> bewust één familie, anders tellen twee
# synoniemen als twee onafhankelijke signalen).
# EXTRA REGEL voor zwak-alleen: staat er uitsluitend zwak
# bewijs in de SAMENVATTING (geen enkele zwakke hit in de
# titel), dan niet doorlaten. Twee/meer zwakke woorden in
# de samenvatting zijn anders samen al 3 punten
# ("station" + "pendelbus" + "bus" in een golfverslag).
# 3. BLACKLIST -> gematchte woorden zonder OV-waarde (stationsstraat, # 3. BLACKLIST -> gematchte woorden zonder OV-waarde (stationsstraat,
# stationair, stationcar, ...); blokkeren de familie niet. # stationair, stationcar, ...); blokkeren de familie niet.
# #
@ -128,13 +139,14 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
# medium (2) # medium (2)
(r"\btrams?\b", 2), (r"\btrams?\b", 2),
(r"\bmetro(?:'?s)?\b", 2), (r"\bmetro(?:'?s)?\b", 2),
(r"\b(?:openbaar vervoer|lightrail|pendelbus|shuttlebus|halte|bushokje|busbaan)\w*\b", 2), (r"\b(?:openbaar vervoer|lightrail|halte|bushokje|busbaan)\w*\b", 2),
(r"\b(?:ov-chipkaart|ovpay|ov-pay|studentenreisproduct)\w*\b", 2), (r"\b(?:ov-chipkaart|ovpay|ov-pay|studentenreisproduct)\w*\b", 2),
(r"\b(?:ov-staking|conducteursstaking)\w*\b", 2), (r"\b(?:ov-staking|conducteursstaking)\w*\b", 2),
(r"\bconduct(?:eur|eurs)\b", 2), # trein-/buspersoneel: onmiskenbaar OV-jargon (r"\bconduct(?:eur|eurs)\b", 2), # trein-/buspersoneel: onmiskenbaar OV-jargon
(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), # ná een koppelteken geen OV: 'VW-bus', 'school-bus' (r"(?<!-)\bbus(?:sen)?\b", 1), # ná een koppelteken geen OV: 'VW-bus', 'school-bus'
(r"\b(?:pendelbus|shuttlebus)\w*\b", 1), # evenement-/hotel-/parkeerpendel ('pendelbus naar de golfbaan') is geen OV; synoniemen bewust in één familie
(r"\b(?:inchecken|uitchecken)\w*\b", 1), # ook op luchthavens/hotels — geen OV-jargon, moet combineren (r"\b(?:inchecken|uitchecken)\w*\b", 1), # ook op luchthavens/hotels — geen OV-jargon, moet combineren
(r"\bsprinters?\b", 1), # ook atleet ('sprinter verpulvert record') — aparte familie, moet combineren (r"\bsprinters?\b", 1), # ook atleet ('sprinter verpulvert record') — aparte familie, moet combineren
(r"\bvervoerders?\b", 1), # ook logistiek ('vrachtvervoerder') — aparte familie, moet combineren (r"\bvervoerders?\b", 1), # ook logistiek ('vrachtvervoerder') — aparte familie, moet combineren
@ -201,7 +213,7 @@ MATCH_FILTER_WHITELIST: frozenset[str] = frozenset({
"busstaking", "tramhalte", "tramlijn", "tramnet", "tramverkeer", "busstaking", "tramhalte", "tramlijn", "tramnet", "tramverkeer",
"metrostation", "metrolijn", "metroverkeer", "metrostation", "metrolijn", "metroverkeer",
# Tarief/vervoerder-jargon # Tarief/vervoerder-jargon
"ov-chipkaart", "ov-staking", "lightrail", "pendelbus", "shuttlebus", "ov-chipkaart", "ov-staking", "lightrail",
"conducteur", "conducteurs", "conducteur", "conducteurs",
"openbaar vervoer", "openbaar vervoer",
}) })
@ -378,16 +390,19 @@ class RSSBot(Plugin):
"""Gewogen familiescore: doorlaten bij 2×(titelscore) + 1×(samenvattingscore) >= drempel. """Gewogen familiescore: doorlaten bij 2×(titelscore) + 1×(samenvattingscore) >= drempel.
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 er een
OV-woord bijkomen. Zwakke woorden (losse 'bus', 'breng', 'overweg', 'overwegen', tweede OV-woord bijkomen.
'sein', 'opwaarderen', 'kaartje', 'rails') moeten combineren met een ander Zwakke woorden (losse 'bus', 'breng', 'overweg', 'overwegen', 'sein',
OV-woord; dezelfde zwakke familie in titel én samenvatting is géén combinatie. 'opwaarderen', 'kaartje', 'rails', 'pendelbus') moeten combineren met een
Weert oa 'VW-bus' (bus matcht niet ná een koppelteken), 'schoolbus', 'bussen ander OV-woord én minstens één zwak woord moet in de titel staan; dezelfde
rijden om' in een woningbrand-lead, het werkwoord 'brengen', het idioom 'goed zwakke familie in titel én samenvatting is géén combinatie. Weert oa
met elkaar overweg kunnen', 'een wijk opwaarderen', brandweerjargon 'VW-bus' (bus matcht niet ná een koppelteken), 'schoolbus', 'bussen rijden
'sein brandmeester' en figuratief 'weer op de rails'. om' in een woningbrand-lead, het werkwoord 'brengen', het idioom 'goed met
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (medium), film-'treinramp' elkaar overweg kunnen', 'een wijk opwaarderen', brandweerjargon
en atleet-'sprinter' (sterk).""" 'sein brandmeester', figuratief 'weer op de rails' en een golfverslag
waarin de samenvatting 'station' + 'pendelbus' + 'bus' noemt.
Bewust doorgelaten: krant 'De Metro'/café 'De Tram' (medium) en atleet-
'sprinter' in combinatie met ander OV-woord."""
# Whitelist: één onmiskenbare OV-term (titel óf samenvatting) laat altijd door. # Whitelist: één onmiskenbare OV-term (titel óf samenvatting) laat altijd door.
if RSSBot._whitelist_hit(title) or RSSBot._whitelist_hit(summary): if RSSBot._whitelist_hit(title) or RSSBot._whitelist_hit(summary):
return True return True
@ -396,9 +411,13 @@ class RSSBot(Plugin):
score = 2 * sum(w for _, w in t_hits) + sum(w for _, w in s_hits) score = 2 * sum(w for _, w in t_hits) + sum(w for _, w in s_hits)
if score < MATCH_FILTER_THRESHOLD: if score < MATCH_FILTER_THRESHOLD:
return False return False
# Alleen zwakke families? Dan moet een tweede (verschillende) familie meetellen. if max(w for _, w in t_hits + s_hits) == 1:
if max(w for _, w in t_hits + s_hits) == 1 and len({i for i, _ in t_hits + s_hits}) < 2: # Alleen zwakke families: dan moeten ≥2 verschillende families meetellen
return False # én minstens één zwak woord moet in de TITEL staan. Zwakke woorden in
# de samenvatting alleen zijn geen bewijs ('station' + 'pendelbus' +
# 'bus' in een golfverslag telt anders op tot de drempel).
if not t_hits or len({i for i, _ in t_hits + s_hits}) < 2:
return False
return True return True
@staticmethod @staticmethod
@ -406,17 +425,20 @@ class RSSBot(Plugin):
"""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, ProRail, Qbuzz, ...), spoorinfra (spoorlijn, spoorwegovergang, seinstoring, perron), " "- Sterk (3): vervoerders (NS, Arriva, ProRail, Qbuzz, ...), spoorinfra (spoorlijn, spoorwegovergang, perron, spoorstoring), "
"station(s), bushalte/buslijn/bushokje/busstation, intercity/sprinter/streekvervoer/veerdienst/buurtbus, 'lijn 12'\n" "trein-/metro-/busstation, samenstellingen als bushalte/buslijn/bushokje/busbaan/busverkeer, treinstoring/treinverkeer/treinstaking, "
"- Medium (2): tram, metro, openbaar vervoer/dienstregeling/inchecken, halte, bushokje, busbaan, vervoerder, reiziger, " "intercity/streekvervoer/veerdienst/buurtbus/belbus, 'lijn 12'\n"
"ov-chipkaart/ovpay/reisproduct, ov-staking, pont\n" "- Medium (2): tram, metro, openbaar vervoer, lightrail, halte, pont, ov-chipkaart/ovpay, ov-staking, conducteur\n"
"- Zwak (1): losse 'bus' (niet ná een koppelteken), 'trein', 'breng', 'overweg', 'overwegen', 'sein', 'kaartje', 'opwaarderen', 'rails' — moet combineren met een ander OV-woord\n\n" "- Zwak (1): losse 'bus' (niet ná een koppelteken), 'pendelbus'/'shuttlebus', 'trein', 'station(s)', 'breng', 'overweg', 'overwegen', "
"'sein', 'kaartje', 'opwaarderen', 'rails', 'inchecken'/'uitchecken', 'dienstregeling', 'chipkaart', 'reisproduct', 'vervoersbewijs', "
"'ontsporen', 'reiziger', 'vervoerder', 'sprinter' — moet combineren met een ander OV-woord, met minstens één zwak woord in de titel\n\n"
"**Whitelist (altijd door):** één onmiskenbare OV-term in titel óf samenvatting is genoeg " "**Whitelist (altijd door):** één onmiskenbare OV-term in titel óf samenvatting is genoeg "
"(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, metrostation, conducteur, " "(NS, ProRail, Nederlandse Spoorwegen, spoorstoring, perron, bushalte, metrostation, conducteur, "
"ov-chipkaart, ovpay, openbaar vervoer, dienstregeling, ...)\n\n" "ov-chipkaart, ovpay, openbaar vervoer, ...)\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', ...) en niet-OV-vormen als 'stationcar' en 'stationair'\n\n" "o.a. straatnamen rond stations ('stationsstraat', 'stationsweg', ...) en niet-OV-vormen als 'stationcar' en 'stationair'\n\n"
"Doorlaten: `2×(titel-score) + 1×(samenvatting-score) ≥ 3`" "Doorlaten: `2×(titel-score) + 1×(samenvatting-score) ≥ 3`; bij uitsluitend zwakke woorden tellen alleen "
"≥2 verschillende families én een zwak woord in de titel"
) )
async def _send( async def _send(

View file

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

View file

@ -77,6 +77,7 @@ AMBIGUOUS = [
"inchecken", "uitchecken", "sein", "overweg", "overwegen", "breng", "inchecken", "uitchecken", "sein", "overweg", "overwegen", "breng",
"kaartje", "opwaarderen", "rails", "dienstregeling", "chipkaart", "kaartje", "opwaarderen", "rails", "dienstregeling", "chipkaart",
"reisproduct", "vervoersbewijs", "vervoerbewijs", "reisproduct", "vervoersbewijs", "vervoerbewijs",
"pendelbus", "shuttlebus",
"ontspor", "ontspoor", "ontspoord", "ontspoorde", "ontsporing", "ontspor", "ontspoor", "ontspoord", "ontspoorde", "ontsporing",
] ]
print(f"== invariant 1: {len(AMBIGUOUS)} dubbelzinnige woorden -> alleen zwak (1), nooit whitelist ==") print(f"== invariant 1: {len(AMBIGUOUS)} dubbelzinnige woorden -> alleen zwak (1), nooit whitelist ==")
@ -123,7 +124,16 @@ GOLDEN = [
("Fietser gewond op de Stationsstraat", "Het slachtoffer is naar het ziekenhuis gebracht.", False), ("Fietser gewond op de Stationsstraat", "Het slachtoffer is naar het ziekenhuis gebracht.", False),
("Kunstenaar maakt kaartje voor het museum", "", False), ("Kunstenaar maakt kaartje voor het museum", "", False),
("Benzineprijs stijgt opnieuw", "De pomp wordt duurder door de olieprijs.", False), ("Benzineprijs stijgt opnieuw", "De pomp wordt duurder door de olieprijs.", False),
# geweerd — zwakke woorden alléén in de samenvatting (geen titel-anker).
# Echt geval: Omroep Brabant, Solheim Cup-golfverslag (11-09-2026) — 'pendelbus'
# stond op de whitelist en 'station'+'pendelbus'+'bus' in de samenvatting was
# samen score 3.
("Golf-fans uit de hele wereld naar Cromvoirt: 'Beter dan sterrenrestaurant'",
"Op het station in Den Bosch stapte de oud-prof in de pendelbus naar Bernardus Golf. "
"In de bus bleek ze zeker niet de enige Amerikaanse.", False),
("Toernooi trekt duizenden bezoekers", "Vanaf het station rijden pendelbussen en extra bussen.", False),
# door — echte OV-berichten # door — echte OV-berichten
("Trein vertraagd, reizigers pakken de bus", "De storing duurde een uur.", True),
("Station Amersfoort krijgt nieuwe perrons", "De NS laat de perrons vernieuwen.", True), ("Station Amersfoort krijgt nieuwe perrons", "De NS laat de perrons vernieuwen.", True),
("Trein ontspoorde bij Weesp", "De spoorlijn was uren dicht.", True), ("Trein ontspoorde bij Weesp", "De spoorlijn was uren dicht.", True),
("Bushalte aan de Markt wordt verplaatst", "De halte gaat 200 meter opschuiven.", True), ("Bushalte aan de Markt wordt verplaatst", "De halte gaat 200 meter opschuiven.", True),