Table of Contents | ||
---|---|---|
|
Versuchsziel
...
Abstract
In dieser Untersuchung wurde getestet, ob ein LLM basierend auf vorgegebenen Metadaten (Beschreibungstexte und Volltexte von Bildungsinhalten), die Neutralität eines Datensatzes auf einer vorgegebenen Skala von 0 bis 5 in vergleichbarer Form bewertet, wie dies zuvor durch Fachredaktionen erfolgt ist.
Erstellung des Test-Datensatzes
Grundlage sind Datensätze mit Bildungsinhalten der Plattform www.Wirlernenonline.de.
Für eine Teilmenge der Datensätze liegen Bewertungen für Neutralität vor. Diese wurden auf einer durch die Redaktionen aufgestellten Skala von 0 bis 5 bewertet und werden zur Qualitätseinschätzung genutzt.
...
Abruf der Daten
Die Daten wurden über die WLO Rest-API-Schnittstelle mit der Customsuche abgerufen.
Dazu wurde eine Kombination des Felds: ccm:oeh_quality_neutralness mit den Werten: 0, 1, 2, 3, 4, 5 genutzt. Der Datensatz wurde als JSON-Datei gespeichert.
Anreicherung der Datensätze mit Volltexten
In den Datensätzen sind Beschreibungstexte der Bildungsinhalte enthalten. Um einen Vergleich mit Volltexten durchführen zu können, wurden die URL aus dem Feld: ccm:wwwurl ausgelesen und die Inhalte der Webseiten extrahiert.
Zum Einsatz kam hierbei die Python Bibliothek Goose3, die intern BeautifulSoup nutzt und überflüssige Textbestandteile z.B. zur Struktur der Webseite mittels NLP-Techniken entfernt. Das Script wird im Anhang aufgeführt.
Die Volltexte, sowie Zusammenfassungen und Keywords wurden dann als additional_data in die JSON gespeichert und den Datensätzen zugeordnet.
Bei Datensätzen, für die Goose3 keinen Volltext generieren konnte (z.B. aufgrund eines sehr kleinen Textkorpus), wurden die Zusammenfassungen genutzt, die von Goose3 aus allen verfügbaren Infos der Webseiten gebildet werden.
Filterung der Rohdaten
Aus den zuvor erstellten Rohdaten wurde ein Test-Datensatz erstellt, der die notwendigen Kriterien erfüllt, u.a. nicht-leere Felder für die im Test relevanten Metadaten sowie eine Mindestlänge der für den Test genutzten Textfelder (Beschreibungstexte und Volltexte). Dieser Schritt soll sicherstellen, dass ausreichend Textmaterial für eine Bewertung vorliegt.
Verarbeitungsschritte für den Datensatz:
...
Entfernung von Datensätzen, bei denen folgende Felder nicht mit Werten gefüllt sind:
properties.cclom:general_description (Beschreibungstexte)
additional_data.full_text (Volltexte)
properties.ccm:oeh_quality_neutralness (Neutralitäts-Score der Redaktionen)
...
Large Language Model (LLM) zur Bewertung der Neutralität von Bildungsinhalten genutzt werden kann, indem es diese vergleichbar wie ein Mensch bewertet.
Dabei wurden 2000 Datensätze, bestehend aus Beschreibungstexten und Volltexten, mithilfe eines angepassten Prompts durch ein KI-Modell bewertet und die Ergebnisse mit bereits vorhandenen redaktionellen Bewertungen verglichen. Die Analyse zeigte, dass die KI-Bewertung, insbesondere bei kürzeren Beschreibungstexten, eine hohe Übereinstimmung mit den menschlichen Bewertungen aufwies (MAE = 0,62). Bei den längeren und komplexeren Volltexten hingegen fiel die Abweichung größer aus (MAE = 0,92). Mögliche Ursachen für das schlechtere Abschneiden der Volltexte können Bewertungen sein, die von den Fachredaktionen in die Texte eingeflossen sind oder eine höhere Textqualität gegenüber Volltexte (trotz geringerer Zeichenlänge). Dennoch zeigt die Untersuchung weiteren Forschungsbedarf auf, insbesondere in Bezug auf die Erfassung von komplexen Inhalten in Volltexten und die Entwicklung eines besseren Messinstruments.
Erstellung des Test-Datensatzes
Die Grundlage dieser Untersuchung bildeten 2000 Datensätze von Bildungsinhalten, die von der Plattform http://WirLernenOnline.de abgerufen wurden und für die bereits redaktionelle Bewertungen zur Neutralität vorlagen.
Diese wurden auf einer Skala von 0 bis 5 eingeordnet, wobei 0 für manipulative oder verfassungswidrige Inhalte und 5 für vollständig neutrale und wissenschaftlich fundierte Inhalte steht.
...
Abruf der Rohdaten
Die Daten wurden über die REST-API der Plattform extrahiert. Dabei wurde das Feld cccm:oeh_quality_neutralnessgenutzt, um Inhalte nach den vorhandenen Neutralitätsscores (0-5) zu filtern. Diese Daten wurden in einer JSON-Datei gespeichert und für die weitere Analyse vorbereitet.
Anreicherung der Rohdaten mit Volltexten
In den Datensätzen sind Beschreibungstexte der Bildungsinhalte enthalten. Um einen Vergleich mit Volltexten durchführen zu können, wurden die URL aus dem Feld: ccm:wwwurl ausgelesen und die Inhalte der Webseiten extrahiert.
Zum Einsatz kam hierbei die Python Bibliothek Goose3, die intern BeautifulSoup nutzt und überflüssige Textbestandteile z.B. zur Struktur der Webseite mittels NLP-Techniken entfernt. Das Script wird im Anhang aufgeführt.
Die Volltexte, sowie Zusammenfassungen und Keywords wurden dann als additional_data in die JSON gespeichert und den Datensätzen zugeordnet.
Bei Datensätzen, für die Goose3 keinen Volltext generieren konnte (z.B. aufgrund eines sehr kleinen Textkorpus), wurden die Zusammenfassungen genutzt, die von Goose3 aus allen verfügbaren Infos der Webseiten gebildet werden.
Filterung der Rohdaten
Zusätzlich zu den Beschreibungstexten wurden Volltexte von den zugehörigen Webseiten der Bildungsinhalte abgerufen. Die URLs, die im Feld ccm:wwwurlgespeichert sind, wurden genutzt, um die Inhalte mittels der Python-Bibliothek Goose3 zu extrahieren (basiert auf beautifulsoup). Diese Bibliothek entfernt unnötige Textbestandteile und extrahiert den Hauptinhalt der Webseiten. Für Datensätze, bei denen keine vollständigen Texte erfasst werden konnten, wurden Zusammenfassungen genutzt, die auf den verfügbaren Informationen der Webseiten basieren.
Filterung der Rohdaten: Die Rohdaten wurden gefiltert, um sicherzustellen, dass alle relevanten Felder gefüllt und die Mindestlänge der für die Bewertung genutzten Textfelder gewährleistet ist. Dabei wurden folgende Kriterien angewendet:
Entfernt wurden Datensätze, bei denen folgende Felder leer waren:
properties.cclom:general_description (Beschreibungstexte)
additional_data.full_text (Volltexte)
...
...
properties.ccm:oeh_quality_neutralness
...
(Neutralitäts-Score der Redaktionen)
Zusätzlich wurden Datensätze ausgeschlossen, deren Textfelder (Beschreibungstexte und Volltexte) weniger als 60 Zeichen umfassen, um ausreichend Material für eine sinnvolle Bewertung zu haben.
properties.cclom:general_description (Beschreibungstexte)
additional_data.full_text (Volltexte)
Schließlich wurde die Anzahl der Datensätze auf 2000 reduziert und normalisiert. Dabei wurden folgende Felder zur gleichmäßigen Verteilung einbezogen:
properties.ccm:oeh_quality_neutralness (Neutralitäts-Score der Redaktionen)
properties.ccm:taxonid (Disziplinen)
...
Disziplinen wurden berücksichtigt, um eine ausgewogene Stichprobe der Bildungsinhalte aus unterschiedlichen Fachbereichen zu gewährleisten.
Ein Round-Robin-Verfahren (zyklisches Rundlaufverfahren) wurde angewandt, um die Daten gleichmäßig zu verteilen.
Eine Gleichverteilung ist jedoch auf Grund der vorübergehend gut bewerteten Inhalte nur bedingt möglich.
LLM und Prompt
LLM-Model
Für den Test die Bewertung der Neutralität von Bildungsinhalten wurde ein aktuelles LLM-Model genutzt, das aufgrund seiner niedrigen Kosten praxisnah ist: gpt-4o-mini (openai).
Der Test kann auch mit den meisten anderen LLM durchgeführt werden, jedoch ohne zwangsläufig die gleichen Ergebnisse zu erzielen. Ein Test mit weiteren Modelle erscheint sinnvoll.
Es ist davon auszugehen, dass ein erneuter Durchlauf mit dem gleichen Model auch nicht immer die gleichen Ergebnisse liefern wird.
Promptentwicklung
Grundlage des Prompts ist die zuvor genannte redaktionelle Skala. Diese wurde ausformuliert, um dem LLM eine genauere Einschätzung der Stufen zu ermöglichen.
Anschließend wurde der Prompt mit dem Promptverbesserungs-Tool “Generate” auf dem openai Playground verbessert (https://platform.openai.com/playground/chat ). Dieses strukturiert den Prompt maschinenlesbar, fügt Markdown-Formatierungen und passende Rückfragen zur Selbstreflexion hinzu.
Neben dem Scoring soll der Prompt auch eine kurze Begründung seiner Bewertung ausgeben, um die Ergebnisse später nachprüfen zu könnenLarge Language Model (LLM) verwendet. Das Modell,gpt-4o-mini (OpenAI), wurde ausgewählt, da es aufgrund seiner Effizienz und geringen Kosten eine praxisnahe Lösung darstellt. Das gleiche Experiment könnte auch mit anderen LLMs durchgeführt werden, allerdings können die Ergebnisse von Modell zu Modell variieren. Es wird davon ausgegangen, dass sogar mit dem gleichen Modell bei wiederholten Tests leicht unterschiedliche Ergebnisse auftreten könnten.
Promptentwicklung
Die Grundlage für den entwickelten Prompt war die redaktionelle Skala (0-5), die zur Bewertung der Neutralität von Bildungsinhalten auf der Plattform http://WirLernenOnline.de verwendet wird. Diese Skala reicht von 0 (manipulativ oder verfassungswidrig) bis 5 (vollständig neutral und wissenschaftlich fundiert). Der Prompt wurde so gestaltet, dass das LLM die Inhalte auf dieser Skala bewerten kann.
Um den Bewertungsprozess zu verbessern, wurde der Prompt auf dem OpenAI Playground mit dem Tool „Generate“ optimiert. Diese Optimierung fügte strukturierte Markdown-Formatierungen und Rückfragen zur Selbstreflexion hinzu, sodass das LLM seine Bewertung fundiert und nachvollziehbar gestalten konnte.
Der entwickelte Prompt dient nicht nur dazu, eine numerische Bewertung der Neutralität abzugeben, sondern fordert das LLM auch auf, eine kurze Begründung für die gewählte Bewertung zu liefern. Diese Begründung ermöglicht es, die Ergebnisse später zu überprüfen und die Entscheidungsfindung der KI nachzuvollziehen.
...
Finaler Prompt:
Code Block |
---|
Bewerte die Neutralität und Verfassungskonformität eines Bildungsinhalts auf einer Skala von 0 bis 5 anhand der folgenden Kriterien. Berücksichtigen Sie aktuelle Diskussionen über Menschenrechte, das Grundgesetz der Bundesrepublik Deutschland und die Anforderungen an schulische Bildung. Begründen Sie die Bewertung ausführlich, indem Sie erklären, warum Sie sich für den gewählten Wert entschieden haben und warum andere Werte nicht zutreffen. ## Bewertungskriterien - **0 (manipulativ oder verfassungswidrig):** Der Text enthält manipulative Aussagen, die darauf abzielen, den Leser in eine bestimmte Richtung zu drängen. Der Text verstößt gegen verfassungsrechtliche Grundprinzipien, wie die Achtung der Menschenwürde oder demokratische Grundwerte (z. B. Förderung von Hassrede, Diskriminierung, Gewalt oder Verherrlichung verfassungswidriger Symbole). - **1 (unneutral oder problematisch):** Der Text zeigt deutliche Anzeichen von Einseitigkeit oder Voreingenommenheit. Inhalte sind zwar verfassungskonform, aber sie verletzen möglicherweise das Neutralitätsgebot, indem sie eine extreme oder engstirnige Sichtweise auf sensible Themen wie Politik, Religion, Menschenrechte, Sexualität oder Identität vertreten, ohne Alternativen oder Gegensätze zu beleuchten. - **2 (ideologisch eingefärbt, aber verfassungskonform):** Der Text ist von einer bestimmten ideologischen Ausrichtung geprägt, aber die Inhalte sind verfassungskonform und korrekt. Es wird jedoch nur eine Seite des Themas beleuchtet, und es fehlen ausgewogene Perspektiven. - **3 (ideologisch eingefärbt, aber transparent und pluralistisch):** Der Text hat eine erkennbare ideologische Ausrichtung, aber diese wird transparent gemacht. Der Autor stellt offen dar, dass es alternative Perspektiven gibt, die ebenfalls verfassungskonform sind. Der Inhalt wahrt das Gebot der Pluralität, wie es in der schulischen Bildung gefordert wird. - **4 (neutrale und ausgewogene Formulierung):** Der Text verwendet eine sachliche und ausgewogene Formulierung ohne erkennbare ideologische Voreingenommenheit. Er beleuchtet das Thema von verschiedenen verfassungskonformen Standpunkten und berücksichtigt Menschenrechte und demokratische Werte, wie sie im Grundgesetz verankert sind. - **5 (neutrale Formulierung von einem unabhängigen, wissenschaftlich fundierten Autor):** Der Text ist vollständig neutral formuliert, ohne ideologische oder politische Voreingenommenheit. Er stammt von einem nachweislich unabhängigen und wissenschaftlich fundierten Autor oder einer Institution, die keine politischen oder ideologischen Interessen vertritt. Der Inhalt entspricht höchsten Standards der Verfassungstreue, Menschenrechte und Anforderungen der Schulbildung. # Steps 1. **Analysiere den Text:** Identifizieren Sie alle relevanten Informationen und Aspekte, die auf Neutralität und Verfassungskonformität hin überprüft werden müssen. 2. **Bewerten Sie anhand der Skala:** Wählen Sie den zutreffenden Wert von 0 bis 5 basierend auf Ihrer Analyse. 3. **Begründung:** Erläutern Sie Ihre Bewertung. Begründen Sie in maximal drei Sätzen, warum Sie sich für diesen Wert entschieden haben. Gehen Sie darauf ein, warum andere Werte nicht passend sind. # Output Format - Eine einzelne Zahl von 0 bis 5. - Eine nachfolgende Begründung in maximal 3 Sätzen. # Beispiele **Input:** Text über das Grundgesetz der Bundesrepublik Deutschland. **Output:** 4 "Der Text beinhaltet eine sachliche und ausgewogene Formulierung verschiedener verfassungskonformer Standpunkte und respektiert demokratische Werte. Er vermeidet extreme Sichtweisen und beleuchtet diverse Perspektiven. Andere Werte treffen nicht zu, da keine einseitige ideologische Ausrichtung erkennbar ist." |
Analyse des Testdatensatzes
Die Bildungsinhalte von Wirlernenonline.de haben den Schwerpunkt auf Schulbildung.
Die Scorings für die Bewertung der Neutralität sind vor allem bei redaktionell gepflegten Inhalten vorhanden, die über die höchste Qualitätsstufe verfügen. Es gibt daher keine Gleichverteilung der Neutralitätswerte, sondern vorwiegend höher eingestufte Inhalte (4 und 5 auf der Skala). Es sollte geprüft werden, ob zukünftige Tests mit weiteren Daten oder synthetisch erzeugten Muster angereichert werden können.
Die Qualität der Beschreibungstexte und Volltexte wurden mit verschiedenen Metriken bestimmt.
Interpretation der Textqualität
Die Volltexte sind mit durchschnittlich 860 Zeichen länger als die Beschreibungstexte (228 Zeichen).
Die Verteilung der Sprachen ist zwischen beiden Feldern vergleichbar.
Unterschiede ergeben sich in der Sentiment-Analyse. Beschreibungstexte sind weniger emotional formuliert, was für eine höhere Qualität im Hinblick auf den Aspekt Neutralität sprechen kann.
Der SMOG-Index zeigt, dass die Beschreibungstexte mit weniger formaler Bildung zu verstehen sind, als die Volltexte. Ein Grund hierfür könnte die redaktionelle Aufbereitung sein.
Beschreibungstexte
...
Volltexte
...
, die in diesem Test bewertet wurden, stammen von der Plattform http://WirLernenOnline.de. Der Fokus lag darauf, die KI-Bewertungen mit den redaktionellen Neutralitätsbewertungen zu vergleichen, um festzustellen, ob das LLM in der Lage ist, ähnliche Bewertungen vorzunehmen.
Die Bildungsinhalte sind vorwiegend dem Bereich Schulbildung zuzuordnen und auf der Neutralitäts-Skala hoch bewertet (4 bis 5). Dies lässt sich durch das redaktionelle einpflegen der Inhalte erklären.
Verteilung der Daten
Ein Großteil der Datensätze ist den Disziplinen: Informatik, Chemie, Physik, Mathematik und Darstellendes Spiel zuzuordnen.
...
Fast alle Inhalte wurden auf der Skala mit 4 oder 5 bewertet, was jedoch im Rahmen der Erwartungen liegt, da von Redaktionen gepflegte Inhalte von eher besserer Qualität sind.
...
...
Analyse der Textqualität
Für die Testdurchführung wurde ein Python-Script genutzt, das ausgewählte Metadatenfelder aus JSON an den Prompt übergibt und das Scoring sowie die Begründung dokumentiert. Anschließend werden diverse Metriken aus dem Vergleich von Originaldaten und KI-generierten Daten gebildet. Eine hohe Übereinstimmung würde auf eine erfolgreiche Bewertung durch die KI hindeuten.
Das Python-Script ist in der Anlage zu finden.
Testergebnisse mit Beschreibungstexten
Ein Großteil der Datensätze ist den Disziplinen: Informatik, Chemie, Physik, Mathematik und Darstellendes Spiel zuzuordnen. Fast alle Inhalte wurden auf der Skala mit 4 oder 5 bewertet, was jedoch im Rahmen der Erwartungen liegt.Da die Beschreibungs- und Volltexte die Grundlage der Bewertung bildeten, wurden diese hinsichtlich ihrer Qualität bewertet.
Die durchschnittliche Zeichenlänge der Volltexte beträgt etwa 860 Zeichen, während die Kurzbeschreibungen im Schnitt nur 228 Zeichen umfassen. Dieser signifikante Unterschied spiegelt wider, dass die Volltexte eine detailliertere und umfassendere Darstellung der Inhalte bieten, während die Kurzbeschreibungen nur die wesentlichen Informationen in verdichteter Form wiedergeben.
Allerdings zeigte sich, das die Beschreibungstexte im Vergleich zu den Volltexten weniger emotional gestaltet sind (Sentiment-Analyse) und mit geringerer formaler Bildung zu verstanden werden können (SMOG-Index).
Textanalyse der Beschreibungstexte
...
Textanalyse Volltexte
...
Testdurchführung
Für die Testdurchführung wurde ein Python-Script entwickelt, das die Beschreibungs- und Volltexte der Bildungsdatensätze von http://WirLernenOnline.de nutzt, um eine KI-basierte Bewertung der Neutralität durchzuführen.
Das Script verknüpfte die im JSON-Format vorliegenden Daten mit dem speziell entwickelten Prompt und führte die Bewertungen der Inhalte durch ein LLM (Large Language Model) aus.
Aufbau des Tests
Der Test wurde in zwei Testreihen durchgeführt:
Testreihe 1 verwendete die Beschreibungstexte der Bildungsinhalte, um die KI-gestützte Bewertung durchzuführen.
Testreihe 2 basierte auf den von den Webseiten extrahierten Volltexten, die eine umfassendere inhaltliche Analyse ermöglichten.
Testergebnisse
Testergebnisse mit Beschreibungstexten
...
Alle Metriken im Überblick (Beschreibungstexte):
Precision: 0.59
F1 Score: 0.55
Mean Absolute Error (MAE): 0.62
Mean Squared Error (MSE): 1.10
Root Mean Squared Error (RMSE): 1.05
R² Score: -3.48
Pearson-Korrelation: -0.02
Durchschnittliche Abweichung: 0.62
Testergebnisse mit Volltexten
Hinweise zu den Metriken
...
Precision misst, wie genau die positiven Vorhersagen des Modells sind. Ein Wert von 0.85 bedeutet, dass 85% der als positiv vorhergesagten Fälle tatsächlich korrekt waren, während 15% falsch positive Vorhersagen waren.
...
Alle Metriken im Überblick (Volltexte):
Precision: 0.58
F1 Score: 0.49
Mean Absolute Error (MAE): 0.92
Mean Squared Error (MSE): 2.57
Root Mean Squared Error (RMSE): 1.60
R² Score: -9.48
Pearson-Korrelation: -0.13
Durchschnittliche Abweichung: 0.92
Hinweise zu den Metriken
Precision misst, wie genau die positiven Vorhersagen des Modells sind. Ein Wert von 0.85 bedeutet, dass 85% der als positiv vorhergesagten Fälle tatsächlich korrekt waren, während 15% falsch positive Vorhersagen waren.
F1 Score ist das harmonische Mittel von Precision und Recall und gibt ein ausgewogenes Maß der Modellleistung. Ein F1 Score von 0.75 zeigt, dass das Modell eine gute Balance zwischen Genauigkeit (Precision) und Vollständigkeit (Recall) der Vorhersagen findet.
Mean Absolute Error (MAE) gibt den durchschnittlichen absoluten Unterschied zwischen den vorhergesagten und den tatsächlichen Werten an. Ein MAE von 2.3 zeigt, dass die Vorhersagen im Durchschnitt um 2.3 Einheiten (z. B. Punkte) vom tatsächlichen Wert abweichen.
Mean Squared Error (MSE) misst den Durchschnitt der quadrierten Fehler. Ein Wert von 10 bedeutet, dass größere Fehler stärker betont werden, und ein niedriger MSE zeigt eine gute Modellleistung an.
Root Mean Squared Error (RMSE) ist die Quadratwurzel des MSE und gibt den Fehler in derselben Einheit wie die Zielvariable an. Ein RMSE von 3.2 zeigt, dass der durchschnittliche Fehler bei etwa 3.2 Einheiten (z. B. Punkten) liegt.
R² Score misst, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. Ein R² von 0.92 bedeutet, dass das Modell 92% der Varianz erklären kann, was auf eine gute Passung hinweist.
Pearson-Korrelation misst die lineare Beziehung zwischen den vorhergesagten und den tatsächlichen Werten. Ein Wert von 0.88 zeigt eine starke positive lineare Korrelation, d. h., die Vorhersagen folgen den tatsächlichen Werten sehr gut.
Durchschnittliche Abweichung zeigt den durchschnittlichen Unterschied zwischen den AI-bewerteten und den Originalwerten an. Eine Abweichung von 1.1 bedeutet, dass die KI-Bewertungen im Durchschnitt um 1.1 Einheiten vom tatsächlichen Wert abweichen, was auf eine hohe Übereinstimmung hindeutet.
...
Analyse der
...
Fazit
Anlage
Tool für die Volltextgenerierung
...
Sonderfälle
In der Analyse der Testdurchführung wurden spezielle Datensätze identifiziert (“Sonderfälle”), bei denen die KI-basierte Bewertung der Neutralität signifikant von den redaktionellen Bewertungen abwich. Diese Abweichungen betrugen in den Sonderfällen mindestens 2 Punkte auf der Neutralitätsskala, was auf potenzielle Herausforderungen oder Missverständnisse in der KI-Bewertung hinweist.
Zwei Beispiele für Sonderfälle aus dem Testdurchlauf mit den Beschreibungstexten:
Code Block |
---|
{
"properties_cclom:general_description": "\"Emilia Galotti\" \u2013 die Zusammenfassung als Chat! Lisa erkl\u00e4rt dir, was in Gotthold Ephraim Lessings b\u00fcrgerlichem Trauerspiel Emilia Galotti passiert und wie die Figuren zueinander stehen. ",
"original_neutralness_score": 2,
"ai_neutralness_score": 4,
"ai_reasoning": "Der Text bietet eine ausgewogene Darstellung des Inhalts von Lessings 'Emilia Galotti', ohne eine spezifische ideologische Voreingenommenheit zu zeigen. Er beleuchtet die Charaktere und Aspekte des Werkes auf eine sachliche Weise und tr\u00e4gt dazu bei, das Verst\u00e4ndnis f\u00fcr die gesellschaftlichen und moralischen Fragen zu f\u00f6rdern, die im Drama behandelt werden. H\u00f6here Werte sind nicht zutreffend, da der Text nicht aus einer wissenschaftlichen Quelle stammt und somit nicht die h\u00f6chsten Standards der Neutralit\u00e4t und Unabh\u00e4ngigkeit erf\u00fcllt."
},
{
"properties_cclom:general_description": "1882 l\u00e4sst Nietzsche in seiner fr\u00f6hlichen Wissenschaft einen \u201etollen Menschen\" feststellen, dass Gott mit der Aufkl\u00e4rung sein Ende gefunden habe. Nun m\u00f6ge der Mensch Gottes Stelle einnehmen. Ein Jahr darauf fordert Nietzsches Zarathustra, der sinnlose, weil leistungsm\u00fcde oder -schwache Mensch m\u00fcsse vom \u00dcbermenschen abgel\u00f6st werden. Der Mensch kranke an seinem Geiz mit der S\u00fcnde. In Nietzsches Antichrist schlie\u00dflich wird das Programm kurz vor Nietzsches geistiger Umnachtung 1888 entfaltet. Er beginnt mit: \"Die Schwachen und Mi\u00dfrathnen sollen zu Grunde gehn: erster Satz unsrer Menschenliebe. Und man soll ihnen noch dazu helfen\" (vgl. AC I,2). ",
"original_neutralness_score": 3,
"ai_neutralness_score": 1,
"ai_reasoning": "Der Text zeigt eine deutliche ideologische Voreingenommenheit, indem er Nietzsches Ansichten zitiert, die menschenverachtende Tendenzen reflektieren. Dies steht im Widerspruch zu den Werten des Grundgesetzes und dem Prinzip der Menschenw\u00fcrde. Eine kritische Auseinandersetzung mit diesen Ideen fehlt, und somit wird das Neutralit\u00e4tsgebot verletzt."
}, |
Ein Auszug der Sonderfälle (ca. 50%) wurde mit dem auf Logik trainierten LLM Model gpt-o1-mini analysiert. Dabei wurden folgende Gründe für die Abweichungen genannt:
Einseitige Perspektive:
Beispiel: Texte, die sich nur auf eine Methode (z.B. Ölfleckversuch zur Atomdurchmesserbestimmung) konzentrieren, ohne alternative Ansätze zu erwähnen.
Mangel an Pluralismus und Ausgewogenheit:
Beispiel: Erklärungen zu technischen Themen wie Wechselstromkreisen ohne Bezug auf unterschiedliche Anwendungen oder gesellschaftliche Implikationen.
Potenzial für Fehlinterpretationen oder Manipulation:
Beispiel: Chemische Experimente ohne Hinweise auf Sicherheitsmaßnahmen, was zu Missverständnissen oder Gefahren führen könnte.
Einseitige Darstellung von Experimenten:
Beispiel: Stumme Experimentiervideos, die ohne erklärenden Kontext präsentiert werden und somit pädagogische Tiefe fehlen lassen.
Technische Inhalte ohne Kontextualisierung:
Beispiel: Förderung spezifischer Softwarelösungen (z.B. GeoGebra für Binomialverteilung) ohne Erwähnung alternativer Methoden.
Fehlende ethische und gesellschaftliche Implikationen:
Beispiel: Diskussion von E-Mail-Sicherheit ohne Berücksichtigung der gesellschaftlichen Bedeutung von Datenschutz.
Manuelle Einzelfallprüfung von Beispielen:
Code Block |
---|
{
"properties_cclom:general_description": "So wie die Natur ver\u00e4ndert sich f\u00fcr uns scheinbar auch der Himmel im Laufe eines Jahres: jedes Quartal hat seine typischen Sternbilder. Um sich in jeder Jahreszeit am Himmel zu orientieren, gibt es vier Formationen, die keine Sternbilder sind, aber \u00e4hnlich wie\u2026",
"original_neutralness_score": 5,
"ai_neutralness_score": 2,
"ai_reasoning": "Der Text ist von einer bestimmten ideologischen Ausrichtung gepr\u00e4gt, jedoch bleibt er verfassungskonform und korrekt. Es wird haupts\u00e4chlich eine Seite des Themas beleuchtet, ohne dass alternative Perspektiven ausreichend ber\u00fccksichtigt werden. Daher entspricht er nicht den Anforderungen f\u00fcr einen h\u00f6heren Wert, da er eine bestimmte Sichtweise nicht transparent macht."
}, |
Auswertung: Es ist korrekt, das keine alternativen Perspektiven angeboten werden (ist hier auch schwierig). Eine ideologische Ausrichtung ist schwer erkennbar.
Auswertung der Ergebnisse
Die Untersuchung der Fähigkeit eines Large Language Models (LLM) zur Bewertung der Neutralität von Bildungsinhalten führte zu einigen aufschlussreichen Ergebnissen. Insgesamt zeigte das LLM in beiden Testreihen – basierend auf Beschreibungstexten und Volltexten – eine solide Leistungsfähigkeit, die mit den redaktionellen Bewertungen vergleichbar war, aber auch einige spezifische Herausforderungen offenbarte.
Leistung des LLM im Vergleich zu menschlichen Bewertungen
Beschreibungstexte:
Die durchschnittliche absolute Abweichung (MAE) lag bei 0,62 Punkten, was eine relativ hohe Übereinstimmung mit den menschlichen Bewertungen signalisiert. Über 50 % der Bewertungen stimmten in einem engen Bereich mit den redaktionellen Einschätzungen überein.
Die KI zeigte eine ähnliche Verteilung der Neutralitätsscores wie die menschlichen Bewertungen, was darauf hindeutet, dass die Beschreibungstexte bereits eine hohe Informationsdichte enthalten, die eine präzise Bewertung erlaubt. Möglicherweise enthalten die Beschreibungstexte bereits Hinweise auf die Qualität, die von den Redaktionen eingearbeitet wurden.
Volltexte:
Die Volltexte waren im Schnitt viermal so lang wie die Beschreibungstexte und stellten eine größere Herausforderung für die KI dar. Die durchschnittliche absolute Abweichung lag bei 0,92 Punkten. Dies könnte darauf hindeuten, dass die Fülle an Informationen oder aber auch die Qualität der Volltexte die Neutralitätsbewertung erschwert.
Auffällig war die Tatsache, dass die Volltexte eine höhere Betonung von Gefühlen und höhere Anforderungen an deren Verständnis aufwiesen, was möglicherweise zu größeren Abweichungen in der Bewertung führte.
Sonderfälle:
In den Sonderfällen, bei denen die KI-Bewertung um mindestens 2 Punkte von der redaktionellen Bewertung abwich, zeigte sich, dass die KI besonders auf einseitige Perspektiven und fehlenden Pluralismus aufmerksam reagierte. Dies führte in einigen Fällen zu strikteren Bewertungen, insbesondere bei technischen oder wissenschaftlichen Inhalten ohne breitere Kontextualisierung.
Diese Abweichungen machen deutlich, dass das LLM in der Lage ist, auf Aspekte aufmerksam zu machen, die menschliche Redaktionen eventuell weniger stark gewichtet haben. Besonders hervorzuheben ist die Fähigkeit des Modells, Risiken für Missverständnisse oder Manipulationen in den Texten zu erkennen.
Vergleich der Metriken
Insgesamt zeigten die Metriken (Precision, F1-Score, MAE, MSE, R²) einen akzeptablen Grad an Übereinstimmung, wobei die Beschreibungstexte besser abschnitten als die Volltexte. Dies deutet darauf hin, dass kürzere, präziser formulierte Texte für die KI einfacher zu bewerten sind als lange, komplexe Volltexte.
Fazit
Das LLM konnte in einem breiten Spektrum von Bildungsinhalten eine angemessene Bewertung der Neutralität liefern. Die erzielten Ergebnisse zeigen, dass es als Unterstützung für redaktionelle Bewertungsprozesse wertvolle Einblicke bieten kann, insbesondere bei der Identifikation von potenziell einseitigen oder ideologisch geprägten Inhalten.
Gleichzeitig weist die Analyse darauf hin, dass die KI bei längeren, komplexeren Volltexten größere Schwierigkeiten hat, was weiteren Forschungsbedarf nahelegt. Weitere Tests könnten untersuchen, ob und wie das LLM durch zusätzliche Trainingseinheiten oder feinere Anpassungen des Prompts (z.B. durch eine optimierte Gewichtung von Aspekten wie Pluralismus) besser in der Lage ist, die volle inhaltliche Tiefe und den pluralistischen Anspruch von Volltexten zu erfassen.
Es zeigt sich auch, dass die LLM-Bewertung auf bestimmte Aspekte der Neutralität stärker fokussiert, wie etwa die Vielfalt der Perspektiven und das Risiko für Fehlinterpretationen. Diese Fokusverschiebung könnte zu wertvollen Ergänzungen im Prozess der Neutralitätsbewertung beitragen, sollte jedoch immer im Kontext der redaktionellen Standards interpretiert werden.
Anlage
Quellen für die Promptentwicklung
siehe hier
Tool für die Volltextgenerierung
Code Block |
---|
# URL-basierte Volltext-Anreicherung mit Goose3
# Anforderungen: pip install beautifulsoup4 streamlit requests goose3
# Script als fulltext_enricher.py speichern
# Start: steamlit run fulltext_enricher.py
import streamlit as st
import json
import os
import datetime
import time
from goose3 import Goose
# List of file extensions to skip (e.g. audio and video formats)
SKIPPED_EXTENSIONS = ['.mp4', '.mp3', '.avi', '.mpeg', '.mov', '.wmv', '.flv']
# Function to scrape a webpage using Goose3 and return the extracted information
def scrape_page(url, follow_redirect=True):
goose = Goose() # Initialize Goose3 with default settings
try:
if isinstance(url, list): # Check if URL is in list format and convert to string
url = url[0]
# Skip URLs with certain extensions
if any(url.lower().endswith(ext) for ext in SKIPPED_EXTENSIONS):
return "skipped", None
article = goose.extract(url=url)
# Extract the relevant information
full_text = article.cleaned_text # Cleaned full text
title = article.title # Extracted title
summary = article.meta_description[:500] if article.meta_description else full_text[:500] # Meta description as summary or first 500 chars
keywords = article.meta_keywords # Extracted meta keywords
top_image = article.top_image.src if article.top_image else None # URL of the top image
# Fallback: If full text is missing, use meta description
if not full_text:
full_text = article.meta_description or "No full text available for this page."
return {
'title': title,
'full_text': full_text,
'summary': summary,
'keywords': keywords,
'top_image': top_image,
'url': url
}
except Exception as e:
st.error(f"Error scraping {url}: {e}")
return None
# Function to process the JSON file and enrich it with scraped data
def process_json(data, url_field, timeout, save_folder, follow_redirect=True, skip_media_files=True, crawl_all=True, num_records=10):
try:
total_records = len(data)
records_to_process = total_records if crawl_all else min(num_records, total_records)
st.write(f"Processing {records_to_process} out of {total_records} records...")
enriched_data = []
for i, record in enumerate(data[:records_to_process], 1):
# Display the progress message for processing records
st.info(f"Processing record {i}/{records_to_process}", icon="ℹ️")
# Extract URL using the selected field
try:
url = eval(f"record{url_field}")
# If URL is in list format, convert it to a string (take the first URL)
if isinstance(url, list):
url = url[0]
except Exception as e:
st.warning(f"No valid URL found for record {i}/{records_to_process}. Skipping... (Error: {e})", icon="⚠️")
continue
if url:
# Skip media files if the option is selected
if skip_media_files and any(url.lower().endswith(ext) for ext in SKIPPED_EXTENSIONS):
st.warning(f"Skipping media file URL #{i}: {url}", icon="⚠️")
continue
# Display the message for scraping the current URL
st.success(f"Scraping URL #{i}: {url}", icon="🟢")
scraped_data = scrape_page(url, follow_redirect=follow_redirect)
if scraped_data != "skipped" and scraped_data:
# Only display the summary preview message without additional status info
if scraped_data['summary']:
st.success(scraped_data['summary'][:250] + "..." if len(scraped_data['summary']) > 250 else scraped_data['summary'])
# Add scraped data to the record
record['additional_data'] = {
'title': scraped_data['title'],
'full_text': scraped_data['full_text'],
'summary': scraped_data['summary'],
'keywords': scraped_data['keywords'],
'top_image': scraped_data['top_image'],
'final_url': scraped_data['url']
}
enriched_data.append(record)
# Introduce delay for the given timeout
time.sleep(timeout)
# Save the enriched JSON file
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
enriched_file_name = os.path.join(save_folder, f"data_enriched_{timestamp}.json")
with open(enriched_file_name, 'w', encoding='utf-8') as f:
json.dump(enriched_data, f, ensure_ascii=False, indent=4)
st.success(f"Enriched data saved as {enriched_file_name}", icon="🟢")
except Exception as e:
st.error(f"Error processing JSON file: {e}")
# Function to extract available fields from JSON structure
def extract_fields(data):
field_set = set()
# Recursive function to explore the JSON structure
def recurse_json(obj, parent_key=''):
if isinstance(obj, dict):
for key, value in obj.items():
new_key = f"{parent_key}['{key}']" if parent_key else f"['{key}']"
field_set.add(new_key)
recurse_json(value, new_key)
elif isinstance(obj, list):
for item in obj:
recurse_json(item, parent_key)
recurse_json(data)
return sorted(list(field_set))
# Streamlit UI
st.title('JSON Web Scraper and Enricher using Goose3')
# Upload JSON file
uploaded_file = st.file_uploader("Choose a JSON file", type="json")
if uploaded_file:
try:
# Load the JSON data
data = json.load(uploaded_file)
# Extract all field paths from the JSON structure
available_fields = extract_fields(data)
# Allow the user to choose a URL field
url_field = st.selectbox("Select the URL field", available_fields, index=available_fields.index("['properties']['ccm:wwwurl']"))
# Other options for processing
timeout = st.number_input("Enter delay between requests (seconds)", min_value=0, value=0)
save_folder = st.text_input("Folder to save enriched JSON", value=".")
follow_redirects = st.checkbox("Follow redirects", value=True)
# Option to skip media files (audio/video)
skip_media_files = st.checkbox("Skip media files (e.g., .mp4, .mp3, .avi)", value=True)
# Option to process all records or only a limited number
crawl_all = st.checkbox("Crawl all records", value=True)
num_records = st.number_input("Number of records to process", min_value=1, value=10, disabled=crawl_all)
if st.button("Start Processing"):
process_json(data, url_field, timeout, save_folder, follow_redirects, skip_media_files, crawl_all, num_records)
except Exception as e:
st.error(f"Error loading JSON file: {e}")
|
Tool für die Filterung, Analyse und Bestimmung der Textqualität
Code Block |
---|
# Tool für die Betrachtung, Analyse und Filterung von JSON-Files # Anforderungen: pip install streamlit pandas orjson matplotlib seaborn plotly textstat nltk langdetect textblob # Script als datamanager_json.py speichern # Start: streamlit run datamanager_json.py import streamlit as st import pandas as pd import orjson import os import matplotlib.pyplot as plt import seaborn as sns import plotly.express as px import textstat import re import nltk from nltk import ngrams from collections import Counter, defaultdict from langdetect import detect, LangDetectException from textblob import TextBlob import time from itertools import cycle # Download NLTK data nltk.download('punkt') # Funktion zum rekursiven Flachlegen von JSON def flatten_json(y): out = {} def flatten(x, name=''): if isinstance(x, dict): for a in x: flatten(x[a], f'{name}{a}_') elif isinstance(x, list): # Join list items with a comma out[name[:-1]] = ', '.join(map(str, x)) else: out[name[:-1]] = x flatten(y) return out @st.cache_data(show_spinner=False) def process_uploaded_file(file_path): try: records = [] with open(file_path, 'rb') as f: content = f.read() data = orjson.loads(content) if isinstance(data, list): for record in data: flat_record = flatten_json(record) records.append(flat_record) elif isinstance(data, dict): flat_record = flatten_json(data) records.append(flat_record) else: st.error("Nicht unterstützte JSON-Struktur.") return pd.DataFrame() df = pd.DataFrame(records) st.success(f"JSON-Daten erfolgreich in DataFrame konvertiert. Spalten: {len(df.columns)}") st.write(f"Anzahl der Datensätze: {len(df)}") return df except Exception as e: st.error(f"Fehler beim Verarbeiten der Datei: {e}") return pd.DataFrame() def merge_similar_fields(df): pattern = re.compile(r'^(.*?)(?:_\d+)?$') base_columns = {} for col in df.columns: match = pattern.match(col) if match: base_name = match.group(1) if base_name not in base_columns: base_columns[base_name] = [] base_columns[base_name].append(col) for base, cols in base_columns.items(): if len(cols) > 1: df[base] = df[cols].apply(lambda row: ', '.join(row.dropna().astype(str)), axis=1) df.drop(columns=cols, inplace=True) return df def calculate_fill_status(df): fill_status = df.notnull().mean() * 100 fill_status = fill_status.sort_values(ascending=False) return fill_status def get_all_fields(data, parent_key='', fields=None): if fields is None: fields = set() if isinstance(data, dict): for key, value in data.items(): full_key = f'{parent_key}.{key}' if parent_key else key fields.add(full_key) if isinstance(value, dict): get_all_fields(value, full_key, fields) elif isinstance(value, list): for item in value: if isinstance(item, dict): get_all_fields(item, full_key, fields) elif isinstance(data, list): for item in data: if isinstance(item, dict): get_all_fields(item, parent_key, fields) return fields def load_json(file_path): with open(file_path, "rb") as f: return orjson.loads(f.read()) def save_json(data, file_path): with open(file_path, "wb") as f: f.write(orjson.dumps(data, option=orjson.OPT_INDENT_2)) def list_json_files(directory): return [file for file in os.listdir(directory) if file.endswith(".json")] def preview_data(data, index=0): if 0 <= index < len(data): return data[index] return {} def get_nested_value(data, path): keys = path.split(".") for key in keys: if isinstance(data, list): # Extrahiere Werte für den Schlüssel aus jedem Dict in der Liste next_data = [] for item in data: if isinstance(item, dict) and key in item: next_data.append(item[key]) data = next_data if next_data else None elif isinstance(data, dict): data = data.get(key) else: return None if data is None: return None # Flatten the list if it's a list of lists if isinstance(data, list): flat_data = [] for item in data: if isinstance(item, list): flat_data.extend(item) else: flat_data.append(item) return flat_data return data def is_field_empty(value): """Prüft, ob ein Feld als 'leer' betrachtet wird (z.B. None, leere Strings, Listen, Dicts).""" if value is None: return True if isinstance(value, str) and value.strip() == "": return True if isinstance(value, list) and len(value) == 0: return True if isinstance(value, dict) and len(value) == 0: return True return False def remove_fields(data, fields_to_remove): for item in data: for field in fields_to_remove: keys = field.split(".") current_dict = item for key in keys[:-1]: if key in current_dict and isinstance(current_dict[key], dict): current_dict = current_dict[key] else: current_dict = {} break if keys[-1] in current_dict: del current_dict[keys[-1]] return data def current_timestamp(): return time.strftime("%Y%m%d_%H%M%S") def text_analysis(df, text_field, min_chars=0): try: texts = df[text_field].dropna().astype(str) except KeyError: st.error(f"Feld '{text_field}' existiert nicht.") return if min_chars > 0: texts = texts[texts.str.len() >= min_chars] if 'text_index' not in st.session_state: st.session_state.text_index = 1 st.subheader("Texte durchsuchen") if not texts.empty: # Verwenden Sie Streamlit's native Funktionen für die Navigation st.markdown("### Vorschau des Textes") max_index = len(texts) st.session_state.text_index = st.number_input( "Datensatznummer", min_value=1, max_value=max_index, value=st.session_state.text_index if st.session_state.text_index <= max_index else max_index, step=1, key='text_navigation' ) current_text = texts.iloc[st.session_state.text_index - 1] st.text_area("Text", value=current_text, height=200, key='text_display') st.write(f"**Datensatz Nummer:** {st.session_state.text_index}") st.subheader("Textstatistik") num_chars = texts.str.len().sum() num_words = texts.apply(lambda x: len(x.split())).sum() avg_chars = texts.str.len().mean() avg_words = texts.apply(lambda x: len(x.split())).mean() st.write(f"**Gesamtanzahl der Zeichen:** {num_chars}") st.write(f"**Gesamtanzahl der Wörter:** {num_words}") st.write(f"**Durchschnittliche Zeichen pro Text:** {avg_chars:.2f}") st.write(f"**Durchschnittliche Wörter pro Text:** {avg_words:.2f}") # **Lesbarkeitsmetriken** st.subheader("Lesbarkeitsmetriken") st.markdown(""" **Flesch Reading Ease:** Bewertet, wie leicht ein Text zu lesen ist. Höhere Werte bedeuten leichter lesbare Texte. **Flesch-Kincaid Grade:** Gibt das Schulniveau an, das für das Verständnis des Textes erforderlich ist. **Gunning Fog Index:** Misst die Anzahl der Jahre formaler Bildung, die ein Leser benötigt, um den Text zu verstehen. **SMOG Index:** Schätzt die erforderlichen Jahre formaler Bildung basierend auf der Anzahl der polysyllabischen Wörter. """) readability_df = pd.DataFrame({ 'Flesch Reading Ease': texts.apply(textstat.flesch_reading_ease), 'Flesch-Kincaid Grade': texts.apply(textstat.flesch_kincaid_grade), 'Gunning Fog Index': texts.apply(textstat.gunning_fog), 'SMOG Index': texts.apply(textstat.smog_index) }) readability_summary = readability_df.mean().round(2) st.write(readability_summary.to_frame(name='Durchschnitt').T) st.markdown(""" **Interpretation der Lesbarkeitsmetriken:** - **Flesch Reading Ease:** Werte zwischen 60-70 sind für die meisten Erwachsenen gut verständlich. - **Flesch-Kincaid Grade:** Ein Wert von 8 bedeutet, dass ein Schüler der 8. Klasse den Text verstehen sollte. - **Gunning Fog Index:** Ein Wert von 12 entspricht dem Niveau eines Highschool-Abschlusses. - **SMOG Index:** Gibt die geschätzten Jahre formaler Bildung an, die für das Verständnis des Textes erforderlich sind. """) # **Sentiment-Analyse** st.subheader("Sentiment-Analyse") st.markdown(""" **Sentiment-Analyse:** Bestimmt die emotionale Tonalität der Texte. Die Kategorien sind: - **Positiv:** Der Text drückt positive Gefühle aus. - **Negativ:** Der Text drückt negative Gefühle aus. - **Neutral:** Der Text drückt weder positive noch negative Gefühle aus. """) sentiments = texts.apply(lambda x: TextBlob(x).sentiment.polarity) sentiment_counts = sentiments.apply(lambda x: 'Positiv' if x > 0 else ('Negativ' if x < 0 else 'Neutral')).value_counts() sentiment_counts_df = sentiment_counts.reset_index() sentiment_counts_df.columns = ['Sentiment', 'Anzahl der Texte'] fig2 = px.bar( sentiment_counts_df, x='Sentiment', y='Anzahl der Texte', labels={'Anzahl der Texte': 'Anzahl der Texte'}, title="Verteilung der Sentiment-Kategorien", hover_data={'Anzahl der Texte': True} ) fig2.update_traces(marker_color='blue') fig2.update_layout(xaxis_title='Sentiment', yaxis_title='Anzahl der Texte') st.plotly_chart(fig2, use_container_width=True, key='sentiment_plot') st.markdown(""" **Interpretation der Sentiment-Analyse:** - **Positiv:** Ein hoher Anteil positiver Texte kann auf eine optimistische Stimmung in den Daten hinweisen. - **Negativ:** Ein hoher Anteil negativer Texte kann auf Herausforderungen oder Kritik in den Daten hinweisen. - **Neutral:** Ein hoher Anteil neutraler Texte deutet auf sachliche oder informative Inhalte hin. """) # **Spracherkennung** st.subheader("Spracherkennung") st.markdown(""" **Spracherkennung:** Identifiziert die Sprache der Texte, um sicherzustellen, dass alle Texte in der erwarteten Sprache verfasst sind. """) def detect_language(text): try: return detect(text) except LangDetectException: return "Unbekannt" languages = texts.apply(detect_language) language_counts = languages.value_counts() language_counts_df = language_counts.reset_index() language_counts_df.columns = ['Sprache', 'Anzahl der Texte'] fig3 = px.bar( language_counts_df, x='Sprache', y='Anzahl der Texte', labels={'Anzahl der Texte': 'Anzahl der Texte'}, title="Verteilung der erkannten Sprachen", hover_data={'Anzahl der Texte': True} ) fig3.update_traces(marker_color='orange') fig3.update_layout(xaxis_title='Sprache', yaxis_title='Anzahl der Texte') st.plotly_chart(fig3, use_container_width=True, key='language_plot') st.markdown(""" **Interpretation der Spracherkennung:** - **Sprache:** Die erkannten Sprachen geben Aufschluss darüber, welche Sprachen in den Textdaten vorherrschen. - **Unbekannt:** Ein hoher Anteil an "Unbekannt" kann auf unklare oder gemischte Sprachinhalte hinweisen. """) # **Lexikalische Vielfalt** st.subheader("Lexikalische Vielfalt") st.markdown(""" **Lexikalische Vielfalt (Typ-Token-Verhältnis, TTR):** Misst die Vielfalt des verwendeten Wortschatzes. Ein höheres TTR weist auf eine größere Wortvielfalt hin, was auf eine reichhaltigere und vielfältigere Sprache im Text hindeutet. """) def type_token_ratio(text): tokens = text.split() types = set(tokens) return len(types) / len(tokens) if len(tokens) > 0 else 0 df['TTR'] = texts.apply(type_token_ratio) ttr_summary = df['TTR'].describe() st.write("**Statistik des Typ-Token-Verhältnisses (TTR):**") st.write(ttr_summary) fig5 = px.histogram( df, x='TTR', nbins=20, title="Verteilung des Typ-Token-Verhältnisses (TTR)", labels={'TTR': 'TTR', 'count': 'Anzahl der Texte'}, opacity=0.75 # histnorm='count' entfernt, da 'count' ungültig ist und 'count' das Standardverhalten ist ) fig5.update_traces(marker_color='green') st.plotly_chart(fig5, use_container_width=True, key='ttr_plot') st.markdown(""" **Interpretation der lexikalischen Vielfalt:** - **Höheres TTR:** Größere Vielfalt im Wortschatz, was auf abwechslungsreichere und reichhaltigere Texte hinweist. - **Niedrigeres TTR:** Weniger Vielfalt im Wortschatz, was auf wiederholende oder monotone Sprache hinweisen kann. """) # **Duplikat-Erkennung** st.subheader("Duplikat-Erkennung") st.markdown(""" **Duplikat-Erkennung:** Identifiziert doppelte oder nahezu doppelte Texte, um Redundanzen in den Daten zu vermeiden. """) duplicate_counts = df[text_field].duplicated().sum() st.write(f"**Anzahl der doppelten Texte:** {duplicate_counts}") if duplicate_counts > 0: duplicates = df[df[text_field].duplicated(keep=False)] st.write("**Doppelte Texte:**") st.write(duplicates[[text_field]]) # **N-Gramm-Analyse** st.subheader("N-Gramm-Analyse") st.markdown(""" **N-Gramm-Analyse:** Analysiert häufig vorkommende Phrasen (Bigrams), um gängige Ausdrucksweisen oder Themen zu identifizieren. """) def get_ngrams(text, n=2): tokens = nltk.word_tokenize(text) return list(ngrams(tokens, n)) bigrams = texts.apply(lambda x: get_ngrams(x, 2)).explode() bigram_counts = Counter(bigrams).most_common(20) bigram_df = pd.DataFrame(bigram_counts, columns=['Bigram', 'Anzahl']) bigram_df['Bigram'] = bigram_df['Bigram'].apply(lambda x: ' '.join(x)) fig6 = px.bar( bigram_df, x='Anzahl', y='Bigram', orientation='h', labels={'Anzahl': 'Anzahl der Vorkommen'}, title="Top 20 Bigrams", hover_data={'Anzahl': True} ) fig6.update_traces(marker_color='magenta') st.plotly_chart(fig6, use_container_width=True, key='bigram_plot') st.markdown(""" **Interpretation der N-Gramm-Analyse:** - **Häufige Bigrams:** Die am häufigsten vorkommenden Bigrams können auf gängige Phrasen oder Themen in den Texten hinweisen. """) def json_filter_tab(df): st.header("🔄 Datenfilter") # Da die JSON-Datei bereits zentral geladen ist, keine weitere Auswahl nötig st.info("Verwenden Sie die bereits geladene JSON-Datei für die Filterung.") data = df.to_dict(orient='records') all_fields = set(df.columns.tolist()) # Filter 1: Leere Felder (standardmäßig deaktiviert) empty_field_filter_active = st.checkbox("🚫 Leere Felder filtern", value=False) if empty_field_filter_active: selected_empty_fields = st.multiselect( "Felder zur Prüfung auf leere Werte auswählen", options=list(all_fields), default=[] ) st.warning("Dieser Filter entfernt Datensätze, bei denen ausgewählte Felder keine Werte enthalten.") # Filter 2: Feld-Wert Kombination mit Operatoren und Auto-Vervollständigung field_value_filter_active = st.checkbox("🔍 Feld-Wert Kombinationen filtern") if field_value_filter_active: st.warning("Dieser Filter entfernt Datensätze, bei denen die angegebenen Feld-Wert-Kombinationen nicht zutreffen.") field_value_filters = [] field_value_count = st.number_input("Anzahl der Feld-Wert-Kombinationen", min_value=1, value=1, step=1) operators = ["=", "!=", ">", "<", ">=", "<="] operator_map = { "=": lambda a, b: a == b, "!=": lambda a, b: a != b, ">": lambda a, b: a > b, "<": lambda a, b: a < b, ">=": lambda a, b: a >= b, "<=": lambda a, b: a <= b } for i in range(int(field_value_count)): col1, col2, col3 = st.columns(3) with col1: field = st.selectbox(f"Feld {i+1}", options=list(all_fields), key=f"filter_field_{i}") with col2: operator = st.selectbox(f"Operator {i+1}", options=operators, key=f"filter_operator_{i}") with col3: value = st.text_input(f"Wert {i+1}", key=f"filter_value_{i}") field_value_filters.append((field, operator, value)) # Filter 3: Mindestzeichenlänge length_filter_active = st.checkbox("✂️ Filter nach Mindestzeichenlänge") if length_filter_active: selected_length_fields = st.multiselect("Felder zur Zeichenlänge auswählen", options=list(all_fields)) min_length = st.number_input("Mindestzeichenlänge", min_value=1, value=30) st.warning("Dieser Filter entfernt Datensätze, bei denen die Länge der ausgewählten Felder kürzer als die angegebene Mindestlänge ist.") # Filter 4: Gleichverteilung balancing_filter_active = st.checkbox("⚖️ Filter zur Gleichverteilung") if balancing_filter_active: selected_balancing_fields = st.multiselect("Felder für Gleichverteilung auswählen", options=list(all_fields)) total_count = st.number_input("Gesamtanzahl der Datensätze nach Gleichverteilung", min_value=1, value=100) st.warning("Dieser Filter reduziert die Daten auf eine Gesamtanzahl und verteilt sie möglichst gleichmäßig auf die angegebenen Felder.") # Filter 5: Felder aus JSON entfernen remove_fields_filter_active = st.checkbox("🗑️ Felder aus JSON entfernen") if remove_fields_filter_active: fields_to_remove = st.multiselect("Wähle die Felder, die entfernt werden sollen", options=list(all_fields), default=[]) st.warning("Dieser Filter entfernt ausgewählte Felder aus den Datensätzen.") # Filter 6: Duplizierte Datensätze entfernen duplicate_filter_active = st.checkbox("🔁 Duplikate entfernen") if duplicate_filter_active: duplicate_fields = st.multiselect("Wähle die Felder, auf denen die Duplikate basieren sollen", options=list(all_fields), default=[]) st.warning("Dieser Filter entfernt doppelte Datensätze basierend auf den ausgewählten Feldern.") # Statusmeldungen und Debugging Informationen if st.button("✅ Daten filtern und speichern"): st.info("Starte Filterprozess...") filtered_data = data.copy() # Kopiere Originaldaten # Leere Felder filtern if empty_field_filter_active and selected_empty_fields: st.info("🚫 Leere Felder filtern...") filtered_data = [ item for item in filtered_data if all( (field_value := get_nested_value(item, field)) is not None and not is_field_empty(field_value) for field in selected_empty_fields ) ] st.write(f"Anzahl der Datensätze nach Filterung leere Felder: {len(filtered_data)}") # Feld-Wert Kombinationen filtern if field_value_filter_active and field_value_filters: st.info("🔍 Feld-Wert Kombinationen filtern...") for field, operator, value in field_value_filters: op_func = operator_map[operator] try: # Versuche den Wert in einen numerischen Typ zu konvertieren, wenn möglich try: value_converted = float(value) except ValueError: value_converted = value filtered_data = [ item for item in filtered_data if (field_value := get_nested_value(item, field)) is not None and ( (isinstance(field_value, list) and any( isinstance(v, (int, float, str)) and op_func(v, value_converted) for v in field_value )) or (isinstance(field_value, (int, float, str)) and op_func(field_value, value_converted)) ) ] except TypeError: st.error(f"Der Wert im Feld '{field}' kann nicht mit dem Operator '{operator}' verglichen werden.") st.write(f"Anzahl der Datensätze nach Feld-Wert Filter: {len(filtered_data)}") # Zeichenlänge filtern if length_filter_active and selected_length_fields: st.info("✂️ Filterung nach Zeichenlänge...") filtered_data = [ item for item in filtered_data if all( (field_value := get_nested_value(item, field)) is not None and ( (isinstance(field_value, str) and len(field_value) >= min_length) or (isinstance(field_value, list) and any(isinstance(v, str) and len(v) >= min_length for v in field_value)) ) for field in selected_length_fields ) ] st.write(f"Anzahl der Datensätze nach Filterung der Mindestzeichenlänge: {len(filtered_data)}") # Gleichverteilung filtern if balancing_filter_active and selected_balancing_fields: st.info("⚖️ Starte Gleichverteilung...") field_groups = defaultdict(list) for item in filtered_data: # Erstelle einen hashbaren Schlüssel, indem Listen in Tupel umgewandelt werden key = tuple( tuple(get_nested_value(item, field)) if isinstance(get_nested_value(item, field), list) else get_nested_value(item, field) for field in selected_balancing_fields ) field_groups[key].append(item) balanced_data = [] groups = list(field_groups.values()) if groups: group_cycle = cycle(groups) while len(balanced_data) < total_count and groups: try: group = next(group_cycle) if group: balanced_data.append(group.pop(0)) if not group: groups.remove(group) group_cycle = cycle(groups) except StopIteration: break filtered_data = balanced_data[:total_count] st.write(f"Anzahl der Datensätze nach Gleichverteilung: {len(filtered_data)}") # Duplikate entfernen if duplicate_filter_active and duplicate_fields: st.info("🔁 Duplikate entfernen...") initial_count = len(filtered_data) if duplicate_fields: filtered_data = pd.DataFrame(filtered_data).drop_duplicates(subset=duplicate_fields, keep='first').to_dict(orient='records') filtered_count = len(filtered_data) st.write(f"Duplikate entfernt: {initial_count - filtered_count}") st.write(f"Anzahl der verbleibenden Datensätze: {filtered_count}") else: st.warning("Bitte wählen Sie mindestens ein Feld für die Duplikatenfilterung aus.") # Felder entfernen if remove_fields_filter_active and fields_to_remove: st.info("🗑️ Felder entfernen...") filtered_data = remove_fields(filtered_data, fields_to_remove) st.write(f"Anzahl der Datensätze nach Entfernen der Felder: {len(filtered_data)} (Anzahl bleibt gleich)") # Speichern der gefilterten Daten mit Namenszusätzen timestamp = current_timestamp() filters_applied = [] if empty_field_filter_active and selected_empty_fields: filters_applied.append("emptyfields") if field_value_filter_active and field_value_filters: filters_applied.append("fieldvalue") if length_filter_active and selected_length_fields: filters_applied.append(f"minlength{min_length}") if balancing_filter_active and selected_balancing_fields: filters_applied.append("balancing") if duplicate_filter_active and duplicate_fields: filters_applied.append("duplicates") if remove_fields_filter_active and fields_to_remove: filters_applied.append("removefields") output_filename = f"{st.session_state.get('selected_file', 'output').split('.')[0]}_{'_'.join(filters_applied)}_{timestamp}.json" output_path = os.path.join(os.getcwd(), output_filename) save_json(filtered_data, output_path) st.write(f"Anzahl der Datensätze nach dem Speichern: {len(filtered_data)}") st.success(f"Gefilterte Daten wurden gespeichert als: {output_filename}") def data_viewer_tab(df): st.header("📁 Datenbetrachter") with st.expander("🔍 Vorschau des Datensatzes"): if 'viewer_index' not in st.session_state: st.session_state.viewer_index = 0 current_record = preview_data(df.to_dict(orient='records'), st.session_state.viewer_index) st.json(current_record) # Anzeige des aktuellen Datensatzes nach dem Button-Klick st.write(f"**Datensatz Nummer:** {st.session_state.viewer_index + 1}") # Buttons zum Blättern (jetzt unter den Elementen) col_prev, col_next = st.columns([1,1]) with col_prev: if st.button("⬅️ Vorheriger Datensatz", key='prev_viewer'): if st.session_state.viewer_index > 0: st.session_state.viewer_index -= 1 with col_next: if st.button("➡️ Nächster Datensatz", key='next_viewer'): if st.session_state.viewer_index < len(df) - 1: st.session_state.viewer_index += 1 st.subheader("📊 Datensätze anzeigen") st.markdown("**Hinweis:** Große Datensätze können sehr lange dauern, um angezeigt zu werden.") col_start, col_end = st.columns(2) with col_start: start_num = st.number_input("Start Datensatz Nummer", min_value=1, value=1, step=1, key='start_num') with col_end: end_num = st.number_input("Ende Datensatz Nummer", min_value=1, value=min(len(df), 10), step=1, key='end_num') st.write("") # Leerzeile if st.button("🔄 Daten anzeigen"): if end_num < start_num: st.error("Ende Nummer muss größer oder gleich Start Nummer sein.") elif end_num > len(df): st.error(f"Ende Nummer darf nicht größer als die Anzahl der Datensätze ({len(df)}) sein.") else: st.write(df.iloc[start_num-1:end_num]) def werteverteilung_tab(df): st.header("📈 Werteverteilung") metadata_fields = df.select_dtypes(include=['object', 'category']).columns.tolist() if not metadata_fields: st.write("Keine Metadatenfelder gefunden.") else: search_field = st.text_input("Feld suchen", "", key='metadata_search') if search_field: filtered_fields = [field for field in metadata_fields if search_field.lower() in field.lower()] else: filtered_fields = metadata_fields if filtered_fields: selected_fields = st.multiselect("Wähle Metadatenfelder zur Visualisierung", filtered_fields, key='metadata_select_multi') if selected_fields: for field in selected_fields: # Handle multiple values by splitting value_series = df[field].dropna().astype(str).str.split(', ').explode() value_counts = value_series.value_counts().head(20) st.write(value_counts.to_frame().reset_index().rename(columns={'index': field, field: 'Anzahl'})) # Truncate labels for readability df_counts = value_counts.to_frame().reset_index() df_counts.columns = [field, 'Anzahl'] df_counts[field] = df_counts[field].apply(lambda x: x[:20] + '...' if len(x) > 20 else x) fig = px.bar( df_counts, x='Anzahl', y=field, orientation='h', labels={'Anzahl': 'Anzahl der Vorkommen', field: 'Feld'}, title="", # Entfernt den Titel über der Grafik hover_data={'Anzahl': True} ) fig.update_traces(marker_color='blue') st.plotly_chart(fig, use_container_width=True, key=f'werteverteilung_{field}') else: st.write("Keine Felder gefunden, die dem Suchbegriff entsprechen.") def fuellstandsanalyse_tab(df): st.header("📊 Füllstandsanalyse") st.write("Filtere Daten basierend auf Feldern und Werten und analysiere den Füllstand der Metadaten.") # Auswahl der Felder und Werte zum Filtern st.subheader("🔍 Filter auswählen") selected_fill_fields = st.multiselect( "Felder zur Filterung auswählen", options=df.columns.tolist(), default=[] ) fill_field_values = {} for field in selected_fill_fields: unique_values = df[field].dropna().unique().tolist() # Optimierung: Verwenden Sie Caching für einzigartige Werte unique_values = st.cache_data(lambda x: x)(unique_values) selected_values = st.multiselect(f"Wähle Werte für {field}", unique_values, default=[], key=f"fill_{field}") fill_field_values[field] = selected_values # Option, ob Filter eine gemeinsame Datenmenge bilden oder getrennt betrachtet werden join_option = st.radio( "Wie sollen die Filter angewendet werden?", options=["Getrennt betrachten", "Gemeinsame Datenmenge bilden"], index=0 ) # Auswahl der Felder zur Anzeige des Füllstands st.subheader("📈 Füllstand anzeigen für:") display_fill_fields = st.multiselect( "Wähle Metadatenfelder zur Anzeige des Füllstands", options=df.columns.tolist(), default=[] ) if st.button("🔄 Füllstand analysieren"): st.info("Starte Füllstandsanalyse...") if selected_fill_fields and any(fill_field_values[field] for field in selected_fill_fields): if join_option == "Gemeinsame Datenmenge bilden": # Filtern der Daten, die alle ausgewählten Feld-Wert-Kombinationen erfüllen filtered_df = df.copy() for field, values in fill_field_values.items(): if values: filtered_df = filtered_df[filtered_df[field].isin(values)] subsets = {"Gemeinsame Datenmenge": filtered_df} else: # Jede Feld-Wert-Kombination als separate Teilmenge subsets = {} for field, values in fill_field_values.items(): if values: for value in values: subset_name = f"{field} = {value}" subsets[subset_name] = df[df[field].isin([value])] else: # Keine Filter angewendet, eine einzige Teilmenge subsets = {"Alle Daten": df} if display_fill_fields: # Begrenzen der Anzahl returnder "skipped"Grafiken, Noneum die Übersichtlichkeit zu wahren article = goose.extract(url=url) max_columns = 2 # Ändert die Anzahl der Spalten von 3 auf #2 Extract the relevant information fullnum_textsubsets = article.cleaned_textlen(subsets) # Cleaned full text num_cols title = article.title # Extracted title= min(max_columns, num_subsets) summarycols = articlest.meta_description[:500] if article.meta_description else full_text[:500] # Meta description as summary or first 500 charscolumns(num_cols) keywords = article.meta_keywords # Extracted meta keywordsfor idx, (subset_name, subset_df) in enumerate(subsets.items()): top_image = article.top_image.src if article.top_image else None #col URL= ofcols[idx the top image% num_cols] # Fallback: If full text is missing,with usecol: meta description if not full_text: # Füge einen Hinweistext über full_textder = article.meta_description or "No full text available for this page."Grafik hinzu return { st.markdown(f"**Filter:** {subset_name}") 'title': title, 'full_text': full_text, 'summary': summary, # Entfernt die Überschrift über den Grafiken 'keywords': keywords, 'top_image': top_image, fill_status = subset_df[display_fill_fields].notnull().mean() * 100 'url': url } fill_status except Exception as e:= fill_status.sort_values(ascending=False) st.error(f"Error scraping {url}: {e}") # Dynamische Anpassung returnder NoneGrafikgröße # Function to process the JSON file and enrich it with scraped data def process_json(data, url_field, timeout, save_folder, follow_redirect=True, skip_media_files=True, crawl_all=True, num_records=10): num_bars = len(fill_status) try: if totalnum_recordsbars = len(data)= 1: records_to_process = total_records if crawl_all else min(num_records, total_records) fig_height = 400 st.write(f"Processing {records_to_process} out of {total_records} records...") enriched_data = [] else: for i, record in enumerate(data[:records_to_process], 1): fig_height # Display the progress message for processing records= max(400, num_bars * 50) st.info(f"Processing record {i}/{records_to_process}", icon="ℹ️") # Extract URL using the selected field # Daten vorbereiten für zweifarbige Balken try: urlfill_percentage = eval(f"record{url_field}")fill_status # If URL isempty_percentage in= list100 format, convert it to a string (take the first URL)- fill_status fill_data = pd.DataFrame({ if isinstance(url, list): url = url[0]'Metadatenfeld': fill_status.index, except Exception as e: 'Gefüllt (%)': fill_percentage.values, st.warning(f"No valid URL found for record {i}/{records_to_process}. Skipping... (Error: {e})", icon="⚠️") 'Leer (%)': empty_percentage.values continue }) if url: # Melt Skipthe mediaDataFrame filesfür ifgestapelte theBalkendiagramme option is selected if skipfill_mediadata_filesmelted and any(url.lower().endswith(ext) for ext in SKIPPED_EXTENSIONS):= fill_data.melt(id_vars='Metadatenfeld', value_vars=['Gefüllt (%)', 'Leer (%)'], var_name='Status', value_name='Prozent') fig = stpx.warning(f"Skipping media file URL #{i}: {url}", icon="⚠️")bar( fill_data_melted, continue x='Prozent', # Display the message for scraping the current URL y='Metadatenfeld', st.success(f"Scraping URL #{i}: {url}", icon="🟢") color='Status', scraped_data = scrape_page(url, follow_redirect=follow_redirect) orientation='h', if scraped_data != "skipped" and scraped_data: title="", # OnlyEntfernt displayden theTitel summaryüber previewder messageGrafik without additional status info if scraped_data['summary']:labels={'Prozent': 'Prozent (%)', 'Metadatenfeld': 'Metadatenfeld'}, st.success(scraped_data['summary'][:250] + "..." if len(scraped_data['summary']) > 250 else scraped_data['summary']) hover_data={'Prozent': True, 'Status': True} ) fig.update_layout(barmode='stack', height=fig_height, showlegend=True) # Add scraped data to the record st.plotly_chart(fig, use_container_width=True, key=f'fuellstand_plot_{idx}') else: record['additional_data'] = { st.warning("Bitte wählen Sie mindestens ein Feld zur Anzeige des Füllstands aus.") def text_analysis_tab(df): st.header("📝 Textanalyse") text_fields = 'title': scraped_data['title'],df.select_dtypes(include=['object', 'category']).columns.tolist() if not text_fields: st.write("Keine Textfelder gefunden.") else: 'fullsearch_text': scraped_data['full_text'],_field = st.text_input("Feld suchen", "", key='text_search') if search_text_field: filtered_text_fields 'summary': scraped_data['summary'], = [field for field in text_fields if search_text_field.lower() in field.lower()] else: 'keywords': scraped_data['keywords'], filtered_text_fields = text_fields if filtered_text_fields: 'top_image': scraped_data['top_image'], selected_text_field = st.selectbox("Wähle ein Metadatenfeld mit Text", filtered_text_fields, key='text_select') if selected_text_field: 'final_url': scraped_data['url'] min_chars = st.number_input("Minimale Anzahl der Zeichen zum Filtern", min_value=0, value=0, step=1, }key='text_min_chars') text_analysis(df, enriched_data.append(recordselected_text_field, min_chars) else: # Introduce delay for the given timeout st.write("Keine Textfelder gefunden, die dem Suchbegriff entsprechen.") def time.sleepmain(timeout): st.set_page_config(page_title="Universelles Datentool für JSON # Save the enriched JSON file timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') Dateien", layout="wide") # Seitenleiste für Titel und JSON-Dateiauswahl with st.sidebar: enriched_file_name = osst.path.join(save_folder, f"data_enriched_{timestamp}.jsontitle("Universelles Datentool für JSON Dateien") with open(enriched_file_name, 'w', encoding='utf-8') as f: st.write("Dieses Tool ermöglicht die Analyse und Bearbeitung von JSON-Daten mit beliebiger Struktur.") json.dump(enriched_data, f, ensure_ascii=False, indent=4) data_dir = stos.successgetcwd(f"Enriched) data saved as {enriched_file_name}", icon="🟢") json_files except Exception as e:= list_json_files(data_dir) if not st.error(f"Error processing JSON file: {e}") # Function to extract available fields from JSON structure def extract_fields(data): field_set = set(json_files: st.warning(f"Keine JSON-Dateien im Verzeichnis '{data_dir}' gefunden. Bitte laden Sie eine Datei in das Verzeichnis hoch.") # Recursive function to explore the JSONselected_file structure= None def recurse_json(obj, parent_key=''): else: if isinstance(obj, dict): selected_file = st.selectbox("📂 Wähle eine JSON-Datei aus dem foraktuellen keyVerzeichnis", value in obj.items():json_files, key='json_select') new_key = f"{parent_key}['{key}']" if parent_key else f"['{key}']" st.button("🔍 Datei laden") and selected_file: with field_set.add(new_key)st.spinner("Verarbeite Datei..."): recurse_json(value, new_keydf = process_uploaded_file(os.path.join(data_dir, selected_file)) elif isinstance(obj, list): if not df.empty: for item in obj: df = recurse_json(item, parent_key) recurse_json(datamerge_similar_fields(df) return sorted(list(field_set)) # Streamlit UI st.title('JSON Web Scraper and Enricher using Goose3') # Upload JSON file uploaded_file = st.file_uploader("Choose a JSON file", type="json") if uploaded_file:session_state['df'] = df try: # Load the JSON data data = json.load(uploaded_file) st.session_state['selected_file'] = selected_file # Speichern des ausgewählten Dateinamens # Extract all field paths# fromVermerk theam JSONunteren structureEnde der Sidebar available_fields = extract_fields(data st.markdown("---") # Allow the user to choose a URL field st.markdown("**by Jan Schachtschabel**") # Überprüfen, ob Daten geladen sind url_field =if 'df' in st.selectbox("Select the URL field", available_fields, index=available_fields.index("['properties']['ccm:wwwurl']"))session_state and not st.session_state['df'].empty: df # Other options for processing= st.session_state['df'] timeout selected_file = st.number_input("Enter delay between requests (seconds)", min_value=0, value=0session_state.get('selected_file', 'output') save_folder = st.text_input("Folder to save enriched JSON", value=".")# Erstellen der Tabs im Hauptbereich follow_redirectstabs = st.checkboxtabs(["Datenbetrachter"Follow, redirects"Werteverteilung", value=True) "Füllstandsanalyse", "Textanalyse", "Datenfilter"]) # Option to skip media files (audio/video) with tabs[0]: skip_media_files = st.checkbox("Skip media files (e.g., .mp4, .mp3, .avi)", value=True data_viewer_tab(df) # Option to process all records or only a limited numberwith tabs[1]: werteverteilung_tab(df) crawl_all = st.checkbox("Crawl all records", value=True) with tabs[2]: num_records = st.number_input("Number of records to process", min_value=1, value=10, disabled=crawl_all fuellstandsanalyse_tab(df) if st.button("Start Processing")with tabs[3]: processtext_analysis_json(data, url_field, timeout, save_folder, follow_redirects, skip_media_files, crawl_all, num_records)tab(df) with tabs[4]: except Exception as e: json_filter_tab(df) if __name__ st.error(f"Error loading JSON file: {e}"== "__main__": main() |
Tool für die Bewertung der Neutralität
Python-Script zur Durchführung des Tests und der Bewertung mittels LLM
Code Block |
---|
# Script für die Bewertung der Neutralität (0-5) # Anforderungen: pip install streamlit openai pydantic scikit-learn scipy matplotlib numpy # Script als evaluate_neutralness_ai_singlescore.py speichern # Start: streamlit runevaluate_neutralness_ai_singlescore app.py import streamlit as st import json import os import matplotlib.pyplot as plt from datetime import datetime from openai import OpenAI from pydantic import BaseModel, ValidationError from sklearn.metrics import precision_score, f1_score, mean_absolute_error, mean_squared_error, r2_score from scipy.stats import pearsonr import numpy as np import time # Überprüfen, ob scipy installiert ist try: from scipy.stats import pearsonr except ImportError: st.error("Die Bibliothek 'scipy' ist nicht installiert. Bitte installieren Sie sie mit `pip install scipy`.") st.stop() # OpenAI-Client initialisieren client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) # Definiere das Schema der erwarteten Antwort mit Pydantic class EvaluationResponse(BaseModel): score: int reasoning: str # Funktion zur Erzeugung eines Zeitstempels def get_timestamp(): return datetime.now().strftime("%Y%m%d_%H%M%S") # Funktion zum Speichern von Dateien def save_file(filename, content, mode='w'): with open(filename, mode) as f: f.write(content) # Funktion zur Erstellung und Speicherung von Grafiken def save_figure(filename): plt.savefig(filename) plt.close() # Funktion zur farblichen Hervorhebung der Statusmeldungen def color_status(deviation): if deviation == 0: return "green" elif deviation == 1: return "yellow" elif deviation == 2: return "orange" else: return "red" # Funktion zur Erstellung einer verkürzten JSON def create_shortened_json(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields): shortened_data = [] # Sicherstellen, dass alle Listen gleich lang sind for i in range(min(len(data), len(ai_scores), len(ai_reasonings))): item = {} for field in selected_fields: # Extraktion der Werte basierend auf dem Feldpfad value = data[i].get(field, None) item[field] = value item.update({ "original_neutralness_score": neutralness_scores[i], "ai_neutralness_score": ai_scores[i], "ai_reasoning": ai_reasonings[i] }) shortened_data.append(item) return shortened_data # Funktion zur Rekursiven Extraktion von Feldnamen def extract_field_names(data): field_names = set() def recurse(item): if isinstance(item, dict): for key, value in item.items(): field_names.add(key) recurse(value) elif isinstance(item, list): for element in item: recurse(element) for entry in data: recurse(entry) return sorted(field_names) # Funktion zur Erstellung einer verkürzten JSON für Edgecases def create_shortened_json_edgecases(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields): return create_shortened_json(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields) # Hauptfunktion zur Analyse der Texte def analyze_texts(texts, neutralness_scores, model_choice, prompt, num_texts, data, json_filename, selected_fields): ai_scores = [] ai_reasonings = [] deviations = [] deviation_counts = {0: 0, 1: 0, 2: 0, 3: 0} failed_attempts = 0 special_cases = [] # Sammlung der Sonderfälle (Abweichung von 2 oder mehr) progress_bar = st.progress(0) status_text = st.empty() texts_to_evaluate = texts[:num_texts] # Nur eine bestimmte Anzahl bewerten total_texts = len(texts_to_evaluate) for i, text in enumerate(texts_to_evaluate): if text: # Kombiniere die ausgewählten Felder in einem einzigen Text combined_text = f"{prompt}\n\n" + "\n".join([f"{field}: {value}" for field, value in text.items() if value]) messages = [ {"role": "system", "content": "You are an AI tasked with evaluating the neutrality and constitutionality of educational content."}, {"role": "user", "content": combined_text} ] try: # Verwende response_format mit json_schema und füge den 'name'-Parameter hinzu response = client.chat.completions.create( model=model_choice, messages=messages, response_format={ "type": "json_schema", "json_schema": { "name": "neutrality_evaluation", # Required name field "schema": { "type": "object", "properties": { "score": {"type": "integer"}, "reasoning": {"type": "string"} }, "required": ["score", "reasoning"], "additionalProperties": False } } }, max_tokens=4000 ) # Antwort als JSON validieren evaluation_response = EvaluationResponse.parse_raw(response.choices[0].message.content) ai_score = evaluation_response.score ai_reasoning = evaluation_response.reasoning ai_scores.append(ai_score) ai_reasonings.append(ai_reasoning) # Vergleich der AI-Werte mit den Originalwerten original_score = neutralness_scores[i] deviation = abs(ai_score - original_score) deviations.append(deviation) # Abweichung speichern if deviation == 0: deviation_counts[0] += 1 elif deviation == 1: deviation_counts[1] += 1 elif deviation == 2: deviation_counts[2] += 1 special_cases.append(data[i]) # Als Sonderfall hinzufügen else: deviation_counts[3] += 1 special_cases.append(data[i]) # Als Sonderfall hinzufügen # Farbliche Statusmeldung je nach Abweichung color = color_status(deviation) st.markdown(f"<span style='color:{color}'>Text {i + 1} bewertet: AI Score = {ai_score} (Original: {original_score})</span>", unsafe_allow_html=True) except ValidationError as ve: st.error(f"Validierungsfehler bei Text {i + 1}: {ve}") failed_attempts += 1 except Exception as e: st.error(f"Fehler bei der Verarbeitung von Text {i + 1}: {e}") failed_attempts += 1 # Fortschritt aktualisieren progress = (i + 1) / total_texts progress_bar.progress(progress) status_text.text(f"Verarbeite Text {i + 1}/{total_texts}...") time.sleep(0.1) # Berechnung der durchschnittlichen Abweichung avg_deviation = np.mean(deviations) if deviations else 0 # Berechnung zusätzlicher Metriken mae = mean_absolute_error(neutralness_scores[:len(ai_scores)], ai_scores) mse = mean_squared_error(neutralness_scores[:len(ai_scores)], ai_scores) rmse = np.sqrt(mse) r2 = r2_score(neutralness_scores[:len(ai_scores)], ai_scores) pearson_corr, _ = pearsonr(neutralness_scores[:len(ai_scores)], ai_scores) # Berechnung von Precision und F1-Score y_true = neutralness_scores[:len(ai_scores)] # Originale Scores y_pred = ai_scores[:len(ai_scores)] # AI generierte Scores precision = precision_score(y_true, y_pred, average='weighted', zero_division=0) f1 = f1_score(y_true, y_pred, average='weighted') # Verkürzte JSON erstellen shortened_data = create_shortened_json(data, ai_scores, neutralness_scores, ai_reasonings, selected_fields) # Speichern der verkürzten JSON-Daten, strukturiert mit Indents shortened_filename = f"{json_filename}_shortened_{get_timestamp()}.json" with open(shortened_filename, 'w') as f: json.dump({ "data": shortened_data, "overall_results": { "precision": precision, "f1_score": f1, "mean_absolute_error": mae, "mean_squared_error": mse, "root_mean_squared_error": rmse, "r2_score": r2, "pearson_correlation": pearson_corr, "avg_deviation": avg_deviation } }, f, indent=4) # JSON strukturiert mit Indent st.success(f"Verkürzte JSON gespeichert als {shortened_filename}") # Verkürzte JSON für Edgecases erstellen (Abweichung >= 2) if special_cases: edgecase_indices = [i for i, dev in enumerate(deviations) if dev >= 2] shortened_edgecases = create_shortened_json_edgecases( [data[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices], [neutralness_scores[i] for i in edgecase_indices], [ai_reasonings[i] for i in edgecase_indices], selected_fields ) # Speichern der Edgecases JSON, strukturiert mit Indents edgecases_filename = f"{json_filename}_shortened_edge_cases_{get_timestamp()}.json" with open(edgecases_filename, 'w') as f: json.dump({ "data": shortened_edgecases, "overall_results": { "precision": precision_score([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices], average='weighted', zero_division=0), "f1_score": f1_score([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices], average='weighted'), "mean_absolute_error": mean_absolute_error([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices]), "mean_squared_error": mean_squared_error([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices]), "root_mean_squared_error": np.sqrt(mean_squared_error([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices])), "r2_score": r2_score([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices]), "pearson_correlation": pearsonr([neutralness_scores[i] for i in edgecase_indices], [ai_scores[i] for i in edgecase_indices])[0], "avg_deviation": np.mean([deviations[i] for i in edgecase_indices]) } }, f, indent=4) # JSON strukturiert mit Indent st.success(f"Edgecases JSON gespeichert als {edgecases_filename}") # Speichern der Ergebnisse mit zusätzlichen Metriken als Textdatei report_filename = f"{json_filename}_evaluation_report_{get_timestamp()}.txt" with open(report_filename, 'w') as f: f.write(f"Precision: {precision:.2f}\n") f.write(f"F1 Score: {f1:.2f}\n") f.write(f"Mean Absolute Error (MAE): {mae:.2f}\n") f.write(f"Mean Squared Error (MSE): {mse:.2f}\n") f.write(f"Root Mean Squared Error (RMSE): {rmse:.2f}\n") f.write(f"R² Score: {r2:.2f}\n") f.write(f"Pearson-Korrelation: {pearson_corr:.2f}\n") f.write(f"Average Deviation: {avg_deviation:.2f}\n") f.write(f"Failed Attempts: {failed_attempts}\n") f.write(f"Deviation Counts: {deviation_counts}\n") st.success(f"Bericht gespeichert als {report_filename}") # Rückgabe der berechneten Metriken return ai_scores, deviations, avg_deviation, failed_attempts, deviation_counts, special_cases, precision, f1, mae, mse, rmse, r2, pearson_corr # Streamlit UI zur Benutzerinteraktion st.title('Neutralitätsbewertung AI') # Titel entsprechend dem vorherigen Titel beibehalten uploaded_file = st.file_uploader("Lade eine JSON-Datei hoch", type="json") if uploaded_file: # JSON-Dateiname extrahieren json_filename = os.path.splitext(uploaded_file.name)[0] # JSON-Daten laden data = json.load(uploaded_file) # Rekursive Extraktion aller Feldnamen field_names = extract_field_names(data) # Standardvorgabe: 'properties_ccm:general_description', falls vorhanden default_fields = ['properties_ccm:general_description'] if 'properties_ccm:general_description' in field_names else [] # Mehrfachauswahl für die Metadatenfelder selected_fields = st.multiselect( "Wähle die Metadatenfelder für die Bewertung aus:", options=field_names, default=default_fields ) if not selected_fields: st.warning("Bitte wähle mindestens ein Metadatenfeld aus.") else: # Extrahieren der relevanten Felder basierend auf der Auswahl def extract_selected_fields(json_data, selected_fields): extracted = [] for item in json_data: record = {} for field in selected_fields: value = item.get(field, None) record[field] = value extracted.append(record) return extracted texts_to_evaluate = extract_selected_fields(data, selected_fields) # Extrahieren der neutralness_scores def extract_metadata(json_data, key): return [int(item.get(key, "0")) for item in json_data if item.get(key, "0").isdigit()] neutralness_scores = extract_metadata(data, "properties_ccm:oeh_quality_neutralness") if not neutralness_scores: st.error("Keine gültigen Neutralitätswerte gefunden in den Daten.") else: # Verteilung von ccm:oeh_quality_neutralness anzeigen st.subheader("Verteilung der Neutralitätswerte (Original)") fig, ax = plt.subplots() ax.hist(neutralness_scores, bins=range(0, 7), align='left', rwidth=0.8, color='skyblue', edgecolor='black') ax.set_xlabel('Neutralitätswert') ax.set_ylabel('Häufigkeit') st.pyplot(fig) # Speichern der Verteilungsgrafik dist_filename = f"{json_filename}_neutralness_distribution_{get_timestamp()}.png" save_figure(dist_filename) # Auswahl, ob alle oder nur bestimmte Datensätze bewertet werden sollen evaluate_all = st.checkbox("Alle Datensätze bewerten", value=True) max_evaluations = len(data) # Standardmäßig alle Datensätze bewerten if not evaluate_all: max_evaluations = st.number_input("Anzahl der zu bewertenden Datensätze", min_value=1, max_value=len(data), value=10) # Eingabefeld für den Prompt (Standardprompt ist vorausgefüllt) default_prompt = """ Bewerte die Neutralität und Verfassungskonformität eines Bildungsinhalts auf einer Skala von 0 bis 5 anhand der folgenden Kriterien. Berücksichtigen Sie aktuelle Diskussionen über Menschenrechte, das Grundgesetz der Bundesrepublik Deutschland und die Anforderungen an schulische Bildung. Begründen Sie die Bewertung ausführlich, indem Sie erklären, warum Sie sich für den gewählten Wert entschieden haben und warum andere Werte nicht zutreffen. ## Bewertungskriterien - **0 (manipulativ oder verfassungswidrig):** Der Text enthält manipulative Aussagen, die darauf abzielen, den Leser in eine bestimmte Richtung zu drängen. Der Text verstößt gegen verfassungsrechtliche Grundprinzipien, wie die Achtung der Menschenwürde oder demokratische Grundwerte (z. B. Förderung von Hassrede, Diskriminierung, Gewalt oder Verherrlichung verfassungswidriger Symbole). - **1 (unneutral oder problematisch):** Der Text zeigt deutliche Anzeichen von Einseitigkeit oder Voreingenommenheit. Inhalte sind zwar verfassungskonform, aber sie verletzen möglicherweise das Neutralitätsgebot, indem sie eine extreme oder engstirnige Sichtweise auf sensible Themen wie Politik, Religion, Menschenrechte, Sexualität oder Identität vertreten, ohne Alternativen oder Gegensätze zu beleuchten. - **2 (ideologisch eingefärbt, aber verfassungskonform):** Der Text ist von einer bestimmten ideologischen Ausrichtung geprägt, aber die Inhalte sind verfassungskonform und korrekt. Es wird jedoch nur eine Seite des Themas beleuchtet, und es fehlen ausgewogene Perspektiven. - **3 (ideologisch eingefärbt, aber transparent und pluralistisch):** Der Text hat eine erkennbare ideologische Ausrichtung, aber diese wird transparent gemacht. Der Autor stellt offen dar, dass es alternative Perspektiven gibt, die ebenfalls verfassungskonform sind. Der Inhalt wahrt das Gebot der Pluralität, wie es in der schulischen Bildung gefordert wird. - **4 (neutrale und ausgewogene Formulierung):** Der Text verwendet eine sachliche und ausgewogene Formulierung ohne erkennbare ideologische Voreingenommenheit. Er beleuchtet das Thema von verschiedenen verfassungskonformen Standpunkten und berücksichtigt Menschenrechte und demokratische Werte, wie sie im Grundgesetz verankert sind. - **5 (neutrale Formulierung von einem unabhängigen, wissenschaftlich fundierten Autor):** Der Text ist vollständig neutral formuliert, ohne ideologische oder politische Voreingenommenheit. Er stammt von einem nachweislich unabhängigen und wissenschaftlich fundierten Autor oder einer Institution, die keine politischen oder ideologischen Interessen vertritt. Der Inhalt entspricht höchsten Standards der Verfassungstreue, Menschenrechte und Anforderungen der Schulbildung. # Steps 1. **Analysiere den Text:** Identifizieren Sie alle relevanten Informationen und Aspekte, die auf Neutralität und Verfassungskonformität hin überprüft werden müssen. 2. **Bewerten Sie anhand der Skala:** Wählen Sie den zutreffenden Wert von 0 bis 5 basierend auf Ihrer Analyse. 3. **Begründung:** Erläutern Sie Ihre Bewertung. Begründen Sie in maximal drei Sätzen, warum Sie sich für diesen Wert entschieden haben. Gehen Sie darauf ein, warum andere Werte nicht passend sind. # Output Format - Eine einzelne Zahl von 0 bis 5. - Eine nachfolgende Begründung in maximal 3 Sätzen. # Beispiele **Input:** Text über das Grundgesetz der Bundesrepublik Deutschland. **Output:** 4 "Der Text beinhaltet eine sachliche und ausgewogene Formulierung verschiedener verfassungskonformer Standpunkte und respektiert demokratische Werte. Er vermeidet extreme Sichtweisen und beleuchtet diverse Perspektiven. Andere Werte treffen nicht zu, da keine einseitige ideologische Ausrichtung erkennbar ist." """ user_prompt = st.text_area("Passe deinen Prompt an", value=default_prompt, height=600) # Modellwahl hinzufügen (gpt-4o-mini als Standard und gpt-4o-2024-08-06) model_choice = st.selectbox( "Wähle das Modell für die Bewertung aus", options=["gpt-4o-mini", "gpt-4o-2024-08-06"], index=0 ) # Button zur Auslösung der Analyse if st.button("Bewerte Texte"): ai_scores, deviations, avg_deviation, failed_attempts, deviation_counts, special_cases, precision, f1, mae, mse, rmse, r2, pearson_corr = analyze_texts( texts_to_evaluate, neutralness_scores, model_choice, user_prompt, max_evaluations, data, json_filename, selected_fields) # Verteilung der Originaldaten am Ende erneut anzeigen st.subheader("Verteilung der Neutralitätswerte (Original)") fig, ax = plt.subplots() ax.hist(neutralness_scores, bins=range(0, 7), align='left', rwidth=0.8, color='skyblue', edgecolor='black') ax.set_xlabel('Neutralitätswert') ax.set_ylabel('Häufigkeit') st.pyplot(fig) # Speichern der Originaldaten-Verteilungsgrafik final_dist_filename = f"{json_filename}_neutralness_distribution_final_{get_timestamp()}.png" save_figure(final_dist_filename) # Verteilung der AI-Scores anzeigen st.subheader("Verteilung der Neutralitätswerte (AI)") fig, ax = plt.subplots() ax.hist(ai_scores, bins=range(0, 7), align='left', rwidth=0.8, color='lightgreen', edgecolor='black') ax.set_xlabel('AI Neutralitätswert') ax.set_ylabel('Häufigkeit') st.pyplot(fig) # Speichern der AI-Scores-Verteilungsgrafik ai_dist_filename = f"{json_filename}_ai_neutralness_distribution_{get_timestamp()}.png" save_figure(ai_dist_filename) # Abweichungsgrafik erstellen st.subheader("Abweichung der AI-Werte von den Originalwerten") fig, ax = plt.subplots() categories = ["Keine Abweichung", "1 Abweichung", "2 Abweichungen", "3+ Abweichungen"] counts = [deviation_counts[0], deviation_counts[1], deviation_counts[2], deviation_counts[3]] colors = ["green", "yellow", "orange", "red"] ax.bar(categories, counts, color=colors) ax.set_xlabel('Abweichungskategorie') ax.set_ylabel('Anzahl der Texte') st.pyplot(fig) # Speichern der Abweichungsgrafik deviation_filename = f"{json_filename}_deviation_distribution_{get_timestamp()}.png" save_figure(deviation_filename) # Grafiken für Precision, F1 Score und zusätzliche Metriken erstellen st.subheader("Modellleistungsmetriken") # Precision und F1 Score fig, ax = plt.subplots() metrics = ["Precision", "F1 Score"] scores = [precision, f1] ax.bar(metrics, scores, color=['blue', 'green']) ax.set_ylabel('Score') for i, v in enumerate(scores): ax.text(i, v + 0.01, f"{v:.2f}", ha='center', va='bottom') st.pyplot(fig) # Speichern der Precision und F1 Score Grafik precision_f1_filename = f"{json_filename}_precision_f1_score_{get_timestamp()}.png" save_figure(precision_f1_filename) # Zusätzliche Metriken mit Erklärungen anzeigen st.markdown(""" **Mean Absolute Error (MAE):** - Durchschnittlicher absoluter Unterschied zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Je niedriger der MAE, desto genauer sind die Vorhersagen des Modells. **Mean Squared Error (MSE):** - Durchschnitt der quadrierten Differenzen zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Betont größere Fehler stärker. Ein niedriger MSE zeigt eine gute Modellleistung an. **Root Mean Squared Error (RMSE):** - Quadratwurzel des MSE. - **Interpretation:** Gibt den Fehler in derselben Einheit wie die Zielvariable an. Niedrigere Werte sind besser. **R² Score:** - Maß dafür, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. - **Interpretation:** Werte nahe 1 bedeuten, dass das Modell die Varianz gut erklärt. **Pearson-Korrelation:** - Maß für die lineare Korrelation zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Werte nahe 1 oder -1 zeigen eine starke lineare Beziehung. Werte nahe 0 bedeuten keine lineare Beziehung. **Precision:** - Maß für die Genauigkeit der positiven Vorhersagen. - **Interpretation:** Ein höherer Precision-Wert zeigt, dass weniger falsche positive Vorhersagen gemacht werden. **F1 Score:** - Harmonisches Mittel von Precision und Recall. - **Interpretation:** Ein ausgewogenes Maß, das sowohl die Genauigkeit als auch die Vollständigkeit der Vorhersagen berücksichtigt. **Abweichung:** - Differenz zwischen den AI-bewerteten und den Originalwerten. - **Interpretation:** Niedrigere Abweichungswerte deuten auf eine höhere Übereinstimmung zwischen AI und Originalbewertungen hin. """) # Anzeige der zusätzlichen Metriken st.markdown(f"**Mean Absolute Error (MAE):** {mae:.2f}") st.markdown(f"**Mean Squared Error (MSE):** {mse:.2f}") st.markdown(f"**Root Mean Squared Error (RMSE):** {rmse:.2f}") st.markdown(f"**R² Score:** {r2:.2f}") st.markdown(f"**Pearson-Korrelation:** {pearson_corr:.2f}") st.markdown(f"**Precision:** {precision:.2f}") st.markdown(f"**F1 Score:** {f1:.2f}") st.markdown(f"**Durchschnittliche Abweichung:** {avg_deviation:.2f}") # Grafik für zusätzliche Metriken st.subheader("Zusätzliche Modellleistungsmetriken") metrics = ["MAE", "MSE", "RMSE", "R²", "Pearson-Korrelation", "Precision", "F1 Score", "Abweichung"] scores = [mae, mse, rmse, r2, pearson_corr, precision, f1, avg_deviation] colors = ['cyan', 'magenta', 'orange', 'purple', 'grey', 'blue', 'green', 'red'] fig, ax = plt.subplots(figsize=(12, 6)) ax.bar(metrics, scores, color=colors) ax.set_ylabel('Wert') ax.set_ylim(0, max(scores) * 1.2) # Anpassung des y-Bereichs for i, v in enumerate(scores): ax.text(i, v + max(scores)*0.01, f"{v:.2f}", ha='center', va='bottom') st.pyplot(fig) # Speichern der zusätzlichen Metriken Grafik additional_metrics_filename = f"{json_filename}_additional_metrics_{get_timestamp()}.png" save_figure(additional_metrics_filename) # Durchschnittliche Abweichung grafisch darstellen st.subheader("Durchschnittliche Abweichung") fig, ax = plt.subplots() ax.bar(["Durchschnittliche Abweichung"], [avg_deviation], color='purple') ax.set_ylabel('Abweichung') ax.text(0, avg_deviation + 0.01, f"{avg_deviation:.2f}", ha='center', va='bottom') st.pyplot(fig) # Speichern der Durchschnittlichen Abweichungsgrafik avg_deviation_filename = f"{json_filename}_avg_deviation_{get_timestamp()}.png" save_figure(avg_deviation_filename) st.write(f"**Precision:** {precision:.2f}") st.write(f"**F1 Score:** {f1:.2f}") st.write(f"**Mean Absolute Error (MAE):** {mae:.2f}") st.write(f"**Mean Squared Error (MSE):** {mse:.2f}") st.write(f"**Root Mean Squared Error (RMSE):** {rmse:.2f}") st.write(f"**R² Score:** {r2:.2f}") st.write(f"**Pearson-Korrelation:** {pearson_corr:.2f}") st.write(f"**Durchschnittliche Abweichung:** {avg_deviation:.2f}") # Speichern der Auswertung als Textdatei report_filename = f"{json_filename}_evaluation_report_{get_timestamp()}.txt" with open(report_filename, 'w') as f: f.write(f"Precision: {precision:.2f}\n") f.write(f"F1 Score: {f1:.2f}\n") f.write(f"Mean Absolute Error (MAE): {mae:.2f}\n") f.write(f"Mean Squared Error (MSE): {mse:.2f}\n") f.write(f"Root Mean Squared Error (RMSE): {rmse:.2f}\n") f.write(f"R² Score: {r2:.2f}\n") f.write(f"Pearson-Korrelation: {pearson_corr:.2f}\n") f.write(f"Average Deviation: {avg_deviation:.2f}\n") f.write(f"Failed Attempts: {failed_attempts}\n") f.write(f"Deviation Counts: {deviation_counts}\n") st.success(f"Bericht gespeichert als {report_filename}") # Zusammenfassung der Ergebnisse st.markdown(""" ### **Zusammenfassung der Ergebnisse** - **Precision:** Gibt an, wie genau die positiven Vorhersagen des Modells sind. - **F1 Score:** Harmonisches Mittel von Precision und Recall, gibt ein ausgewogenes Maß der Modellleistung. - **Mean Absolute Error (MAE):** Durchschnittlicher absoluter Unterschied zwischen den vorhergesagten und den tatsächlichen Werten. - **Mean Squared Error (MSE):** Durchschnitt der quadrierten Differenzen zwischen den vorhergesagten und den tatsächlichen Werten. - **Root Mean Squared Error (RMSE):** Quadratwurzel des MSE, gibt den Fehler in derselben Einheit wie die Zielvariable an. - **R² Score:** Maß dafür, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. - **Pearson-Korrelation:** Maß für die lineare Korrelation zwischen den vorhergesagten und den tatsächlichen Werten. - **Durchschnittliche Abweichung:** Durchschnittlicher Unterschied zwischen den AI-bewerteten und den Originalwerten. """) # Erklärung der Metriken st.markdown(""" --- ### **Erklärung der Metriken** **Mean Absolute Error (MAE):** - Durchschnittlicher absoluter Unterschied zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Je niedriger der MAE, desto genauer sind die Vorhersagen des Modells. **Mean Squared Error (MSE):** - Durchschnitt der quadrierten Differenzen zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Betont größere Fehler stärker. Ein niedriger MSE zeigt eine gute Modellleistung an. **Root Mean Squared Error (RMSE):** - Quadratwurzel des MSE. - **Interpretation:** Gibt den Fehler in derselben Einheit wie die Zielvariable an. Niedrigere Werte sind besser. **R² Score:** - Maß dafür, wie gut die Varianz der Zielvariable durch das Modell erklärt wird. - **Interpretation:** Werte nahe 1 bedeuten, dass das Modell die Varianz gut erklärt. **Pearson-Korrelation:** - Maß für die lineare Korrelation zwischen den vorhergesagten und den tatsächlichen Werten. - **Interpretation:** Werte nahe 1 oder -1 zeigen eine starke lineare Beziehung. Werte nahe 0 bedeuten keine lineare Beziehung. **Precision:** - Maß für die Genauigkeit der positiven Vorhersagen. - **Interpretation:** Ein höherer Precision-Wert zeigt, dass weniger falsche positive Vorhersagen gemacht werden. **F1 Score:** - Harmonisches Mittel von Precision und Recall. - **Interpretation:** Ein ausgewogenes Maß, das sowohl die Genauigkeit als auch die Vollständigkeit der Vorhersagen berücksichtigt. **Abweichung:** - Differenz zwischen den AI-bewerteten und den Originalwerten. - **Interpretation:** Niedrigere Abweichungswerte deuten auf eine höhere Übereinstimmung zwischen AI und Originalbewertungen hin. --- """) |
...