v1.13.0: één term = één signaal — overlappende familie-treffers wegen 1×
'busbaan' stond in de bus-samenstelling-familie (fam 4) én de halte/busbaan-familie (fam 13): één woord scoorde 2+2=4 en liet zo een niet-OV-bericht door (Omroep Brabant politie-achtervolging 'via de busbaan', 13-09-2026). Latente dubbeltellingen: 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat 'vervoer'. _fam_hits voegt nu overlappende span van verschillende families samen met het hoogste gewicht (retour blijft (familie-index, gewicht), dus de familie-één-signaal-regel over titel+samenvatting is intact). Beleidscommentaar + regressie (56 cases) bijgewerkt; 45-dagen-replay (90 geposte entries): precies 1 gedragsverschil — het fatbikers- bericht wordt nu geweerd, delftopzondag (tram+halte, twee termen) blijft door (besluit 2026-09-13).
This commit is contained in:
parent
e1b30fc2ee
commit
1346cbb549
3 changed files with 53 additions and 11 deletions
|
|
@ -55,8 +55,11 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
||||||
# Gewogen OV-trefwoord-families voor het match-filter. Elke tuple = (regex, gewicht).
|
# Gewogen OV-trefwoord-families voor het match-filter. Elke tuple = (regex, gewicht).
|
||||||
# Per familie wordt 1× geteld per ARTIKEL (titel én samenvatting samen; herhaling
|
# Per familie wordt 1× geteld per ARTIKEL (titel én samenvatting samen; herhaling
|
||||||
# of het woord in beide velden = één signaal — 'busstation' 60× weegt 1×, besluit
|
# of het woord in beide velden = één signaal — 'busstation' 60× weegt 1×, besluit
|
||||||
# 2026-09-11). Drempel (3): doorlaten als de som van de unieke familie-gewichten
|
# 2026-09-11). Eén term die meerdere families matcht telt óók maar 1× (hoogste
|
||||||
# >= MATCH_FILTER_THRESHOLD (titel weegt niet extra).
|
# gewicht wint): 'busbaan' zit in de bus-samenstelling- én de halte/busbaan-familie,
|
||||||
|
# 'ov-chipkaart' bevat 'chipkaart', 'openbaar vervoer' bevat 'vervoer' — geen
|
||||||
|
# dubbeltelling (besluit 2026-09-13). Drempel (3): doorlaten als de som van de
|
||||||
|
# unieke familie-gewichten >= MATCH_FILTER_THRESHOLD (titel weegt niet extra).
|
||||||
# MAX-GEWICHT 2 — besluit 2026-09-12: een enkele FAMILIE-hit kan nooit solo
|
# MAX-GEWICHT 2 — besluit 2026-09-12: een enkele FAMILIE-hit kan nooit solo
|
||||||
# doorlaten (2 < 3); écht OV-nieuws combineert in de praktijk altijd met een
|
# doorlaten (2 < 3); écht OV-nieuws combineert in de praktijk altijd met een
|
||||||
# tweede signaal (gevalideerd op 205 productie-artikelen; zie beleidsblok).
|
# tweede signaal (gevalideerd op 205 productie-artikelen; zie beleidsblok).
|
||||||
|
|
@ -118,6 +121,10 @@ rss_change_level = EventType.find("xyz.maubot.rss", t_class=EventType.Class.STAT
|
||||||
# 2. GEWOGEN FAMILIES -> score = som van unieke OV-families over het hele
|
# 2. GEWOGEN FAMILIES -> score = som van unieke OV-families over het hele
|
||||||
# artikel (titel + samenvatting; per familie 1× per artikel — herhaling of
|
# artikel (titel + samenvatting; per familie 1× per artikel — herhaling of
|
||||||
# beide velden tellen niet dubbel, besluit 2026-09-11) >= drempel (3).
|
# beide velden tellen niet dubbel, besluit 2026-09-11) >= drempel (3).
|
||||||
|
# Overlappende treffers van verschillende families (één term op één plek)
|
||||||
|
# tellen samen als 1× met het hoogste gewicht — een term mag nooit dubbel
|
||||||
|
# wegen ('busbaan' in de bus-samenstelling- én de halte/busbaan-familie,
|
||||||
|
# besluit 2026-09-13).
|
||||||
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
# - sterk (2): onmiskenbaar OV-jargon/merken/samenvoegingen (spoorlijn,
|
||||||
# bushalte, busverkeer, intercity, veerboot, ertms,
|
# bushalte, busverkeer, intercity, veerboot, ertms,
|
||||||
# rangeer..., ice-frasen, ...). Eén hit (titel óf samenvatting)
|
# rangeer..., ice-frasen, ...). Eén hit (titel óf samenvatting)
|
||||||
|
|
@ -175,7 +182,7 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
||||||
(r"\b(?:spoorweg|spoorba(?:an|nen)|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|seinstoring|wisselstoring|spoorstaking|spoorsector|spoorbeheerder|spoornet|spoorvervoerder|ertms)\w*\b", 2), # spoorinfra; let op de klinkerwisseling: 'ontsporing' (open lettergreep, één o) vs 'ontspoord'/'ontspoorde' (dicht, dubbel o) — vandaar beide stammen. Géén 'spoorbrug': die is 2 maar vaak louter locatie (besluit 2026-09-12). Uitbreiding 2026-09-12 (completheids-audit): spoorsector, spoorbeheerder, spoornet, spoorvervoerder, ertms
|
(r"\b(?:spoorweg|spoorba(?:an|nen)|spoorlijn|spoorverkeer|spoorwegovergang|spoorstoring|perron|seinstoring|wisselstoring|spoorstaking|spoorsector|spoorbeheerder|spoornet|spoorvervoerder|ertms)\w*\b", 2), # spoorinfra; let op de klinkerwisseling: 'ontsporing' (open lettergreep, één o) vs 'ontspoord'/'ontspoorde' (dicht, dubbel o) — vandaar beide stammen. Géén 'spoorbrug': die is 2 maar vaak louter locatie (besluit 2026-09-12). Uitbreiding 2026-09-12 (completheids-audit): spoorsector, spoorbeheerder, spoornet, spoorvervoerder, ertms
|
||||||
(r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 2), # station-samenstellingen: onmiskenbaar OV, maar één station-woord náást een ander onderwerp ('pand naast busstation', besluit 2026-09-11) is geen OV-bericht; in de samenvatting moet een tweede OV-woord bijkomen
|
(r"\b(?:treinstation|treinstations|metrostation|metrostations|busstation|busstations)\b", 2), # station-samenstellingen: onmiskenbaar OV, maar één station-woord náást een ander onderwerp ('pand naast busstation', besluit 2026-09-11) is geen OV-bericht; in de samenvatting moet een tweede OV-woord bijkomen
|
||||||
(r"(?<![\w-])\bstations?\b", 1), # los 'station(s)' is ZWAK: ook tv-/radio-/regionale omroepstations ('de lokale stations'), moet combineren met een andere OV-familie
|
(r"(?<![\w-])\bstations?\b", 1), # los 'station(s)' is ZWAK: ook tv-/radio-/regionale omroepstations ('de lokale stations'), moet combineren met een andere OV-familie
|
||||||
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 2), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/buschauffeur/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee — fix 2026-09-11. Géén 'ongeluk'/'vervoer': die zijn bewust zwak (busongeluk = ook touringcar, busvervoer = ook bedrijfsvervoer, audit 2026-09-12)
|
(r"\b(?:bus|tram|metro)-?(?:halte|haltes|lijn|lijnen|verkeer|net|staking|ba(?:an|nen)|hok(?:je|jes)|chauffeur)\w*\b", 2), # voertuig-samenstelling (bushalte/buslijn/bushokje/busbanen/buschauffeur/…); géén 'station': *station-samenstellingen zitten in de station-familie, anders telde 'busstation' dubbel mee — fix 2026-09-11. Géén 'ongeluk'/'vervoer': die zijn bewust zwak (busongeluk = ook touringcar, busvervoer = ook bedrijfsvervoer, audit 2026-09-12). 'busbaan' staat óók in fam 13: overlappende treffers tellen 1× met het hoogste gewicht (besluit 2026-09-13)
|
||||||
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk|treinvertraging|treinhinder|treinuitval|treinverstoring)\w*\b", 2), # trein-jargon-samenstellingen: onmiskenbaar OV; uitgebreid met vertraging/hinder/uitval/verstoring (audit 2026-09-12)
|
(r"\b(?:treinstoring|treinverkeer|treinstaking|treinongeval|treinbotsing|treinongeluk|treinvertraging|treinhinder|treinuitval|treinverstoring)\w*\b", 2), # trein-jargon-samenstellingen: onmiskenbaar OV; uitgebreid met vertraging/hinder/uitval/verstoring (audit 2026-09-12)
|
||||||
(r"\b\w*trein\w*\b", 1), # losse 'trein' en algemene samenstellingen (stoptrein, goederentrein, treinverkeer al in bovenstaande familie) — ZWAK: 'de trein naar mijn nieuwe huis' is geen OV-jargon, moet combineren met een andere familie
|
(r"\b\w*trein\w*\b", 1), # losse 'trein' en algemene samenstellingen (stoptrein, goederentrein, treinverkeer al in bovenstaande familie) — ZWAK: 'de trein naar mijn nieuwe huis' is geen OV-jargon, moet combineren met een andere familie
|
||||||
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus|maaslijn|nedersaksenlijn|kamperlijn)\w*\b", 2), # treintypes, vervoerssectoren, veren en lijnnamen ('sprinter' staat in de zwakke groep: atleet-botsing); maaslijn/nedersaksenlijn/kamperlijn via audit 2026-09-12
|
(r"\b(?:intercity|streekvervoer|stadsvervoer|veerdienst|veerboot|veerboten|veerpont|voetveer|veerverbinding|buurtbus|belbus|maaslijn|nedersaksenlijn|kamperlijn)\w*\b", 2), # treintypes, vervoerssectoren, veren en lijnnamen ('sprinter' staat in de zwakke groep: atleet-botsing); maaslijn/nedersaksenlijn/kamperlijn via audit 2026-09-12
|
||||||
|
|
@ -185,7 +192,7 @@ MATCH_FILTER_FAMILIES: list[tuple[str, int]] = [
|
||||||
# onmiskenbaar OV maar iets algemener (2, moet ook combineren)
|
# onmiskenbaar OV maar iets algemener (2, moet ook combineren)
|
||||||
(r"\btrams?\b", 2),
|
(r"\btrams?\b", 2),
|
||||||
(r"\bmetro(?:'?s)?\b", 2),
|
(r"\bmetro(?:'?s)?\b", 2),
|
||||||
(r"\b(?:openbaar vervoer|lightrail|halte|bushokje|busbaan)\w*\b", 2),
|
(r"\b(?:openbaar vervoer|lightrail|halte|bushokje|busbaan)\w*\b", 2), # 'busbaan' ook in fam 4, 'openbaar vervoer' bevat 'vervoer' (fam 41): één term = één signaal (besluit 2026-09-13)
|
||||||
(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
|
||||||
|
|
@ -442,25 +449,47 @@ class RSSBot(Plugin):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
def _fam_hits(text: str) -> list[tuple[int, int]]:
|
||||||
"""(familie-index, gewicht) voor elke OV-familie die in text voorkomt (1× per familie).
|
"""(familie-index, gewicht) per OV-signaal in text (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.
|
||||||
hits: list[tuple[int, int]] = []
|
|
||||||
|
Eén term telt maximaal één signaal (besluit 2026-09-13): overlappende
|
||||||
|
treffers van verschillende families op dezelfde plek worden samengevoegd
|
||||||
|
met het hoogste gewicht. Voorheen telde één woord dubbel als het in
|
||||||
|
meerdere families stond — 'busbaan' zit in de bus-samenstelling-familie
|
||||||
|
én in de halte/busbaan-familie (2+2=4), 'ov-chipkaart' bevat 'chipkaart'
|
||||||
|
(2+1=3), 'openbaar vervoer' bevat 'vervoer' (2+1=3). Max-2-stelsel:
|
||||||
|
één term kan nooit solo doorlaten."""
|
||||||
|
spans: list[tuple[int, int, int, int]] = [] # (start, end, familie-idx, gewicht)
|
||||||
for idx, (family, weight) in enumerate(_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
|
||||||
hits.append((idx, weight))
|
spans.append((m.start(), m.end(), idx, weight))
|
||||||
break
|
break
|
||||||
return hits
|
spans.sort()
|
||||||
|
merged: list[tuple[int, int, int, int]] = []
|
||||||
|
for start, end, idx, weight in spans:
|
||||||
|
if merged and start < merged[-1][1]:
|
||||||
|
s0, e0, i0, w0 = merged[-1]
|
||||||
|
if weight > w0:
|
||||||
|
merged[-1] = (s0, max(e0, end), idx, weight)
|
||||||
|
else:
|
||||||
|
merged[-1] = (s0, max(e0, end), i0, w0)
|
||||||
|
else:
|
||||||
|
merged.append((start, end, idx, weight))
|
||||||
|
return [(idx, w) for _, _, idx, w in merged]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _filter_passes(title: str, summary: str) -> bool:
|
def _filter_passes(title: str, summary: str) -> bool:
|
||||||
"""Gewogen familiescore (max-2-stelsel, besluit 2026-09-12): elke OV-familie
|
"""Gewogen familiescore (max-2-stelsel, besluit 2026-09-12): elke OV-familie
|
||||||
telt 1× per artikel (titel + samenvatting samen; herhaling of beide velden =
|
telt 1× per artikel (titel + samenvatting samen; herhaling of beide velden =
|
||||||
één signaal). Doorlaten bij som >= drempel (3). Geen enkele familie weegt
|
één signaal). Overlappende treffers van verschillende families tellen samen
|
||||||
|
als 1× met het hoogste gewicht — 'busbaan' (bus-samenstelling- én
|
||||||
|
halte/busbaan-familie) levert één signaal van 2, geen 4 (besluit 2026-09-13).
|
||||||
|
Doorlaten bij som >= drempel (3). Geen enkele familie weegt
|
||||||
meer dan 2, dus een enkele familie-hit kan nooit solo doorlaten — 'hermes'
|
meer dan 2, dus een enkele familie-hit kan nooit solo doorlaten — 'hermes'
|
||||||
als achternaam passeerde vroeger op één vervoerder-hit van 3 (Margraten,
|
als achternaam passeerde vroeger op één vervoerder-hit van 3 (Margraten,
|
||||||
2026-09-12); écht OV-nieuws combineert in de praktijk altijd met een tweede
|
2026-09-12); écht OV-nieuws combineert in de praktijk altijd met een tweede
|
||||||
|
|
@ -494,6 +523,7 @@ class RSSBot(Plugin):
|
||||||
s_hits = RSSBot._fam_hits(summary)
|
s_hits = RSSBot._fam_hits(summary)
|
||||||
# Eén familie telt 1× per ARTIKEL (titel én samenvatting samen): 'busstation'
|
# Eén familie telt 1× per ARTIKEL (titel én samenvatting samen): 'busstation'
|
||||||
# in beide velden, of 60× in de tekst, blijft één signaal (besluit 2026-09-11).
|
# in beide velden, of 60× in de tekst, blijft één signaal (besluit 2026-09-11).
|
||||||
|
# _fam_hits levert per term al het hoogste gewicht bij overlappende families.
|
||||||
combined = dict(t_hits + s_hits)
|
combined = dict(t_hits + s_hits)
|
||||||
score = sum(combined.values())
|
score = sum(combined.values())
|
||||||
whitelist_title = RSSBot._whitelist_hit(title)
|
whitelist_title = RSSBot._whitelist_hit(title)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
id: nl.eddydevink.rss
|
id: nl.eddydevink.rss
|
||||||
version: 1.12.0
|
version: 1.13.0
|
||||||
modules:
|
modules:
|
||||||
- buzzrss
|
- buzzrss
|
||||||
main_class: RSSBot
|
main_class: RSSBot
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,18 @@ GOLDEN = [
|
||||||
("Meerdere doden na busongeluk in Zwitserland", "Het busongeluk gebeurde naast de snelweg bij een afrit.", False), # busongeluk(1) alleen = zwak-alleen
|
("Meerdere doden na busongeluk in Zwitserland", "Het busongeluk gebeurde naast de snelweg bij een afrit.", False), # busongeluk(1) alleen = zwak-alleen
|
||||||
("Antwerpen ontvangt eerste ICE", "De eerste ICE van Deutsche Bahn rijdt vanaf volgende maand naar Antwerpen.", False), # bewust geweerd: 'ice van' is NIET opgenomen (ijs-botsing); deutsche bahn(2) alleen < 3 — was vóór de wijziging óók al geweerd
|
("Antwerpen ontvangt eerste ICE", "De eerste ICE van Deutsche Bahn rijdt vanaf volgende maand naar Antwerpen.", False), # bewust geweerd: 'ice van' is NIET opgenomen (ijs-botsing); deutsche bahn(2) alleen < 3 — was vóór de wijziging óók al geweerd
|
||||||
("NS verlengt noodregeling tot volgend jaar", "De vervoerder kampt met personeelstekorten.", True), # ns(2) + vervoerder(1) = 3
|
("NS verlengt noodregeling tot volgend jaar", "De vervoerder kampt met personeelstekorten.", True), # ns(2) + vervoerder(1) = 3
|
||||||
|
# fix 2026-09-13: één term mag nooit via twee families dubbel wegen.
|
||||||
|
# Echte case: Omroep Brabant (13-09-2026) — politie-achtervolging van fatbikers
|
||||||
|
# 'via de busbaan' scoorde 2+2=4 omdat 'busbaan' in de bus-samenstelling-familie
|
||||||
|
# (fam 4) én in de halte/busbaan-familie (fam 13) staat. Nu één signaal (2):
|
||||||
|
# zonder tweede OV-term onder de drempel.
|
||||||
|
("Groep jonge fatbikers door Eindhoven achtervolgd na beroving met geweld",
|
||||||
|
"Agenten reden met zwaailichten achter de groep aan, onder meer via de busbaan en een fietspad.", False),
|
||||||
|
("De politie achtervolgde de groep via de busbaan", "De achtervolging duurde enkele minuten.", True), # whitelist-titel-anker: 'busbaan' in de TITEL blijft door (+2); de merge raakt alleen de teller
|
||||||
|
# recall bewaakt: echt busbaan-nieuws blijft door (whitelist-titel-anker +2) en
|
||||||
|
# twee VERSCHILLENDE termen blijven samen tellen (geen over-merge).
|
||||||
|
("Busbaan wordt afgesloten voor het doorgaande verkeer", "De omleiding duurt tot het voorjaar.", True),
|
||||||
|
("Bus rijdt om, busbaan is afgesloten", "De bussen rijden via de wijk.", True), # bus(1) + busbaan(2) = 3
|
||||||
]
|
]
|
||||||
print(f"== regressie: {len(GOLDEN)} cases (precision-first) ==")
|
print(f"== regressie: {len(GOLDEN)} cases (precision-first) ==")
|
||||||
for t, s, want in GOLDEN:
|
for t, s, want in GOLDEN:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue