Haiku aus dem Nationalrat

Haiku aus den stenographischen Sitzungsprotokollen des Nationalrat extrahieren.

Der korrekte Plural von Haiku ist Haiku.
Der Verständlichkeit halber wird im Code aber Haikus verwendet.

Die direkt vom Parlament veröffentlichten stenographischen Protkolle der Nationalratssitzungen sind schlecht strukturiert. Wir bauen deshalb auf Mario Zechner's Vorarbeiten in seinem Projekt Wos wor mei Leistung? auf. Sauber als JSON strukturierte Sitzungsprotkolle - die Doku direkt auf der Projektseite.

Als erstes Protokolle aus dem gecloned und ausgeführten Repo laden.

In [1]:
import json
import pandas as pd

with open("woswormeileistung/data/sessions.json") as f:
    sessions = json.load(f)

wortmeldungen = pd.json_normalize(
    sessions,
    record_path=["sections"],
    meta=["period", "sessionNumber", "date"],
)

wortmeldungen = wortmeldungen[["period", "sessionNumber", "date", "speaker", "text"]]
wortmeldungen = wortmeldungen.rename(columns={"sessionNumber": "session"})

wortmeldungen.head()
Out[1]:
period session date speaker text
0 XXVII 276 2024-09-18T00:00:00 88386 Präsident Mag. Wolfgang Sobotka: Meine sehr ge...
1 XXVII 276 2024-09-18T00:00:00 88386 Präsident Mag. Wolfgang Sobotka: Meine sehr ge...
2 XXVII 276 2024-09-18T00:00:00 88386 Präsident Mag. Wolfgang Sobotka: Der Herr Bund...
3 XXVII 276 2024-09-18T00:00:00 88386 Präsident Mag. Wolfgang Sobotka: Die Amtlichen...
4 XXVII 276 2024-09-18T00:00:00 88386 Präsident Mag. Wolfgang Sobotka: Ich darf beka...

Die Wortmeldungen enthalten den Namen der sprechenden Person gefolgt von einem Doppelpunkt. Die wollen wir entfernen.

In [2]:
wortmeldungen["text"] = wortmeldungen["text"].str.split(": ", n=1).str[1]
wortmeldungen.head()
Out[2]:
period session date speaker text
0 XXVII 276 2024-09-18T00:00:00 88386 Meine sehr geehrten Damen und Herren Abgeordne...
1 XXVII 276 2024-09-18T00:00:00 88386 Meine sehr geehrten Damen und Herren auf der G...
2 XXVII 276 2024-09-18T00:00:00 88386 Der Herr Bundespräsident hat mit Entschließung...
3 XXVII 276 2024-09-18T00:00:00 88386 Die Amtlichen Protokolle der 272. und der 273....
4 XXVII 276 2024-09-18T00:00:00 88386 Ich darf bekannt geben, dass von der Bundeswah...

Alle möglichen Haiku im Text finden:

  1. Texte mit spacy in Sätze splitten
  2. Wenn möglich aus jedem Satz ein Haiku extrahieren:
    1. Ungeeignete Sätze überspringen. Solche enthalten etwa:
      • Zahlen
      • Klammern
      • Phrasen die auf sehr häufige Aussagen hindeuten, z.B. "zu wort gemeldet" deutet auf die sehr häufige Aufforderung des bzw. der Nationalratspräsident:in zum Beginn einer Rede hin.
    2. Satz in Wörter splitten (Bindestrichwörter dabei nicht trennen)
    3. Zeile für Zeile des Haiku auffüllen und Silben mitzählen. Dabei weitere Heuristiken anwenden um 'schlechte' Haiku rauszufiltern:
      • Satzzeichen (abgesehen von Anführungszeichen und Bindestriche) dürfen nur am Ende einer Haiku-Zeile auftreten
      • Der syntaktische Kopf von Wörtern am Beginn einer Zeile darf nicht auf der vorherigen Zeile liegen. Gleiches gilt für Wörter am Ende einer Zeile - der syntaktische Kopf darf nicht auf der nächsten liegen.
        Das filtert Haiku-Kandidaten mit stark zusammenhängenden Wörtern an den Zeilenenden.
        Was ist der syntaktische Kopf? Im Beispiel "Die Katze sitzt auf der Matte":
        • Die -> Katze
        • Katze -> sitzt
        • auf -> sitzt
        • der -> Matte
        • Matte -> auf
      • Jegliche Satzzeichen abgesehen von den Bindestrichen in Bindestrich-Wörtern nicht in die finalen Haiku-Zeilen aufnehmen
In [4]:
import spacy
from spacy.tokenizer import Tokenizer
from spacy.util import compile_infix_regex
import pyphen
import regex
from tqdm import tqdm

!python -m spacy download de_core_news_lg
#!python -m spacy download de_core_news_sm

nlp = spacy.load("de_core_news_lg")
#nlp = spacy.load("de_core_news_sm")

# bindestrich-wörter nicht trennen
infixes = [x for x in nlp.Defaults.infixes if "-" not in x and "–" not in x and "—" not in x]
infix_re = compile_infix_regex(infixes)
nlp.tokenizer = Tokenizer(
    nlp.vocab,
    rules=nlp.Defaults.tokenizer_exceptions,
    prefix_search=nlp.tokenizer.prefix_search,
    suffix_search=nlp.tokenizer.suffix_search,
    infix_finditer=infix_re.finditer,
)

dic = pyphen.Pyphen(lang="de_DE")


def count_syllables(word: str) -> int:
    parts = word.split("-")
    total = 0
    for part in parts:
        if not part:
            continue
        hyphenated = dic.inserted(part, hyphen="·")
        total += hyphenated.count("·") + 1
    return total


def extract_haiku(sentence):
    pattern = r"^[\p{L} .!?'\":;,–—―]+$"
    if not bool(regex.fullmatch(pattern, sentence)):
        return None
    
    common_phrases = ["zu wort gemeldet", "nächste rednerin", "nächster redner", "damen und herren"]
    if any(phrase in sentence.lower() for phrase in common_phrases):
        return None
    
    tokens = nlp(sentence)
    line_limits = [5, 7, 5]
    line_idx = 0
    line_sum = 0
    extracted_haiku = [[], [], []]

    for i in range(len(tokens)):
        token = tokens[i]
        if token.text in [",", ":", ";", "–", "—", "―"]:
            if line_sum == 0:
                continue
            return None

        if token.is_punct:
            continue

        if line_sum == 0 and token.head.i < i:
            return None

        extracted_haiku[line_idx].append(token.text)
        line_sum += count_syllables(token.text)
        if line_sum > line_limits[line_idx]:
            return None
        
        if line_sum == line_limits[line_idx]:
            if token.head.i > i:
                return None
            
            line_idx += 1
            line_sum = 0
            if line_idx == 3:
                remaining_syllables = sum([count_syllables(t.text) for t in tokens[i + 1 :] if not t.is_punct])
                if remaining_syllables > 0:
                    return None
                break

    return [" ".join(line) for line in extracted_haiku] if line_idx == 3 else None


haikus = []
context_indices = []
for doc in tqdm(nlp.pipe(wortmeldungen["text"].fillna("").astype(str), batch_size=50), total=len(wortmeldungen)):
    haikus.append([extract_haiku(sentence.text) for sentence in doc.sents])
    context_indices.append([(sentence.start_char, sentence.end_char) for sentence in doc.sents])
    
wortmeldungen["lines"] = haikus
wortmeldungen["context_indices"] = context_indices
wortmeldungen.head()
Collecting de-core-news-lg==3.8.0
  Downloading https://github.com/explosion/spacy-models/releases/download/de_core_news_lg-3.8.0/de_core_news_lg-3.8.0-py3-none-any.whl (567.8 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 567.8/567.8 MB 6.8 MB/s  0:01:060:00:0100:03
✔ Download and installation successful
You can now load the package via spacy.load('de_core_news_lg')
100%|██████████| 185983/185983 [4:43:06<00:00, 10.95it/s]   
Out[4]:
period session date speaker text lines context_indices
0 XXVII 276 2024-09-18T00:00:00 88386 Meine sehr geehrten Damen und Herren Abgeordne... [None, None, None, None, None] [(0, 49), (50, 162), (163, 187), (188, 303), (...
1 XXVII 276 2024-09-18T00:00:00 88386 Meine sehr geehrten Damen und Herren auf der G... [None, None, None, None, None, None, None, Non... [(0, 113), (114, 176), (177, 317), (318, 507),...
2 XXVII 276 2024-09-18T00:00:00 88386 Der Herr Bundespräsident hat mit Entschließung... [None, None, None, None, None, None] [(0, 76), (77, 118), (119, 207), (208, 241), (...
3 XXVII 276 2024-09-18T00:00:00 88386 Die Amtlichen Protokolle der 272. und der 273.... [None, None, None, None] [(0, 245), (245, 301), (302, 335), (335, 373)]
4 XXVII 276 2024-09-18T00:00:00 88386 Ich darf bekannt geben, dass von der Bundeswah... [None, None, None, None, None, None, None, Non... [(0, 132), (133, 182), (182, 261), (262, 294),...

Das Datenset auf eine Zeile pro Haiku flatten und den Kontext vor und nach den Haiku richtig setzen.

In [5]:
haikus = wortmeldungen.explode(["lines", "context_indices"]).dropna(subset=["lines"])
haikus["line1"] = haikus["lines"].str[0]
haikus["line2"] = haikus["lines"].str[1]
haikus["line3"] = haikus["lines"].str[2]
haikus["context_before"] = haikus.apply(lambda row: row["text"][: row["context_indices"][0]], axis=1)
haikus["context_after"] = haikus.apply(lambda row: row["text"][row["context_indices"][1] :], axis=1)
haikus = haikus.drop(columns=["text", "context_indices", "lines"])
haikus.head()
Out[5]:
period session date speaker line1 line2 line3 context_before context_after
535 XXVII 274 2024-07-05T00:00:00 83059 Um Gottes Willen es geht um Interessen der Konsumenten Sehr geehrter Herr Präsident! Frau Bundesminis... Die müssen ja wissen, wohin sie sich wenden k...
843 XXVII 272 2024-07-04T00:00:00 83113 Niemand von der ÖVP würde sich hinstellen und ganz offen sagen Herr Präsident! Geschätzter Herr Bundesministe... Ich bin für die Zweiklassenmedizin! Oder: Es ...
1007 XXVII 272 2024-07-04T00:00:00 35520 Sie ticken also genauso machtbesessen wie die Giftgrünen Herr Präsident! Meine Damen und Herren auf der... (Abg. Michael Hammer: ... der Giftzwerg!)\nDa...
1158 XXVII 272 2024-07-04T00:00:00 83151 Wir haben jetzt schon große Stauprobleme auf dieser Autobahn Herr Präsident! Sehr geehrte Frau Ministerin! ... Wenn natürlich nur noch die Hälfte der Kapazi...
1534 XXVII 270 2024-07-03T00:00:00 83124 In diesem Sinne volle Unterstützung von unserer Seite Also die Themenlage, wenn man über den Katastr... – Vielen Dank. (Beifall bei den NEOS.)\n18.26

Die Gesetzgebungsperiode zusätzlich als arabische Zahl abspeichern.

In [6]:
import roman

haikus["period_roman"] = haikus["period"]
haikus["period"] = haikus["period"].apply(lambda periodRoman: roman.fromRoman(periodRoman))
haikus.head()
Out[6]:
period session date speaker line1 line2 line3 context_before context_after period_roman
535 27 274 2024-07-05T00:00:00 83059 Um Gottes Willen es geht um Interessen der Konsumenten Sehr geehrter Herr Präsident! Frau Bundesminis... Die müssen ja wissen, wohin sie sich wenden k... XXVII
843 27 272 2024-07-04T00:00:00 83113 Niemand von der ÖVP würde sich hinstellen und ganz offen sagen Herr Präsident! Geschätzter Herr Bundesministe... Ich bin für die Zweiklassenmedizin! Oder: Es ... XXVII
1007 27 272 2024-07-04T00:00:00 35520 Sie ticken also genauso machtbesessen wie die Giftgrünen Herr Präsident! Meine Damen und Herren auf der... (Abg. Michael Hammer: ... der Giftzwerg!)\nDa... XXVII
1158 27 272 2024-07-04T00:00:00 83151 Wir haben jetzt schon große Stauprobleme auf dieser Autobahn Herr Präsident! Sehr geehrte Frau Ministerin! ... Wenn natürlich nur noch die Hälfte der Kapazi... XXVII
1534 27 270 2024-07-03T00:00:00 83124 In diesem Sinne volle Unterstützung von unserer Seite Also die Themenlage, wenn man über den Katastr... – Vielen Dank. (Beifall bei den NEOS.)\n18.26 XXVII

Das Personen Datenset verknüpfen.

In [7]:
personen = pd.read_json("woswormeileistung/data/persons.json")
personen = personen[["id", "name", "parties", "imageUrl"]]
personen = personen.rename(columns={"imageUrl": "image_url"})
personen["id"] = personen["id"].astype(str)
haikus = haikus.merge(personen, left_on="speaker", right_on="id", how="left")
haikus = haikus.drop(columns=["id"])
haikus = haikus.rename(columns={"name": "person_name", "speaker": "person_id"})
haikus["parties"] = haikus["parties"].apply(
    lambda x: x if isinstance(x, list) and len(x) > 0 else ["Ohne Klub"]
)
haikus.head()
Out[7]:
period session date person_id line1 line2 line3 context_before context_after period_roman person_name parties image_url
0 27 274 2024-07-05T00:00:00 83059 Um Gottes Willen es geht um Interessen der Konsumenten Sehr geehrter Herr Präsident! Frau Bundesminis... Die müssen ja wissen, wohin sie sich wenden k... XXVII Mag. Michaela Steinacker [ÖVP] https://parlament.gv.at/dokument/bild/200697/2...
1 27 272 2024-07-04T00:00:00 83113 Niemand von der ÖVP würde sich hinstellen und ganz offen sagen Herr Präsident! Geschätzter Herr Bundesministe... Ich bin für die Zweiklassenmedizin! Oder: Es ... XXVII Philip Kucher [SPÖ] https://parlament.gv.at/dokument/bild/201238/2...
2 27 272 2024-07-04T00:00:00 35520 Sie ticken also genauso machtbesessen wie die Giftgrünen Herr Präsident! Meine Damen und Herren auf der... (Abg. Michael Hammer: ... der Giftzwerg!)\nDa... XXVII Herbert Kickl [FPÖ] https://parlament.gv.at/dokument/bild/201134/2...
3 27 272 2024-07-04T00:00:00 83151 Wir haben jetzt schon große Stauprobleme auf dieser Autobahn Herr Präsident! Sehr geehrte Frau Ministerin! ... Wenn natürlich nur noch die Hälfte der Kapazi... XXVII Dipl.-Kffr. (FH) Elisabeth Pfurtscheller [ÖVP] https://parlament.gv.at/dokument/bild/200697/2...
4 27 270 2024-07-03T00:00:00 83124 In diesem Sinne volle Unterstützung von unserer Seite Also die Themenlage, wenn man über den Katastr... – Vielen Dank. (Beifall bei den NEOS.)\n18.26 XXVII Michael Bernhard [NEOS] https://parlament.gv.at/dokument/bild/201426/2...

Für jedes Haiku eine möglichst stabile ID erzeugen. Diese ist ein Hash aus:

  • Gesetzgebungsperiode
  • Sitzungsnummer
  • Person-ID
  • die drei Haiku-Zeilen
In [8]:
import hashlib

def hash(row):
    combined = (
        f"{row['period']} {row['session']} {row["person_id"]} {row["line1"]} {row["line2"]} {row["line3"]}"
    )
    return hashlib.sha256(combined.encode()).hexdigest()

haikus["id"] = haikus.apply(hash, axis=1)
haikus.head()
Out[8]:
period session date person_id line1 line2 line3 context_before context_after period_roman person_name parties image_url id
0 27 274 2024-07-05T00:00:00 83059 Um Gottes Willen es geht um Interessen der Konsumenten Sehr geehrter Herr Präsident! Frau Bundesminis... Die müssen ja wissen, wohin sie sich wenden k... XXVII Mag. Michaela Steinacker [ÖVP] https://parlament.gv.at/dokument/bild/200697/2... 1c55402e5c33637b0bd2675df4cf7198adc16cfa3e4462...
1 27 272 2024-07-04T00:00:00 83113 Niemand von der ÖVP würde sich hinstellen und ganz offen sagen Herr Präsident! Geschätzter Herr Bundesministe... Ich bin für die Zweiklassenmedizin! Oder: Es ... XXVII Philip Kucher [SPÖ] https://parlament.gv.at/dokument/bild/201238/2... fd1b95922b526ef56fea38a1d2ec02b3a0f082c585ceec...
2 27 272 2024-07-04T00:00:00 35520 Sie ticken also genauso machtbesessen wie die Giftgrünen Herr Präsident! Meine Damen und Herren auf der... (Abg. Michael Hammer: ... der Giftzwerg!)\nDa... XXVII Herbert Kickl [FPÖ] https://parlament.gv.at/dokument/bild/201134/2... 4bf6999233cc9f6b025d3ec8cda91f12190217b871ff77...
3 27 272 2024-07-04T00:00:00 83151 Wir haben jetzt schon große Stauprobleme auf dieser Autobahn Herr Präsident! Sehr geehrte Frau Ministerin! ... Wenn natürlich nur noch die Hälfte der Kapazi... XXVII Dipl.-Kffr. (FH) Elisabeth Pfurtscheller [ÖVP] https://parlament.gv.at/dokument/bild/200697/2... 90256bd7dc16b1ac1bdfecd0d1fc7f1b6636aa7a37f656...
4 27 270 2024-07-03T00:00:00 83124 In diesem Sinne volle Unterstützung von unserer Seite Also die Themenlage, wenn man über den Katastr... – Vielen Dank. (Beifall bei den NEOS.)\n18.26 XXVII Michael Bernhard [NEOS] https://parlament.gv.at/dokument/bild/201426/2... 37ccf54dad446bdc00827b93fac581fd138286b035b236...

Gibt es Kollisionen bei den Haiku-IDs nur das chronologisch älteste Haiku behalten.
Da die Haiku-ID Gesetzgebungsperiode, Sitzungsnummer und Person inkludiert muss es sich bei Haiku-ID-Kollisionen um ein in einem Redebeitrag mehrmals vorkommendes handeln. Dadurch reicht die Länge des Kontexts vor dem Haiku um das "älteste" zu identifizieren.

In [9]:
haikus["context_before_length"] = haikus["context_before"].str.len()
haikus = haikus.sort_values(by=["date", "context_before_length"])
haikus = haikus.drop_duplicates(subset=["id"], keep="first")
haikus = haikus.drop(columns=["context_before_length"])
haikus.head()
Out[9]:
period session date person_id line1 line2 line3 context_before context_after period_roman person_name parties image_url id
544 22 3 2003-01-23T00:00:00 1817 Nehmen Sie es so wie es im Gesetz steht und wie es gemeint ist Herr Präsident! Frau Vizekanzlerin! Frau Bunde... Es soll mit dieser Änderung eine zeitlich beg... XXII Dr. Michael Spindelegger [ÖVP] https://parlament.gv.at/dokument/bild/43871/43... 643114295391a11250d6677019be80863968eb867c6330...
543 22 7 2003-03-06T00:00:00 8178 Mutlos ist leider auch die Weiterentwicklung des Kindergeldes Sehr geehrte Damen und Herren! Wenn man sich d... Es wird nur von Evaluierung gesprochen. Sie h... XXII Mag. Andrea Kuntzl [SPÖ] https://parlament.gv.at/dokument/bild/200697/2... ddb773f0ee5da8848733d53e738844241c198c1594e2cf...
542 22 7 2003-03-06T00:00:00 1933 Es ist kein Zufall dass der Herr Bundeskanzler heute gesagt hat Herr Präsident! Herr Bundeskanzler! Meine Dame... Für die Zukunft brauchen wir Verantwortung. –... XXII Dipl.-Kfm. Dr. Günter Stummvoll [ÖVP] https://parlament.gv.at/dokument/bild/34886/34... 20de432d3a7efcfcdc8c7a971f0aea3d911ce04cef5ce3...
541 22 9 2003-03-19T00:00:00 14693 Wir haben gesagt eigentlich ist das von uns zu unterstützen Herr Präsident! Herr Bundeskanzler! Hohes Haus... Dann haben wir abgezählt und gesagt, schaut, ... XXII Barbara Rosenkranz, MA [BZÖ, FPÖ] https://parlament.gv.at/dokument/bild/44447/44... 555b94c4d9629d0264b2b06c19c9d52b12520b4d73f4f3...
540 22 10 2003-03-26T00:00:00 444 Dieses Schlagwort ist im wahrsten Sinn des Wortes schlagend geworden Meine Herren Präsidenten! Frau Bundesministeri... (Abg. Dr. Mitterlehner: Es hat Sie erwischt!)... XXII Heinz Gradwohl [SPÖ] https://parlament.gv.at/dokument/bild/20997/20... b95982bb3546d9300f36e8a90e942fa535f207f7043f8b...

Alle gefundenen Haiku als JSON speichern.

In [ ]:
haikus.to_json("web/db/haikus.json", orient="records", force_ascii=False, indent=2)