Modul 4 der Spezifikation, dazu das Lexikon, auf das es sich stuetzt. Drei Schritte, streng getrennt: die NER liefert Kandidatenspannen, das Lexikon entscheidet den Typ, die Amtstabelle loest Aemter gegen das Datum des Items auf. Die Trennung ist gemessen begruendet und nicht uebernommen: de_core_news_lg haelt "Merz" fuer einen Ort, "Landtagswahl" fuer einen Ort und "Ukraine-Krieg" fuer eine Person. Der NER-Typ steht als ner_typ in der Nutzlast, entscheidet aber nichts. Modellwahl gemessen: sm und lg finden fast gleich viele Entitaeten (14249 gegen 14261), aber nicht dieselben - nur 10,7 Prozent der Items bekommen von beiden dasselbe Ergebnis, der Jaccard ueber alle Spannen liegt bei 0,590. lg findet 170-mal zusaetzlich "AfD" sowie Merz, Putin, Trump, Kushner, Witkoff. Zeitlich kostet das nichts (7,3 gegen 8,0 Sekunden fuer den Gesamtbestand), an Plattenplatz 568 MB. Der Spiegel ist eine Abschrift, keine Auslegung: lexikon/*.json enthaelt die Wikidata-Antworten unveraendert, mit MANIFEST.json als Beleg. Jede Ableitung geschieht in lexikon_laden.py. Nur lexikon.py --spiegeln fasst das Netz an, und es laeuft von Hand - die WDQS ist ein spendenfinanzierter Dienst, kein Endpunkt fuer eine Pipeline im Viertelstundentakt. p2-005 legt akteure und amtsinhaber an, dazu zwei Betriebssichten. Keine Ausschlussbeschraenkung gegen ueberlappende Amtszeiten: Doppelspitzen sind im deutschen Parteiensystem der Normalfall, und Bremen fuehrt "Buergermeister" als kollegiales Amt. Die Sicht amtsinhaber_ueberschneidungen macht Ueberlappungen sichtbar, statt sie zu verbieten. Amtszeiten sind halboffen - der Tag der Uebergabe gehoert dem Nachfolger, sonst waere jeder Amtswechsel eine Ueberschneidung. Messung ueber den Gesamtbestand: 3407 Items in 21,1 Sekunden, 14652 Nennungen, 2889 aufgeloest. Ein Slot-Lauf erzeugt bitgleich dieselbe Nutzlast wie der Gesamtlauf, 0 Abweichungen. spaCy ist stapelunabhaengig (Stapelgroesse 64, 7, 1 und rueckwaerts: identische Spannen), der Kunstgriff mit der festen Tokenlaenge aus dem Embeddingmodul ist hier nicht noetig. Zwei Fallen sind in NOTES.md festgehalten: eine geratene QID haengt einen Eintrag an ein fremdes Objekt (Q157617 ist die Commerzbank, nicht der Bundesrat), und FILTER (lang(?name) = "de") verliert lautlos alles ohne deutsches Label - Apple, Microsoft und Meta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NupWEBPzsyohVPG47HsTz8
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase 2 als Ganzes: alle Module in fester Reihenfolge.
|
|
|
|
python3 phase2.py --backfill
|
|
python3 phase2.py --slot 2026-09-07T08:00
|
|
python3 phase2.py --neueste # der juengste Slot in raw_items
|
|
python3 phase2.py --aufholen # jeder Slot, dem eine Kodierung fehlt
|
|
|
|
Das ist der Einstiegspunkt im Betrieb; die Module lassen sich weiter einzeln
|
|
aufrufen. Die Reihenfolge ist nicht beliebig: die regelbasierten Module sind
|
|
in Millisekunden fertig, das Embedding braucht Modell und Rechenzeit. Bricht
|
|
es ab, stehen die uebrigen Ergebnisse trotzdem.
|
|
|
|
Jedes Modul haelt seine eigene coder_version. Ein Fehlschlag in einem Modul
|
|
laesst die anderen unberuehrt - dafuer ist die Schichtung in codings da.
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
# Reihenfolge der Spezifikation, um die Revisionen ergaenzt. akteure laeuft
|
|
# vor embeddings, weil die Geokodierung spaeter auf seiner NER-Ausgabe
|
|
# aufsetzt - und weil das teuerste Modul zuletzt kommt.
|
|
MODULE = ["dubletten", "revisionen", "akteure", "embeddings"]
|
|
|
|
|
|
def slots_ohne_kodierung(conn):
|
|
"""Slots, in denen Items liegen, die noch keine Dublettenzeile haben.
|
|
|
|
Absichtlich am ersten Modul festgemacht und nicht an allen: laeuft das
|
|
Embedding einmal nicht durch, soll der Aufholvorgang nicht dauerhaft
|
|
dieselben Slots wiederholen. Fehlende Vektoren holt ein Backfill.
|
|
"""
|
|
return [r[0] for r in conn.execute(
|
|
"""select distinct r.slot from raw_items r
|
|
where not exists (select 1 from codings c
|
|
where c.raw_item_id = r.id and c.ebene = 'dublette')
|
|
order by 1""")]
|
|
|
|
|
|
def fuehre_aus(name, argumente):
|
|
modul = __import__(name)
|
|
print(f"\n--- {name} {' '.join(argumente)}")
|
|
try:
|
|
rueck = modul.main(argumente)
|
|
except Exception as fehler: # noqa: BLE001
|
|
# Ein Modul darf die uebrigen nicht mitreissen. Der Fehler steht im
|
|
# Protokoll und im Rueckgabewert, nicht in einem abgebrochenen Lauf.
|
|
print(f" FEHLER in {name}: {fehler.__class__.__name__}: {fehler}",
|
|
file=sys.stderr)
|
|
return 1
|
|
return rueck or 0
|
|
|
|
|
|
def main(argv=None):
|
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
gruppe = p.add_mutually_exclusive_group(required=True)
|
|
gruppe.add_argument("--backfill", action="store_true", help="Gesamtbestand")
|
|
gruppe.add_argument("--slot", help="ein Slot, z.B. 2026-09-07T08:00")
|
|
gruppe.add_argument("--neueste", action="store_true",
|
|
help="der juengste Slot in raw_items")
|
|
gruppe.add_argument("--aufholen", action="store_true",
|
|
help="jeder Slot, dem eine Kodierung fehlt")
|
|
p.add_argument("--ohne", action="append", default=[], metavar="MODUL",
|
|
help="Modul auslassen, mehrfach angebbar")
|
|
p.add_argument("--trocken", action="store_true", help="rechnen, nichts schreiben")
|
|
a = p.parse_args(argv)
|
|
|
|
module = [m for m in MODULE if m not in a.ohne]
|
|
zusatz = ["--trocken"] if a.trocken else []
|
|
|
|
if a.backfill:
|
|
laeufe = [["--backfill"]]
|
|
elif a.slot:
|
|
laeufe = [["--slot", a.slot]]
|
|
else:
|
|
from db import verbindung
|
|
with verbindung() as conn:
|
|
if a.neueste:
|
|
zeile = conn.execute("select max(slot) from raw_items").fetchone()
|
|
slots = [zeile[0]] if zeile and zeile[0] else []
|
|
else:
|
|
slots = slots_ohne_kodierung(conn)
|
|
if not slots:
|
|
print("kein offener Slot")
|
|
return 0
|
|
print(f"{len(slots)} Slot(s): {slots[0]} bis {slots[-1]}")
|
|
laeufe = [["--slot", s.isoformat()] for s in slots]
|
|
|
|
schlecht = 0
|
|
for lauf in laeufe:
|
|
for name in module:
|
|
schlecht += fuehre_aus(name, lauf + zusatz)
|
|
if schlecht:
|
|
print(f"\n{schlecht} Modullauf/-laeufe mit Fehler", file=sys.stderr)
|
|
return 1 if schlecht else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|