Sentimentanalyse mit Senti Ws

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Vesemir
User
Beiträge: 1
Registriert: Montag 6. April 2026, 16:39

Hallo :) ich möchte Reden mit einer Sentimentanalyse auswerten (SentiWS - https://www.kaggle.com/datasets/sibeliu ... ositiv.csv). Ich habe schon Code und möchte gerne wissen, ob und wie ich den reduzieren kann, da ich das Gefühl habe, dass der Code viel zu umständlich ist. Ich möchte als Ergebnis haben, ob das Sentiment je Rede eines Sprechers negativ (negative Zahl), positiv (positive Zahl)oder neutral (0) ist (Werte werden in Zahlen angegeben). Das habe ich bereits:

files = [ Rede.txt]
data = []

for file in files:

with open(file, encoding="utf-8") as f:

text = f.read()

year, speaker = file.replace(".txt","").split("_", 1)

data.append({

"Jahr": int(year),

"Sprecher": speaker,

"Text": text

})

Neujahrsansprachen = pd.DataFrame(data)

Neujahrsansprachen.head()

import pandas as pd

import re

import nltk

from nltk.corpus import stopwords

# Stopwörter laden

nltk.download("stopwords")

german_stop_words = set(stopwords.words("german"))

# SentiWS laden

def load_sentiws_simple(path):

df = pd.read_csv(path, sep=None, engine="python", header=0)

senti = {}

for _, row in df.iterrows():

word = str(row[1]).lower()

score = float(row[2])

senti[word] = score

return senti

senti_neg = load_sentiws_simple("SentiWS_ML_negativ.csv")

senti_pos = load_sentiws_simple("SentiWS_ML_positiv.csv")

senti_dict = {**senti_neg, **senti_pos}

print("Wörter im Lexikon:", len(senti_dict))

# ---------------------------------------------------------

# Tokenisierung

# ---------------------------------------------------------

def tokenize(text):

words = re.findall(r"[a-zA-ZäöüÄÖÜß]+", text.lower())

return [w for w in words if w not in german_stop_words]

Neujahrsansprachen["Tokens"] = Neujahrsansprachen["Text"].apply(tokenize)

# Sentiment pro Rede

def sentiws_score(tokens):

return sum(senti_dict.get(w, 0) for w in tokens)

Neujahrsansprachen["SentiWS_Score"] = Neujahrsansprachen["Tokens"].apply(sentiws_score)

# Sentiment pro Sprecher

sentiment_by_speaker = (

Neujahrsansprachen

.groupby("Sprecher")["SentiWS_Score"]

.mean()

.sort_values(ascending=False)

)

sentiment_by_speaker
Tim12
User
Beiträge: 2
Registriert: Dienstag 7. April 2026, 10:45

Dein Code funktioniert, ist aber etwas umständlich geschrieben. Man kann ihn deutlich vereinfachen, indem man Listenverständnisse statt Schleifen nutzt und Funktionen klar trennt (z. B. für Tokenisierung und Bewertung). Außerdem lässt sich das Laden des SentiWS-Lexikons effizienter mit dict(zip(...)) lösen. Insgesamt wird der Code dadurch kürzer. schneller und besser lesbar.
Sirius3
User
Beiträge: 18399
Registriert: Sonntag 21. Oktober 2012, 17:20

Code postet man hier in code-Tags </>. So wie jetzt ist der Code unlesbar.

Importe stehen immer am Anfang der Datei, dann folgen Konstanten und Funktionsdefinitionen.
Das Hauptprogramm steht auch in einer Funktion, die üblicherweise `main` genannt wird.
Variablennamen werden komplett klein geschrieben.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1339
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Ohne NLTK

Code: Alles auswählen

#!/usr/bin/env python3

import csv
import pickle
import sys

from functools import cache
from pathlib import Path


@cache
def load_word_lists(pos_list_file, neg_list_file):
    file_cache = Path.home().joinpath(".cache", "positive_negative_word_list.pickle")

    if file_cache.exists():
        with file_cache.open("rb") as fd:
            return pickle.load(fd)

    results = []
    for file in (pos_list_file, neg_list_file):
        with open(file, encoding="utf8", newline="") as fd:
            reader = csv.reader(fd)
            data = {}

            for _, word, value in reader:
                try:
                    value = float(value)
                except ValueError:
                    continue
                else:
                    data[word.lower()] = value

            results.append(data)

    with file_cache.open("wb") as fd:
        pickle.dump(results, fd)

    return results


def check(text, positive_words, negative_words):
    result = 0.0

    for word in text.split():
        result += positive_words.get(word.lower(), 0)
        result += negative_words.get(word.lower(), 0)

    return result


if __name__ == "__main__":
    pos_word_list = Path.home().joinpath("Downloads/SentiWS_ML_positiv.csv")
    neg_word_list = Path.home().joinpath("Downloads/SentiWS_ML_negativ.csv")
    pos, neg = load_word_lists(pos_word_list, neg_word_list)
    text = sys.stdin.read()
    result = check(text, pos, neg)
    print(result)

Kann man dann so ausführen:

echo "Guten Tag" | python check.py
0.3716

Ggf. kann man das Ergebnis verbessern, wenn man str.casefold verwendet. Wörter, die nicht im positive- oder negative-dict vorkommen, liefern eine 0 (get Methode).

NLTK kann einem helfen Wörter aus dem Text zu filtern, was aber nicht notwendig ist, da nicht vorhandene Wörter als 0 zählen.


Anstatt die Textdatei mit einer Pipe zu übergeben, könnte man sie auch in Python direkt laden.
Der Datei-Cache ist bei so wenig Daten nicht notwendig. Habs trotzdem mal in die Funktion eingebaut, um das Prinzip zu zeigen.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Sirius3
User
Beiträge: 18399
Registriert: Sonntag 21. Oktober 2012, 17:20

@DeaD_EyE: der halbe Code beschäftigt sich nur mit Caching, was wie Du selbst sagst, nicht notwendig ist. Zudem muß man dann wissen, dass man die Cache-Daten löschen muß, wenn man andere Scores benutzen möchte.
Eine Funktion sollte eine Sache machen, `load_word_lists` liest dagegen zwei Dateien.
Fehler einfach zu ignorieren ist selten eine gute Idee. Statt den ValueError zu ignorieren mußt Du nur die Header-Zeile explizit einlesen.
`text.split()` hat das Problem, dass alle Wörter am Ende von Sätzen, Aufzählungen, etc. bei der Analyse ignoriert werden.
Das Hauptprogramm steht üblicherweise in einer Funktion `main`.
Um zu vermeiden, dass bei vielen Dateien die Listen immer wieder gelesen werden, könnte man ja erlauben, viele Dateinamen per Argument zu übergeben.

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import re
import sys

POSITIVE_WORD_SCORE_FILE = "Downloads/SentiWS_ML_positiv.csv"
NEGATIVE_WORD_SCORE_FILE = "Downloads/SentiWS_ML_negativ.csv"

def load_word_list(filename):
    with open(filename, encoding="utf8", newline="") as file:
        reader = csv.reader(file)
        _header = next(reader)
        return {
            word: float(score)
            for _, word, score in reader
        }


def calulate_score(text, positive_word_scores, negative_word_scores):
    return sum(
        positive_word_scores.get(word, 0)
        + negative_word_scores.get(word, 0)
        for word in re.findall(r"\w+", text)
    )


def main():
    positive_word_scores = load_word_list(POSITIVE_WORD_SCORE_FILE)
    negative_word_scores = load_word_list(NEGATIVE_WORD_SCORE_FILE)
    for filename in sys.args[1:]:
        with open(filename, encoding="utf8") as file:
            text = file.read()
        print(calulate_score(text, positive_word_scores, negative_word_scores), filename)


if __name__ == "__main__":
    main()
Benutzeravatar
DeaD_EyE
User
Beiträge: 1339
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Sirius3 hat geschrieben: Mittwoch 8. April 2026, 10:22 @DeaD_EyE: der halbe Code beschäftigt sich nur mit Caching, was wie Du selbst sagst, nicht notwendig ist. Zudem muß man dann wissen, dass man die Cache-Daten löschen muß, wenn man andere Scores benutzen möchte.
Stimmt, der Cache würde nie überschrieben werden. Es wird auch nicht geprüft, ob der Cache älter ist, als eine der beiden Eingabedateien.
Eine Funktion sollte eine Sache machen, `load_word_lists` liest dagegen zwei Dateien.
Genau genommen macht die Funktion mehr. Cache lesen, wenn vorhanden, falls nicht vorhanden, Dateien lesen und Cache schreiben.

Fehler einfach zu ignorieren ist selten eine gute Idee. Statt den ValueError zu ignorieren mußt Du nur die Header-Zeile explizit einlesen.
Ich war zu faul mir den Inhalt der Datei anzusehen.
`text.split()` hat das Problem, dass alle Wörter am Ende von Sätzen, Aufzählungen, etc. bei der Analyse ignoriert werden.

Code: Alles auswählen

re.sub(r"\W", "", "§$%&ÜberMirkowelle$%&$/&%/%")
'ÜberMirkowelle'

Btw. wenn ich morgens auf einen Beitrag antworte, ist das meist vor der Arbeit. Heute hatte ich nur ein paar Minuten.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
__blackjack__
User
Beiträge: 14374
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Vesemir: Ich probiere mich dann auch mal am Original. Es wurde ja schon einiges gezeigt und gesagt.

Funktionen bekommen alles was sie ausser Konstanten benötigen, als Argument(e) übergeben. Globale Variablen und dann Funktionen die einfach magisch auf solche zugreifen sind schlecht, weil unübersichtlich und fehleranfällig. `tokenize()` braucht beispielsweise die Stoppworte als Argument, statt einfach so auf irgendwo in der ”Umgebung” existierende Stoppworte zuzugreifen.

Pandas zum Einlesen der SentiWS-CSV-Dateien ist Overkill. Bei Pandas sollte man immer wenn man `iterrows()` verwendet sowieso mal kurz innehalten und überlegen was man da macht, denn das ist genau das was man in Pandas ja eigentlich nicht machen will — selber manuell über alle Zeilen iterieren.

Insgesamt wird Pandas hier arg missbraucht, denn auch das Speichern von Listen in einzelnen Zellen ist nicht schön oder sinnvoll. Zudem ist das auch nur ein Zwischenwert, der nicht wirklich verwendet wird in dem DataFrame.

Einbuchstabige Namen sind in der Regel keine guten Namen. Zeichen in Namen kosten nichts, und man sollte eher auf Lesbarkeit Wert legen denn auf wenig Tippen. Quelltext wird deutlich häufiger gelesen als geschrieben, und mit Autovervollständigung in Editoren kann man auch nicht mehr wirklich mit zu viel Mehrarbeit beim schreiben argumentieren.

Grunddatentypen haben in Namen nichts zu suchen. Den Leser interessiert ja in aller Regel nicht was für einen Typ er da hat, sondern was der Wert bedeutet. Bei Abbildungen wie Wörterbüchern bietet es sich an im Namen zu kodieren was die Schlüssel und die Werte bedeuten. Also beispielsweise `word_to_score` statt `senti_dict`, weil dort Worte auf Bewertungen abgebildet werden.

Sirius3 hat \w+ als regulären Ausdruck für Worte verwendet. Das ist nicht einfach nur kürzer, sondern erfasst nicht nur Umlaute und ß. Auch in ”deutschen” Worten können andere Zeichen, zum Beispiel mit Accents vorkommen.

Das `sentiment_by_speaker` am Ende sollte man vielleicht auch ausgeben. Sonst passiert an der Stelle einfach nichts.

`file` ist ein passender Name für ein Dateiobjekt, wo man so Methoden wie `close()` oder `read()` erwartet, aber nicht für einen Datei*namen*. Das wäre `filename`. Das wurde dadurch umschifft, dass das eigentliche `file` im Quelltext `f` genannt wurde.

`str.replace()` ersetzt in der gesamten Zeichenkette, nicht nur am Ende. Wenn man etwas am Ende entfernen möchte, nimmt man `str.removesuffix()`. Wenn man mit Dateinamen und -pfaden arbeitet, sollte man aber am besten `pathlib.Path` verwenden und da nicht drauf operieren als wären es beliebige Zeichenketten.

`str.split()` das auf eine Trennstelle begrenzt wird, ist eigentlich `str.partition()`.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import re
from pathlib import Path

import nltk
import pandas as pd
from nltk.corpus import stopwords


def load_sentiws_simple(path):
    with open(path, encoding="utf-8", newline="") as file:
        return {
            word.lower(): float(score) for _, word, score in csv.reader(file)
        }


def tokenize(stop_words, text):
    return [
        match[0]
        for match in re.finditer(r"\w+", text.lower())
        if match[0] not in stop_words
    ]


def calculate_sentiws_score(word_to_score, tokens):
    return sum(word_to_score.get(token, 0) for token in tokens)


def main():
    filenames = ["2025_Bundespräsident.txt"]

    nltk.download("stopwords")
    german_stop_words = set(stopwords.words("german"))
    word_to_score = {
        **load_sentiws_simple("SentiWS_ML_negativ.csv"),
        **load_sentiws_simple("SentiWS_ML_positiv.csv"),
    }
    print("Wörter im Lexikon:", len(word_to_score))

    speeches = []
    for path in map(Path, filenames):
        year, _, speaker = path.name.partition("_")
        text = path.read_text(encoding="utf-8")
        speeches.append(
            {
                "Jahr": int(year),
                "Sprecher": speaker,
                "Text": text,
                "SentiWS_Score": calculate_sentiws_score(
                    word_to_score, tokenize(german_stop_words, text)
                ),
            }
        )

    sentiment_by_speaker = (
        pd.DataFrame(speeches)
        .groupby("Sprecher")["SentiWS_Score"]
        .mean()
        .sort_values(ascending=False)
    )
    print(sentiment_by_speaker)


if __name__ == "__main__":
    main()
Pandas könnte man mit ein bisschen Code hier auch noch loswerden mit der `sort()`-Methode auf Listen oder der `sorted()`-Funktion und `itertools.groupby()` und `statistics.mean()` aus der Standardbibliothek.

Ebenfalls ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import re
from itertools import groupby
from operator import itemgetter
from pathlib import Path
from statistics import mean

import nltk
from nltk.corpus import stopwords


def load_sentiws_simple(path):
    with open(path, encoding="utf-8", newline="") as file:
        return {
            word.lower(): float(score) for _, word, score in csv.reader(file)
        }


def tokenize(stop_words, text):
    return [
        match[0]
        for match in re.finditer(r"\w+", text.lower())
        if match[0] not in stop_words
    ]


def calculate_sentiws_score(word_to_score, tokens):
    return sum(word_to_score.get(token, 0) for token in tokens)


def main():
    filenames = ["2025_Bundespräsident.txt"]

    nltk.download("stopwords")
    german_stop_words = set(stopwords.words("german"))
    word_to_score = {
        **load_sentiws_simple("SentiWS_ML_negativ.csv"),
        **load_sentiws_simple("SentiWS_ML_positiv.csv"),
    }
    print("Wörter im Lexikon:", len(word_to_score))

    speeches = []
    for path in map(Path, filenames):
        year, _, speaker = path.name.partition("_")
        text = path.read_text(encoding="utf-8")
        speeches.append(
            {
                "Jahr": int(year),
                "Sprecher": speaker,
                "Text": text,
                "SentiWS_Score": calculate_sentiws_score(
                    word_to_score, tokenize(german_stop_words, text)
                ),
            }
        )

    get_speaker = itemgetter("Sprecher")
    speeches.sort(key=get_speaker)
    sentiment_by_speaker = [
        (mean(speech["SentiWS_Score"] for speech in group), speaker)
        for speaker, group in groupby(speeches, get_speaker)
    ]
    sentiment_by_speaker.sort(reverse=True)
    for mean_score, speaker in sentiment_by_speaker:
        print(f"{speaker:>20} {mean_score}")


if __name__ == "__main__":
    main()
Who is General Failure and why is he reading my hard disk?
Benutzeravatar
__blackjack__
User
Beiträge: 14374
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Einen hab ich noch: Wenn man Pandas rauswirft, hat man Wörterbücher mit einem festen Satz an Zeichenketten als Schlüssel, also eigentlich Objekte und keine Wörterbücher. Also bietet es sich an da tatsächlich Objekte draus zu machen (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import re
from itertools import groupby
from operator import attrgetter
from pathlib import Path
from statistics import mean

import nltk
from attrs import frozen
from nltk.corpus import stopwords


@frozen
class Speech:
    year: int
    speaker: str
    text: str
    score: float

    @classmethod
    def from_file_path(cls, stop_words, word_to_score, path):
        year, _, speaker = path.name.partition("_")
        text = path.read_text(encoding="utf-8")
        return cls(
            int(year),
            speaker,
            text,
            calculate_sentiws_score(word_to_score, tokenize(stop_words, text)),
        )


def load_sentiws_simple(path):
    with open(path, encoding="utf-8", newline="") as file:
        return {
            word.lower(): float(score) for _, word, score in csv.reader(file)
        }


def tokenize(stop_words, text):
    return [
        match[0]
        for match in re.finditer(r"\w+", text.lower())
        if match[0] not in stop_words
    ]


def calculate_sentiws_score(word_to_score, tokens):
    return sum(word_to_score.get(token, 0) for token in tokens)


def main():
    filenames = ["2025_Bundespräsident.txt"]

    nltk.download("stopwords")
    german_stop_words = set(stopwords.words("german"))
    word_to_score = {
        **load_sentiws_simple("SentiWS_ML_negativ.csv"),
        **load_sentiws_simple("SentiWS_ML_positiv.csv"),
    }
    print("Wörter im Lexikon:", len(word_to_score))

    get_speaker = attrgetter("speaker")
    speeches = sorted(
        (
            Speech.from_file_path(german_stop_words, word_to_score, path)
            for path in map(Path, filenames)
        ),
        key=get_speaker,
    )

    sentiment_by_speaker = sorted(
        (
            (mean(speech.score for speech in group), speaker)
            for speaker, group in groupby(speeches, get_speaker)
        ),
        reverse=True,
    )
    speaker_width = max(len(speaker) for _, speaker in sentiment_by_speaker)
    for mean_score, speaker in sentiment_by_speaker:
        print(f"{speaker:>{speaker_width}} {mean_score}")


if __name__ == "__main__":
    main()
Who is General Failure and why is he reading my hard disk?
Antworten