Zum Hauptinhalt springen Skip to page footer

Best Practice: Code-Entwicklung mit KI

Themenseite "KI im Bildungsmonitoring"

Auf einen Blick

Datenaufbereitung und -umformung: Einmal entwickelte Python- oder R-Skripte können Rohdatensätze automatisiert bereinigen, strukturieren und in das gewünschte Format bringen – vorausgesetzt, identische Datensätze müssen regelmäßig nach demselben Muster aufbereitet werden.

Datenvisualisierung und Diagrammerstellung: Auch die Erstellung von Visualisierungen wie Balken- oder Liniendiagramme lässt sich mit Python automatisieren. Wenn die zugrunde liegende Datenstruktur gleich bleibt, können einmal entwickelte Skripte jederzeit erneut ausgeführt werden und im Ergebnis die gewünschten Grafiken erzeugen.

Interaktive Web-Apps für Datenanalysen: Neben einfachen Diagrammen lassen sich mit Python auch Web-Visualisierungen erstellen, die flexibel in bestehende Websites eingebunden werden können und die Nutzer*innen zur interaktiven Auseinandersetzung mit den Daten einladen.

Automatisierte Berichterstattung als PDF: Selbst mehrseitige PDF-Berichte mit Daten, Diagrammen und dynamisch aus den Daten generierten Erläuterungstexten können mit Python erstellt und automatisch aktualisiert werden, sofern die Datengrundlage identisch strukturiert bleibt.

Best Practice: Code-Entwicklung mit generativer KI

Hinweis: Einige KI-Systeme ermöglichen die Auswahl bestimmter KI-Modelle, die besser zur Code-Generierung geeignet sind als andere Modelle, die möglicherweise bei der Texterstellung bessere Ergebnisse erzielen. Unserer Erfahrung nach können derzeit vor allem Google mit Gemini 2.5 Pro Experimental sowie Claude mit dem Modell 3.7 Sonnet als Code-Assistenten überzeugen. Aber auch ChatGPT mit den Modellen o1 und o3 oder Le Chat mit Mistral Large liefern schnelle, zuverlässige Outputs. Eine Alternative stellt der Code-Assistent GitHub Copilot dar, bei dem mit einem Klick zwischen Modellen verschiedener Anbieter (Claude 3.5 Sonnet, OpenAI o3 und GPT 4o) gewechselt werden kann (Stand April 2025).

Für die Code-Entwicklung haben wir bei den vorgestellten Best Practices mit den KI-Tools ChatGPT (Modelle o1 und o3) und Claude (Modell 3.7 Sonnet) sowie Python als Programmiersprache gearbeitet. Das beschriebene Vorgehen ist jedoch problemlos auch mit anderen statistischen Programmiersprachen wie R und alternativen, zur Codegenerierung geeigneten KI-Tools umsetzbar.

Auf dieser Unterseite zeigen wir Ihnen konkrete Einsatzmöglichkeiten für generative KI bei der Entwicklung von Python-Code – speziell für Routineaufgaben im Bildungsmonitoring, von der Datenaufbereitung bis zur Erstellung kurzer PDF-Reports.

Dabei ist wichtig: Auch mit KI-Unterstützung ist das Arbeiten mit Python, oder anderen statistischen Programmiersprachen wie R, kein Selbstläufer. Sie benötigen keine tiefgehenden Programmierkenntnisse, um entsprechende Vorhaben umzusetzen, doch mit einem gewissen Grundverständnis für die syntaxbasierte Datenverarbeitung gelangen Sie schneller zu Ergebnissen. 

Und gerade hier liegt ein weiteres Potenzial der Arbeit mit generativer KI: Denn die Chatbots lassen sich nicht nur zur Codegenerierung nutzen, sondern auch als persönliche Lernbegleiter. Teilen Sie der KI zum Beispiel in ihrem Prompt mit, dass Sie Einsteiger*in sind und dass Sie sich ausführliche Erklärungen zum generierten Code, zu Begriffen und zum Vorgehen wünschen. So wird der Chatbot zu Ihrem persönlichen Coding-Tutor und unterstützt Sie nicht nur bei der Entwicklung von Skripten, sondern auch beim Kompetenzaufbau.

Anwendungsfall 1: Einen Rohdatensatz aufbereiten und umformen

Wir beginnen dort, wo auch im beruflichen Alltag die Arbeit meistens beginnt: Bei der Aufbereitung von Rohdaten. Dieser Schritt kann in der Praxis mitunter mehr Zeit in Anspruch nehmen als die anschließende Analyse der Daten. Nehmen wir zum Beispiel die unten gezeigte Tabelle: 

  • Sie enthält viele Spalten, die für unsere Analyse irrelevant sind und die wir entfernen möchten, um eine übersichtlichere Tabelle zu erhalten.
  • Einige Spalten weisen fehlende Werte auf, die die Analyse erschweren und die wir nach einem einheitlichen Schema kodieren möchten.
  • Die Bezeichnungen der Spalten sind unpassend für unsere Zwecke und sollen durch „sprechende“ Spaltenbezeichnungen ersetzt werden.
  • Vor allem aber entspricht die Struktur der Tabelle nicht unseren Anforderungen und muss vor der Analyse umgeformt („pivotiert“) werden.

Genau bei solchen Aufgaben zeigt sich das große Potenzial der Automatisierung mithilfe von Python. Gehen wir z. B. von dem Szenario aus, dass die in der Tabelle enthaltenen Daten in regelmäßigen Abständen für die Aktualisierung eines Berichtsdokuments über die Entwicklung der Schüler*innenzahlen im gebundenen Ganztag benötigt werden. Anstatt die Tabelle mit den aktualisierten Daten jedes Mal händisch für die Auswertung aufzubereiten, könnte dieser Prozess mithilfe eines Skripts automatisiert werden. Wenn dann die nächste Fortschreibung des Berichts ansteht, kann das Skript einfach erneut auf die aktualisierte Tabelle ausgeführt werden und innerhalb von Sekunden die aufbereiteten Daten generieren. Wird diese Vorgehensweise auf mehrere (oder alle) der für den Bericht benötigten Tabellen ausgeweitet, können im Ergebnis viele Stunden an Arbeit für die manuelle Datenaufbereitung eingespart werden.

Die Erstellung eines solchen Skripts ist mit Python unkompliziert – besonders, wenn man sich dabei von einem KI-Tool unterstützen lässt. Das folgende Schaubild zeigt Best Practices zur Formulierung von entsprechenden Prompts, um optimale Ergebnisse bei der Entwicklung von Skripten zur Datenaufbereitung zu erzielen. Ein Beispiel-Prompt, der sich auf die oben gezeigte Tabelle bezieht, verdeutlicht diesen Ansatz.

Best Practice

Anforderungen präzise beschreiben

KI-Chatbots können Ihre Tabelle nicht sehen und arbeiten folglich nur mit den Informationen, die Sie bereitstellen. Daher ist es wichtig, dass die gewünschten Operationen präzise beschrieben und die zu bearbeitenden Spalten oder Zeilen eindeutig benannt werden.

Umgang mit fehlenden Werten klären

Fehlende Werte, die in den Ausgangsdaten mit Sonderzeichen wie „-“ kodiert wurden, müssen mit Pythons Standardwert „NaN“ (Not a Number) ersetzt werden, um Fehlermeldungen bei späteren Arbeitsschritten zu vermeiden.

Schritt-für-Schritt-Anweisungen

Bei komplexeren oder – wie in diesem Fall – besonders umfangreichen Aufgaben ist es hilfreich, die Aufgabe in einzelne Arbeitsschritte aufzuschlüsseln und sie nacheinander zu beschreiben.

Beispiel-Prompt

Erstelle ein neues dataframe aus dem dataframe "df", welches nur die Spalten „Zeit“, „1_Auspraegung_Label“, „2_Auspraegung_Label“, „3_Auspraegung_Label“, „4_Auspraegung_Label“, „5_Auspraegung_Label“, „NW-W01GT__Schueler/-innen__Anzahl“ übernimmt.

Benenne dann die folgenden Spalten nach dem vorgegebenen Schema um:
„Zeit“ zu „Schuljahr“,  „1_Auspraegung_Label“ zu „Gebietskoerperschaft“, „2_Auspraegung_Label“ zu „Ganztagsform“, „3_Auspraegung_Label“ zu „Geschlecht“, „4_Auspraegung_Label“ zu „Schulform“, „5_Auspraegung_Label“ zu „Traegerschaft“, „NW-W01GT__Schueler/-innen__Anzahl“ zu „Anzahl_Schueler_innen“

Entferne dann alle Zeilen, die in einer Spalte den Wert "Insgesamt" enthalten.
Ersetze anschließend in der Spalte „Anzahl_Schueler_innen“ alle "-" und "." mit NaN-Werten.

Pivotiere die Tabelle abschließend so, dass die Spalte "Anzahl_Schueler_innen" in mehrere Spalten aufgeteilt wird, sodass für jeden Wert in der Spalte "Geschlecht" eine separate Spalte entsteht.

Füge ausführliche Kommentierungen in den Code ein, damit ich ihn gut nachvollziehen und ggf. anpassen kann.

Zeige mir abschließend die ersten 5 Zeilen der neuen Tabelle an.


Abbildung 2: Best Practice bei der Formulierung von Prompts für Aufgaben aus dem Bereich der Datenaufbereitung. 

Aufgabenstellungen wie diese sind für die aktuelle Generation von KI-Chatbots keine große Herausforderung mehr. Führen wir einen entsprechenden Prompt in einem Tool wie ChatGPT, Claude oder Gemini aus, generiert uns der Chatbot üblicherweise bereits im ersten Anlauf einen Python-Code, der unsere Anforderungen exakt erfüllt. Den Code, mit dem in unserem Beispiel ChatGPT auf den oben präsentierten Beispiel-Prompt geantwortet hat, können Sie im folgenden Abschnitt einsehen:

Python-Code mit Farbcodierung
import pandas as pd
import numpy as np

# 1. Erstelle ein neues DataFrame mit den gewünschten Spalten
df_new = df[
    [
        "Zeit",
        "1_Auspraegung_Label",
        "2_Auspraegung_Label",
        "3_Auspraegung_Label",
        "4_Auspraegung_Label",
        "5_Auspraegung_Label",
        "NW-W01GT__Schueler/-innen__Anzahl"
    ]
].copy()

# 2. Spalten umbenennen
df_new = df_new.rename(
    columns={
        "Zeit": "Schuljahr",
        "1_Auspraegung_Label": "Gebietskoerperschaft",
        "2_Auspraegung_Label": "Ganztagsform",
        "3_Auspraegung_Label": "Geschlecht",
        "4_Auspraegung_Label": "Schulform",
        "5_Auspraegung_Label": "Traegerschaft",
        "NW-W01GT__Schueler/-innen__Anzahl": "Anzahl_Schueler_innen"
    }
)

# 3. Entferne alle Zeilen, die irgendwo den Wert "Insgesamt" haben
df_new = df_new[~df_new.eq("Insgesamt").any(axis=1)]

# 4. Ersetze "-" und "." in der Spalte "Anzahl_Schueler_innen" durch NaN und konvertiere zu numerischen Werten
df_new["Anzahl_Schueler_innen"] = df_new["Anzahl_Schueler_innen"].replace(["-", "."], np.nan)
df_new["Anzahl_Schueler_innen"] = pd.to_numeric(df_new["Anzahl_Schueler_innen"])

# 5. Pivotiere die Tabelle nach Geschlecht, so dass es zwei Spalten für die Anzahl der
#    männlichen und weiblichen Schüler*innen gibt
df_pivot = df_new.pivot_table(
    index=["Schuljahr", "Gebietskoerperschaft", "Ganztagsform", "Schulform", "Traegerschaft"],
    columns="Geschlecht",
    values="Anzahl_Schueler_innen"
).reset_index()

# 6. Zeige die ersten 5 Zeilen der neuen Tabelle an
df_pivot.head()

Kopieren wir diesen Code anschließend in unsere Python-Umgebung, um ihn auf unsere Daten auszuführen, erhalten wir im Ergebnis eine exakt nach unseren Vorgaben aufbereitete Tabelle: 

Die Erstellung unseres Skripts wäre damit abgeschlossen – es sei denn, wir wollen weitere Arbeitsschritte automatisieren und auch die anschließende Visualisierung der Daten in das Skript aufnehmen.

Anwendungsfall 2: Daten in einer Diagrammdarstellung visualisieren

Denn auch die Analyse und Visualisierung unserer Daten kann in das Skript integriert und somit automatisiert werden, wenn beispielsweise – um im oben beschriebenen Szenario zu bleiben – für einen kommunalen Bildungsbericht im zweijährigen Rhythmus identische Analysen auf Grundlage eines jeweils aktualisierten Datensatzes erstellt werden sollen. Statt diese Auswertungen und Visualisierungen für jede Fortschreibung des Berichts manuell anzufertigen, erstellt unser Python-Skript die erforderlichen Visualisierungen dann „auf Knopfdruck“, sobald uns die aktualisierten Daten vorliegen. Das spart nicht nur Zeit, sondern stellt auch sicher, dass die für den Bericht benötigten Diagramme fortlaufend in einem konsistenten Design und Format erstellt werden. 

Mit Python-Bibliotheken wie Matplotlib, Seaborn oder Plotly lässt sich eine Vielzahl an Diagrammtypen erstellen und – bei Bedarf – bis in die kleinsten Details anpassen. Im folgenden Schaubild zeigen wir einige Best Practices zur Formulierung von Prompts, die eine zielgenaue Generierung von Python-Code zur Erstellung entsprechender Visualisierungen unterstützen. Ein ausführlicher Beispiel-Prompt, der auf die oben gezeigte Tabelle abgestimmt ist, veranschaulicht die Anwendung dieser Vorgehensweise in der Praxis.

Best Practice

Filterkriterien konkretisieren

Wenn die Tabelle für das Diagramm noch gefiltert werden muss, formulieren Sie die Filterbedingungen präzise und detailliert, um nur die relevanten Daten für die Analyse auszuwählen.

Achsen und Aggregationsmethoden definieren

Beschreiben Sie detailliert, welche Spalten auf den Achsen abgebildet werden sollen und wie die Daten aggregiert oder berechnet werden sollen (z. B. Summe der Werte aus bestimmten Spalten).

Gestaltungsvorgaben präzise beschreiben

Formulieren Sie klare Anforderungen für die Gestaltung des Diagramms, etwa hinsichtlich Überschriften, Farben und Achsenskalierung.

Fachsprache aus Datenanalyse verwenden

Verwenden Sie gerne Fachbegriffe und spezifische Bezeichnungen aus der Datenanalyse und Diagrammerstellung, um der KI das Verständnis Ihrer Anforderungen zu erleichtern.

Beispiel-Prompt

Filtere die Tabelle so, dass nur Zeilen übrig bleiben, die in der Spalte "Gebietskoerperschaft" die Werte "Nordrhein-Westfalen" und "Düsseldorf, Kreisfreie Stadt" enthalten.

Erstelle zwei Liniendiagramme, die nebeneinander angezeigt werden.

Diagramm 1 enthält eine Linie. Diese trägt auf der x-Achse die Werte aus der Spalte "Schuljahr" ab und auf der y-Achse die Summen der Werte in den Spalten "Männlich" und "Weiblich" (nicht aber die männlichen und weiblichen Einzelwerte), wo in der Spalte Ganztag der Wert "Schüler/-innen im gebundenen Ganztagesbetrieb", in der Spalte Schulform der Wert "Gesamtschulen", in der Spalte "Gebietskoerperschaft" der Wert "Düsseldorf, Kreisfreie Stadt" und in der Spalte “Traegerschaft” der Wert "Öffentlich" steht.

Diagramm 2 enthält ebenfalls eine Linie. Diese trägt auf der x-Achse die Werte aus der Spalte "Schuljahr" ab und auf der y-Achse die Summen der Werte in den Spalten "Männlich" und "Weiblich" (nicht aber die männlichen und weiblichen Einzelwerte), wo in der Spalte Ganztag der Wert "Schüler/-innen im gebundenen Ganztagesbetrieb", in der Spalte Schulform der Wert "Gesamtschulen", in der Spalte "Gebietskoerperschaft" der Wert "Nordrhein-Westfalen" und in der Spalte “Traegerschaft” der Wert "Öffentlich" steht.

Für die Gestaltung der Abbildung sind mir folgende Aspekte wichtig:

  • Eine Überschrift, die den Gegenstand der Abbildung benennt (welcher empirische Sachverhalt wird dargestellt), einschließlich der verwendeten Analysedimensionen wie Raumebenen, abgebildeter Zeitraum etc. (Welche Vergleichsperspektiven und Differenzierungsmerkmale werden herangezogen?)
     
  • Eine Bildunterschrift, die auf folgende Quelle verweist: Statistische Ämter des Bundes und der Länder, 2024: Kommunale Bildungsdatenbank, online abrufbar unter https://www.bildungsmonitoring.de/"
     
  • Verwendung eines Farbschemas, welches folgende Farben verwendet: #9AB688, #2EA8D9, #FAC240
     
  • beide Diagramme sollen auf der y-Achse beim Nullpunkt beginnen, d.h. nicht skalieren oder “reinzoomen”

Abbildung 4: Best Practice bei der Formulierung von Prompts für Aufgaben aus dem Bereich der Datenvisualisierung. 

Führen wir diesen Prompt im KI-Chatbot aus, erhalten wir als Antwort einen ersten funktionsfähigen Python-Code. In unserem Versuch waren für eine optimale Diagrammdarstellung anschließend noch drei Anpassungsschleifen nötig, in denen gestalterische Details wie die Position der Legende oder der Abstand zwischen Überschrift und Diagramm überarbeitet wurde. Durch diesen iterativen Prozess wurde der Code schrittweise verfeinert, bis das gewünschte Ergebnis erzielt wurde. Sie können den finalen Code im folgenden Aufklappelement einsehen:

Python-Code mit Farbcodierung
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 1) DataFrame auf die gewünschten Gebietskörperschaften filtern
df_filtered = df_pivot[df_pivot['Gebietskoerperschaft'].isin([
    "Nordrhein-Westfalen",
    "Düsseldorf, Kreisfreie Stadt"
])]

# 2) Zwei Teil-DataFrames für unsere beiden Diagramme anlegen:
#    Diagramm 1: Düsseldorf, Kreisfreie Stadt
df_plot1 = df_filtered[
    (df_filtered['Ganztagsform'] == "Schüler/-innen im gebundenen Ganztagesbetrieb") &
    (df_filtered['Schulform'] == "Gesamtschulen") &
    (df_filtered['Traegerschaft'] == "Öffentlich") &
    (df_filtered['Gebietskoerperschaft'] == "Düsseldorf, Kreisfreie Stadt")
].copy()

#    Diagramm 2: Nordrhein-Westfalen
df_plot2 = df_filtered[
    (df_filtered['Ganztagsform'] == "Schüler/-innen im gebundenen Ganztagesbetrieb") &
    (df_filtered['Schulform'] == "Gesamtschulen") &
    (df_filtered['Traegerschaft'] == "Öffentlich") &
    (df_filtered['Gebietskoerperschaft'] == "Nordrhein-Westfalen")
].copy()

# 3) Summiere Männlich + Weiblich zu einer neuen Spalte "Summe"
df_plot1['Summe'] = df_plot1['Männlich'] + df_plot1['Weiblich']
df_plot2['Summe'] = df_plot2['Männlich'] + df_plot2['Weiblich']

# 4) Sortiere nach Schuljahr (falls nicht chronologisch sortiert)
#    (Voraussetzung: Schuljahr ist etwas, das sich sortieren lässt, z.B. 2010/11 < 2011/12 etc.)
df_plot1 = df_plot1.sort_values(by='Schuljahr')
df_plot2 = df_plot2.sort_values(by='Schuljahr')

# 5) Erstelle die Abbildung mit zwei nebeneinanderliegenden Subplots
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))

# 6) Linienplot für Düsseldorf
ax1.plot(df_plot1['Schuljahr'], df_plot1['Summe'], color="#2EA8D9", marker='o')
ax1.set_title("Düsseldorf, Kreisfreie Stadt", fontsize=11)
# y-Achse bei Null starten, obere Grenze etwas über dem Maximalwert
ax1.set_ylim(0, df_plot1['Summe'].max() * 1.1)

# 7) Linienplot für Nordrhein-Westfalen
ax2.plot(df_plot2['Schuljahr'], df_plot2['Summe'], color="#9AB688", marker='o')
ax2.set_title("Nordrhein-Westfalen", fontsize=11)
ax2.set_ylim(0, df_plot2['Summe'].max() * 1.1)

# 8) Gemeinsame übergeordnete Abbildungs-Überschrift:
fig.suptitle(
    "Entwicklung der Schüler/-innenzahlen im gebundenen Ganztagsbetrieb an öffentlichen Gesamtschulen\n"
    "in Düsseldorf, Kreisfreie Stadt und Nordrhein-Westfalen (Schuljahre 2005/06 - 2022/23)",
    fontsize=13
)

# 9) Achsenbeschriftungen setzen und Schuljahre um 45° drehen
for ax in (ax1, ax2):
    ax.set_xlabel("Schuljahr")
    ax.set_ylabel("Anzahl Schüler*innen")
    ax.tick_params(axis='x', rotation=45)  # x-Achsen-Beschriftung schräg stellen

# 10) Farb-Layout anpassen (noch eine dritte Farbe #FAC240 wäre verfügbar, z.B. für Hintergründe oder weitere Elemente)
#     Hier z.B. nur ein Beispiel für den Hintergrund bei ax1 oder ax2:
# ax1.set_facecolor("#FAC240")

# 11) Bildunterschrift (Quelle) hinzufügen
#    Mit `plt.figtext` kannst Du unten im Diagramm eine zusätzliche Textzeile platzieren.
plt.figtext(
    0.5,                           # x-Position (zentriert)
    0.01,                          # y-Position, sehr weit unten
    ("Quelle: Statistische Ämter des Bundes und der Länder, 2024: Kommunale Bildungsdatenbank, "
     "online abrufbar unter https://www.bildungsmonitoring.de/"),
    ha="center", fontsize=10
)

# 12) Layout anpassen und anzeigen
plt.tight_layout(rect=[0, 0.03, 1, 0.95])  # rect-Anpassung damit der suptitle & figtext nicht abgeschnitten werden
plt.show()

Kopieren wir diesen Code anschließend in unsere Python-Umgebung, um ihn auf unsere Tabelle auszuführen, erhalten wir im Ergebnis eine exakt nach unseren Vorgaben erstellte Datenvisualisierung: 

Anwendungsfall 3: Eine Diagrammdarstellung mit einem dynamischen Erläuterungstext versehen

An diesem Punkt verfügen wir bereits über ein ausgesprochen hilfreiches Skript, welches die Aufbereitung, Auswertung und Visualisierung unserer Datentabelle vollumfänglich automatisiert. In der Praxis könnte die generierte Diagrammabbildung nun in einen regelmäßig erscheinenden Bericht integriert und mit Hinweisen zur Interpretation und Einordnung versehen werden: Was genau sehen die Betrachter*innen in der Abbildung? Auf welche Entwicklung, welches Ergebnis möchte ich die Betrachter*innen hinweisen? Und Sie ahnen es bereits: Auch die Erstellung eines solchen Begleittextes zu unserem Diagramm kann zuverlässig in das Skript aufgenommen und somit automatisiert werden. Das Besondere dabei: Damit dieser Text flexibel bleibt und sich an aktualisierte Daten anpasst, wird er dynamisch aus den analysierten Daten generiert.

Was bedeutet das genau? Anstatt einen statischen Text zu schreiben, der immer dieselben Inhalte liefert – etwa unabhängig davon, ob die analysierte Zeitreihe bis 2020 oder 2024 reicht –, wird der Text aus den Daten selbst abgeleitet. Das heißt, das Skript erkennt automatisch, welche Gebietskörperschaften verglichen werden, welche Werte dargestellt sind und ob beispielsweise ein Anstieg oder ein Rückgang der Zahlen vorliegt. Diese Informationen werden als „Variablen“ direkt in den Begleittext eingefügt, der sich dadurch bei jedem Durchlauf des Skripts an die jeweils aktuellen Daten anpasst. So funktioniert das Skript auch, wenn wir statt Düsseldorf plötzlich eine andere Region wie Bonn oder Münster betrachten oder wenn Daten für ein neues Berichtsjahr vorliegen.

In unserem bereits vorliegenden Beispieldiagramm soll der Text unterhalb der Diagramme folgende Informationen bereitstellen:

  • Begriffliche Klärung
    Eine kurze Erklärung des Konzepts des gebundenen Ganztags.
     
  • Zentrale Erkenntnisse der Datenauswertung
    Eine Beschreibung der Entwicklung der Schülerzahlen in zwei frei wählbaren Gebietskörperschaften.
     
  • Vergleich
    Eine Bewertung der relativen Entwicklung in den beiden Gebietskörperschaften (z. B. der Stadt Düsseldorf und dem Land NRW), die Unterschiede in der Stärke oder Richtung der Trends hervorhebt.

Das folgende Schaubild zeigt, welche Best Practices bei der Formulierung entsprechender Prompts für die Verwendung in einem KI-Tool berücksichtigt werden sollten. Dabei gehen wir davon aus, dass – wie in unserem Fall – zuvor bereits der Code zur Generierung eines Diagramms erzeugt wurde und bloß noch weiterentwickelt werden soll. Der Beispiel-Prompt illustriert das Vorgehen und setzt das Gespräch mit der KI an dem Punkt fort, wo der Beispiel-Prompt in Abb. 4 (zur Generierung des Diagramms) aufgehört hatte.

Best Practice

Auf dynamische Inhalte verweisen

Heben Sie in Ihrem Prompt hervor, dass der Text auf der Basis der Daten generiert werden soll, die dem Diagramm zugrunde liegen. Dieses Vorgehen gewährleistet, dass die generierten Texte stets aktuell und präzise sind – selbst wenn die analysierten Daten oder die verglichenen Gebietskörperschaften wechseln. Fehleranfällige manuelle Anpassungen entfallen vollständig.

Integration in das Layout

Fordern Sie eine Anpassung des bestehenden Diagramm-Layouts an die Platzierung des Textes. Zum Beispiel können Sie betonen, dass der Text direkt unterhalb des Diagramms platziert werden soll, oder dass der Text leserlich eingebunden wird, ohne die Darstellung der Diagramme zu beeinträchtigen.

Beispiel-Prompt

Ergänze den bisherigen Code so, dass er unterhalb der Diagramme einen Erläuterungstext platziert, der eine knappe Analyse der Darstellung vornimmt. Insbesondere soll ein Vergleich der relativen Entwicklung der Zahlen in den beiden Gebietskörperschaften daraus hervorgehen. Der Text sollte benennen, ob die Entwicklung in der jeweiligen Gebietskörperschaft steigend oder sinkend war, und ob sie stärker oder schwächer gestiegen oder gesunken ist, als in der Vergleichsgebietskörperschaft. Ein knapper Einführungstext, der erklärt, was der gebundene Ganztag ist, soll ebenfalls aufgenommen werden. Der gesamte Text soll durchgehend unterhalb der beiden Diagramme platziert werden, nicht in zwei Spalten aufgeteilt.

Wichtig dabei: Es soll ein "dynamischer" Text werden, der auch funktioniert, wenn ich statt "Düsseldorf, Kreisfreie Stadt" eine andere Gebietskörperschaft mit "Nordrhein-Westfalen" vergleiche. D.h. also, die entsprechenden Werte sollten aus der gefilterten Tabelle entnommen werden und nicht statisch definiert sein.


Abbildung 6: Best Practice bei der Formulierung von Prompts für die Entwicklung von dynamischen Erläuterungstexten.

Da wir in diesem Beispiel auf ein bereits generiertes Diagramm aufbauen und dieses „nur“ um den dynamischen Begleittext ergänzen möchten, liefert uns die KI ausgehend vom oben präsentierten Beispiel-Prompt in nur wenigen Überarbeitungsschleifen das gewünschte Ergebnis. Der generierte Code kann im folgenden Aufklappelement eingesehen werden, ist aber in weiten Teilen mit dem bereits im vorherigen Schritt generierten Code identisch:

Python-Code mit Farbcodierung
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 1) DataFrame auf die gewünschten Gebietskörperschaften filtern
df_filtered = df_pivot[df_pivot['Gebietskoerperschaft'].isin([
    "Nordrhein-Westfalen",
    "Düsseldorf, Kreisfreie Stadt"
])]

# 2) Zwei Teil-DataFrames für unsere beiden Diagramme anlegen:
#    Diagramm 1: z. B. Düsseldorf, Kreisfreie Stadt
df_plot1 = df_filtered[
    (df_filtered['Ganztagsform'] == "Schüler/-innen im gebundenen Ganztagesbetrieb") &
    (df_filtered['Schulform'] == "Gesamtschulen") &
    (df_filtered['Traegerschaft'] == "Öffentlich") &
    (df_filtered['Gebietskoerperschaft'] == "Düsseldorf, Kreisfreie Stadt")
].copy()

#    Diagramm 2: Nordrhein-Westfalen
df_plot2 = df_filtered[
    (df_filtered['Ganztagsform'] == "Schüler/-innen im gebundenen Ganztagesbetrieb") &
    (df_filtered['Schulform'] == "Gesamtschulen") &
    (df_filtered['Traegerschaft'] == "Öffentlich") &
    (df_filtered['Gebietskoerperschaft'] == "Nordrhein-Westfalen")
].copy()

# 3) Summiere Männlich + Weiblich zu einer neuen Spalte "Summe"
df_plot1['Summe'] = df_plot1['Männlich'] + df_plot1['Weiblich']
df_plot2['Summe'] = df_plot2['Männlich'] + df_plot2['Weiblich']

# 4) Sortiere nach Schuljahr (falls nicht chronologisch sortiert)
df_plot1 = df_plot1.sort_values(by='Schuljahr')
df_plot2 = df_plot2.sort_values(by='Schuljahr')

# ---------------------------
# Dynamische Analyse vorbereiten
# ---------------------------

def berechne_entwicklung(df_plot):
    """Gibt zu einem gefilterten DataFrame (für genau 1 Gebietskörperschaft)
    die früheste und späteste Summe zurück sowie relative Veränderung."""
    if df_plot.empty:
        return None

    # Name der Gebietskörperschaft (alle Zeilen enthalten denselben Wert)
    region = df_plot['Gebietskoerperschaft'].iloc[0]

    # Erster und letzter Wert
    earliest_year = df_plot['Schuljahr'].iloc[0]
    latest_year = df_plot['Schuljahr'].iloc[-1]
    earliest_val = df_plot['Summe'].iloc[0]
    latest_val = df_plot['Summe'].iloc[-1]

    # Absolute und relative Veränderung
    diff_val = latest_val - earliest_val
    # Um Division durch 0 zu vermeiden
    if earliest_val == 0:
        rel_diff_pct = None
    else:
        rel_diff_pct = (diff_val / earliest_val) * 100

    return {
        'region': region,
        'earliest_year': earliest_year,
        'latest_year': latest_year,
        'earliest_val': earliest_val,
        'latest_val': latest_val,
        'diff_val': diff_val,
        'rel_diff_pct': rel_diff_pct
    }

# Entwicklung für beide Teil-DataFrames berechnen
entwicklung1 = berechne_entwicklung(df_plot1)
entwicklung2 = berechne_entwicklung(df_plot2)

def generate_analysis_text(entw1, entw2):
    """Erzeugt einen dynamischen Analyse- und Erläuterungstext
       basierend auf den Daten der beiden Gebietskörperschaften."""

    if not entw1 or not entw2:
        return "Für mindestens eine Gebietskörperschaft liegen keine Daten vor."

    # Kurze Erklärung zum gebundenen Ganztag
    intro_text = ("Unter gebundenem Ganztag versteht man eine Organisationsform von Schule, "
    "in der der Unterricht und zusätzliche Angebote (z. B. Arbeitsgemeinschaften, "
    "Förderstunden, Projekte) über den ganzen Tag hinweg verpflichtend für alle "
    "Schüler/-innen stattfinden. Ziel ist meist eine engere Verzahnung von "
    "Unterricht und Freizeitangeboten sowie eine intensivere pädagogische Betreuung."
    )

    # Hilfsfunktionen für "gestiegen/gesunken" etc. (Verb und passendes Substantiv)
    def get_trend_verb(diff):
        """Gibt das Verb zurück: 'gestiegen', 'gesunken' oder 'konstant geblieben'."""
        if diff > 0:
            return "gestiegen"
        elif diff < 0:
            return "gesunken"
        else:
            return "konstant geblieben"

    def get_trend_noun(diff):
        """Gibt das passende Substantiv zurück: 'Anstieg', 'Rückgang' oder 'keine Veränderung'."""
        if diff > 0:
            return "Anstieg"
        elif diff < 0:
            return "Rückgang"
        else:
            return "keine Veränderung"

    # Prozentangaben schön formatieren
    def fmt_rel_diff(entw):
        if entw['rel_diff_pct'] is None:
            # Wenn earliest_val = 0 oder wir keine Zahlen haben
            return "nicht prozentual vergleichbar"
        else:
            return f'{abs(entw["rel_diff_pct"]):.1f}%'

    # Ermitteltes Verb und Substantiv für beide Gebietskörperschaften
    trend_verb_1 = get_trend_verb(entw1['diff_val'])
    trend_verb_2 = get_trend_verb(entw2['diff_val'])
    trend_noun_1 = get_trend_noun(entw1['diff_val'])
    trend_noun_2 = get_trend_noun(entw2['diff_val'])

    pct1 = fmt_rel_diff(entw1)
    pct2 = fmt_rel_diff(entw2)

    # Dynamischer Text:
    #   - Erwähnt den jeweiligen Start- und Endwert (abgerundet),
    #   - nennt das Verb (gestiegen/gesunken usw.) + prozentuale Veränderung.
    text_1 = (
        f'An den öffentlichen Gesamtschulen in {entw1["region"]} ist die Zahl der Schüler/-innen im gebundenen Ganztag von {int(entw1["earliest_val"])} im Schuljahr {entw1["earliest_year"]} auf {int(entw1["latest_val"])} im Schuljahr {entw1["latest_year"]} {trend_verb_1} (um ca. {pct1}). Zum Vergleich: An den öffentlichen Gesamtschulen in {entw2["region"]} ist die Zahl der Schüler/-innen im gebundenen Ganztag von {int(entw2["earliest_val"])} im Schuljahr {entw2["earliest_year"]} auf {int(entw2["latest_val"])} im Schuljahr {entw2["latest_year"]} {trend_verb_2} (um ca. {pct2}).'
    )

    # Vergleich, welche Gebietskörperschaft stärker oder schwächer gestiegen/gesunken ist
    # Wir vergleichen den Betrag der relativen Veränderung
    if (entw1['rel_diff_pct'] is None) or (entw2['rel_diff_pct'] is None):
        comparison_text = (
            "Ein direkter Vergleich der relativen Veränderung ist nicht möglich, "
            "da für mindestens eine Gebietskörperschaft zu wenige Daten vorliegen "
            "oder der Startwert 0 war."
        )
    else:
        abs_diff1 = abs(entw1['rel_diff_pct'])
        abs_diff2 = abs(entw2['rel_diff_pct'])

        if abs_diff1 > abs_diff2:
            comparison_text = (
                f'Damit war der {trend_noun_1} in {entw1["region"]} '
                f'stärker als der {trend_noun_2} in {entw2["region"]}.'
            )
        elif abs_diff1 < abs_diff2:
            comparison_text = (
                f'Damit war der {trend_noun_2} in {entw2["region"]} '
                f'stärker als der {trend_noun_1} in {entw1["region"]}.'
            )
        else:
            if trend_verb_1 == trend_verb_2:
                comparison_text = (
                    f'Beide Gebietskörperschaften haben sich in ähnlichem Umfang {trend_verb_1}.'
                )
            else:
                comparison_text = (
                    "Beide Gebietskörperschaften haben eine ähnliche Veränderung erfahren."
                )

    # Gesamter Analyse-Text
    analysis_text = (
        f'{intro_text}\n\n{text_1}\n\n{comparison_text}'
    )

    return analysis_text

analysis_text = generate_analysis_text(entwicklung1, entwicklung2)

# ---------------------------
# Erstelle die Plots
# ---------------------------

fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(14, 6))

# Diagramm 1
ax1.plot(df_plot1['Schuljahr'], df_plot1['Summe'], color="#2EA8D9", marker='o')
ax1.set_title(df_plot1['Gebietskoerperschaft'].iloc[0], fontsize=11)
ax1.set_ylim(0, df_plot1['Summe'].max() * 1.1 if not df_plot1.empty else 1)

# Diagramm 2
ax2.plot(df_plot2['Schuljahr'], df_plot2['Summe'], color="#9AB688", marker='o')
ax2.set_title(df_plot2['Gebietskoerperschaft'].iloc[0], fontsize=11)
ax2.set_ylim(0, df_plot2['Summe'].max() * 1.1 if not df_plot2.empty else 1)

# Übergeordneter Titel
fig.suptitle(
    "Entwicklung der Schüler/-innenzahlen im gebundenen Ganztagsbetrieb an öffentlichen Gesamtschulen\n"
    "in Düsseldorf, Kreisfreie Stadt und Nordrhein-Westfalen (Schuljahre 2005/06 - 2022/23)",
    fontsize=13
)

# Achsenbeschriftungen und X-Achsen drehen
for ax in (ax1, ax2):
    ax.set_xlabel("Schuljahr")
    ax.set_ylabel("Anzahl Schüler*innen")
    ax.tick_params(axis='x', rotation=45)

# ---------------------------
# Text-Elemente hinzufügen
# ---------------------------

# Quellenverweis (bereits bestehend)
plt.figtext(
    0.5,
    0.11,
    ("Quelle: Statistische Ämter des Bundes und der Länder, 2024: Kommunale Bildungsdatenbank, "
     "online abrufbar unter https://www.bildungsmonitoring.de/"),
    ha="center",
    fontsize=8
)

# Dynamischer Erläuterungstext (Analyse)
# Wir setzen ihn etwas höher als die Quelle, damit sich beide Texte nicht überlagern.
plt.figtext(
    0.05,  # x auf 0.05 setzen für linksbündig
    -0.14,  # höher als 0.01, damit kein Überlapp
    analysis_text,
    ha="left",  # ha auf "left" setzen
    wrap=True,  # Zeilenumbruch
    fontsize=10
)

# Layout anpassen
plt.tight_layout(rect=[0, 0.12, 1, 0.90])
# rect-Anpassung:
#  - 0.12 für unten, damit wir für figtext etwas mehr Platz haben
#  - 0.90 für oben, damit der suptitle gut zu sehen ist

plt.show()

Kopieren wir den gesamten Code anschließend in unsere Python-Umgebung, um ihn auf unsere Tabelle auszuführen, erhalten wir im Ergebnis eine mit dem gewünschten Erläuterungstext ergänzte Diagrammdarstellung:

Wenn anschließend z. B. ein Diagramm für eine andere Kommune erstellt werden soll, kann im Code einfach der Name “Düsseldorf, Kreisfreie Stadt” durch den Namen der gewünschten Kommune ersetzt werden. Das Skript erzeugt dann automatisch ein angepasstes Diagramm (einschließlich des Erläuterungstextes) für diese Kommune, solange die entsprechenden Daten in der Tabelle enthalten sind:

Anwendungsfall 4: Eine Datenanalyse als interaktive Web-App exportieren

An diesem Punkt haben wir bereits schrittweise ein Python-Skript entwickelt, das unsere Daten zur Ganztagsbildung nicht nur automatisch aufbereitet, auswertet und visualisiert, sondern auch einen dynamischen, auf die Daten abgestimmten Begleittext zur Visualisierung generiert. Das Diagramm wurde dabei bislang als eine Bilddatei ausgegeben, die sich beispielsweise für die Erstellung eines gedruckten Bildungsberichts verwenden ließe. Doch was, wenn dies gar nicht beabsichtigt ist, sondern die Analyse beispielsweise im Kontext einer Informations-Homepage zur kommunalen Ganztagsbildung erscheinen soll? Natürlich könnte man auch hierzu einfach die oben generierte Bilddatei verwenden. Zeitgemäßer wäre es jedoch, die Analyse in Form einer kleinen Web-App in unsere Homepage aufzunehmen.

Um dies zu erreichen, verwenden wir in diesem Arbeitsschritt die Python-Bibliothek Plotly und passen das Skript so an, dass unser Diagramm am Ende in Form einer HTML-Datei ausgegeben wird, die wir in eine bestehende Webseite integrieren können. Aus unserer statischen Bilddatei wird dadurch eine interaktive Web-App, mit der die Nutzer*innen direkt auf der Homepage interagieren und sich z. B. nähere Informationen zu einzelnen Datenpunkten einblenden lassen können. Diese Interaktivität ermöglicht es den Nutzer*innen, die Informationen aus dem Diagramm besser zu erfassen und aktiv zu erkunden.

Das folgende Schaubild zeigt, welche Best Practices bei der Formulierung entsprechender Prompts für KI-Tools berücksichtigt werden sollten. Dabei gehen wir davon aus, dass – wie in unserem Fall – zuvor bereits der Code zur Generierung eines Diagramms erzeugt wurde und dieser lediglich angepasst werden soll, um das Ergebnis als HTML-Datei (statt als Bilddatei) auszugeben. Der Beispiel-Prompt illustriert das Vorgehen und setzt die Konversation mit unserem KI-Assistenten an dem Punkt fort, wo der Beispiel-Prompt in Abb. 6 (zur Generierung des Diagramms mit dynamischem Text) aufgehört hatte.

Best Practice

Anforderungen an die Interaktivität spezifizieren

Geben Sie explizit an, welche Interaktionselemente gewünscht sind, z. B. Tooltips, die beim Überfahren von Datenpunkten Werte anzeigen, oder Schaltflächen, mit denen zwischen verschiedenen Ansichten umgeschaltet werden kann.

Flexibilität durch dynamische Inhalte sicherstellen

Beschreiben Sie, wie die Visualisierung auf Änderungen in den Daten reagieren soll. Geben Sie an, dass interaktive Elemente wie Schaltflächen mit dynamischen Titel- und Achsenbeschriftungen arbeiten sollen, die sich an die jeweilige Auswahl (z. B. „Öffentlich“ oder „Privat“) anpassen.

Technische Anforderungen an die HTML-Ausgabe formulieren

Fordern Sie eine Ausgabe, die als eigenständige HTML-Datei funktioniert und leicht per Iframe in eine Webseite eingebunden werden kann. Erwähnen Sie, dass alle Daten in die HTML-Datei eingebettet werden sollen, um externe Abhängigkeiten zu minimieren.

Beispiel-Prompt

Ich habe folgenden Python-Code, der mir ein Diagramm als Bilddatei generiert:

[zuvor generierter Code, der ein Diagramm als Bilddatei erzeugt]

Ich möchte diesen Code so anpassen, dass das Diagramm am Ende als interaktive HTML-Abbildung ausgegeben wird (nicht - wie aktuell - als Bilddatei), die ich auf einer Homepage einbinden kann und die - wenn ich mit der Maus über die Linien gehe - die dahinterliegenden Werte der einzelnen Datenpunkte als Tool-Tip einblendet.

Baue außerdem dort, wo das Diagramm im aktuellen Code auf die Trägerschaft „Öffentlich“ filtert, in dem HTML-Diagramm stattdessen eine Schaltfläche ein, mit der die Anwender selbst auf einzelne Werte in der Spalte „Traegerschaft“ filtern können. Stelle sicher, dass der Titel des Diagramms auf diese Filterdefinitionen des Anwenders reagiert und sich dynamisch anpasst.

Ganz wichtig dabei wäre, dass diese HTML-Datei einfach z. B. per iFrame in meine Webseite eingebunden werden kann und nicht als eigenständige App auf einem Server laufen muss. Das würde auch bedeuten, dass die zugrundeliegenden Daten am Ende in den HTML-Code statisch eingebunden werden (und somit nicht mehr aus der CSV-Datei geladen werden).


Abbildung 8: Best Practice bei der Formulierung von Prompts zur Entwicklung von HTML-Visualisierungen. 

Verglichen mit den weiter oben entwickelten, statischen Bilddateien handelt es sich für die KI bei der Entwicklung einer interaktiven Web-App um eine vergleichsweise komplexe Aufgabe. Der vorgestellte Beispiel-Prompt lieferte in unserem Versuch zwar schon im ersten Schritt eine lauffähige HTML-Datei, die jedoch anschließend weitaus kleinschrittiger im Dialog mit der KI angepasst werden musste, bevor das Ergebnis unseren Erwartungen entsprach. Wir empfehlen daher, für entsprechende Vorhaben mit den KI-Modellen der neuesten Generation zu arbeiten (im April 2025 sind dies z. B. ChatGPT o3, Gemini 2.5 Pro Experimental oder Claude 3.7 Sonnet), da diese die Anzahl der notwendigen Überarbeitungsschleifen deutlich reduzieren. Der ausgehend vom oben präsentierten Beispiel-Prompt schrittweise optimierte Code-Output kann im folgenden Aufklappelement eingesehen werden:

Python-Code mit Farbcodierung
import pandas as pd
import plotly.graph_objects as go

# 1) DataFrame auf gewünschte Gebietskörperschaften filtern
df_filtered = df_pivot[df_pivot['Gebietskoerperschaft'].isin([
    "Nordrhein-Westfalen",
    "Düsseldorf, Kreisfreie Stadt"
])]

# 2) Nur die benötigten Zeilen (Ganztagsform, Schulform) ausfiltern
df_filtered = df_filtered[
    (df_filtered['Ganztagsform'] == "Schüler/-innen im gebundenen Ganztagesbetrieb") &
    (df_filtered['Schulform'] == "Gesamtschulen")
].copy()

# 3) Neue Spalte "Summe" = Männlich + Weiblich
df_filtered['Summe'] = df_filtered['Männlich'] + df_filtered['Weiblich']

# 4) Sortierung nach Schuljahr
df_filtered = df_filtered.sort_values(by='Schuljahr')

# Farben wie im ursprünglichen Code
farbschema = {
    "Düsseldorf, Kreisfreie Stadt": "#2EA8D9",
    "Nordrhein-Westfalen": "#9AB688"
}

# Wir wollen zwischen zwei Traegerschaften "Öffentlich" und "Privat" umschalten.
traegerschaften_zum_umschalten = ["Öffentlich", "Privat"]

# Plotly-Figure anlegen
fig = go.Figure()

# Für jeden Wert in traegerschaften_zum_umschalten legen wir zwei Linien an:
# (1) Düsseldorf, (2) NRW.
for traeger in traegerschaften_zum_umschalten:
    # Teil-DataFrame
    df_t = df_filtered[df_filtered['Traegerschaft'] == traeger]

    # Düsseldorf
    df_dus = df_t[df_t['Gebietskoerperschaft'] == "Düsseldorf, Kreisfreie Stadt"]
    fig.add_trace(
        go.Scatter(
            x=df_dus['Schuljahr'],
            y=df_dus['Summe'],
            mode='lines+markers',
            line={color:farbschema["Düsseldorf, Kreisfreie Stadt"]},
            name=f'Düsseldorf, {traeger}',
            visible=(traeger == "Öffentlich")  # nur Öffentlich beim Start an
        )
    )

    # Nordrhein-Westfalen
    df_nrw = df_t[df_t['Gebietskoerperschaft'] == "Nordrhein-Westfalen"]
    fig.add_trace(
        go.Scatter(
            x=df_nrw['Schuljahr'],
            y=df_nrw['Summe'],
            mode='lines+markers',
            line={color:farbschema["Nordrhein-Westfalen"]},
            name=f'NRW, {traeger}',
            visible=(traeger == "Öffentlich")
        )
    )

# ---------------------------------------------------------
# Zwei dynamische Titelvarianten für die Buttons:
# Öffentlich vs. Privat
# ---------------------------------------------------------
titel_oeffentlich = (
    "Entwicklung der Schüler/-innenzahlen im gebundenen Ganztagsbetrieb an öffentlichen Gesamtschulen<br>"
    "in Düsseldorf, Kreisfreie Stadt und Nordrhein-Westfalen (Schuljahre 2005/06 - 2022/23)"
)
titel_privat = (
    "Entwicklung der Schüler/-innenzahlen im gebundenen Ganztagsbetrieb an privaten Gesamtschulen<br>"
    "in Düsseldorf, Kreisfreie Stadt und Nordrhein-Westfalen (Schuljahre 2005/06 - 2022/23)"
)

# ---------------------------------------------------------
# UpdateMenus: Buttons zum Umschalten
# ---------------------------------------------------------
updatemenus = [
    {
        type:"buttons",
        direction:"down",
        buttons:[
            {
                label:"Öffentlich",
                method:"update",
                args:[
                    {visible:[True, True, False, False]},   # Düsseldorf/NRW Öffentlich AN, Privat AUS
                    {
                        title:{
                            text:titel_oeffentlich,
                            x:0.5,
                            xanchor:"center"
                        }
                    }
                ]
            },
            {
                label:"Privat",
                method:"update",
                args:[
                    {visible:[False, False, True, True]},   # Düsseldorf/NRW Öffentlich AUS, Privat AN
                    {
                        title:{
                            text:titel_privat,
                            x:0.5,
                            xanchor:"center"
                        }
                    }
                ]
            }
        ],
        showactive:True,
        # Positionierung der Buttons knapp unterhalb der Legende
        x:1.02,
        y:0.9,
        xanchor:"left",
        yanchor:"top"
    }
]

# ---------------------------------------------------------
# Start-Layout: Weißer Hintergrund, Titel zentriert, Legende oben rechts
# ---------------------------------------------------------
fig.update_layout(
    template="plotly_white",   # sorgt für weißen Default-Hintergrund
    # damit wir sicher keinen andersfarbigen BG haben:
    paper_bgcolor='white',
    plot_bgcolor='white',

    # Initialer Titel (wenn Buttons noch nicht geklickt wurden)
    title={
        text:titel_oeffentlich,
        x:0.5,
        xanchor:'center'
    },

    xaxis_title="Schuljahr",
    yaxis_title="Anzahl Schüler*innen",

    legend={
        x:1.02,
        y:1,
        xanchor:"left",
        yanchor:"top"
    },

    updatemenus=updatemenus,

    margin={
        t:80,   # oben Platz für Titel
        r:150,  # rechts Platz für Legende & Buttons
        b:80    # unten genug Platz für Annotation
    }
)

# ---------------------------------------------------------
# Quellenangabe als Annotation am unteren Rand
# ---------------------------------------------------------
fig.add_annotation(
    x=0.5,
    y=-0.15,  # so, dass sie ungefähr unterhalb des Diagramms erscheint
    showarrow=False,
    text=(
        "Quelle: Statistische Ämter des Bundes und der Länder, 2024: Kommunale Bildungsdatenbank, "
        "online abrufbar unter https://www.bildungsmonitoring.de/"
    ),
    xref="paper",
    yref="paper",
    xanchor="center",
    yanchor="top",
    font={size:10}
)

# ---------------------------------------------------------
# Ausgabe in eine HTML-Datei (inkl. Plotly-JS)
# ---------------------------------------------------------
fig.write_html(
    "mein_interaktives_diagramm.html",
    include_plotlyjs='cdn',  # Lädt plotly.js aus dem CDN
    full_html=True
)

print("Interaktives Diagramm wurde als 'mein_interaktives_diagramm.html' gespeichert.")

Kopieren wir diesen Code anschließend in unsere Python-Umgebung, um ihn auszuführen, erhalten wir im Ergebnis die exakt nach unseren Vorgaben erstellte Web-App als HTML-Datei und können sie per iFrame oder HTML-Code in unsere Webseite integrieren: 

Abbildung 9: Auf Grundlage des vorgestellten Beispielprompts generiertes interaktives HTML-Diagramm

Anwendungsfall 5: Einen kompakten Datenbericht im PDF-Format erstellen

Nachdem wir nun neben der statischen Bilddatei außerdem eine interaktive Web-App zur Visualisierung unserer Daten erstellt haben, wollen wir uns abschließend einem letzten, in der Praxis des Bildungsmonitorings denkbaren Anwendungsfall widmen: Der automatisierten Erstellung eines kompakten Berichts im PDF-Format, der mehrere Analysen und Visualisierungen übersichtlich zusammenfasst. Ein solcher Bericht könnte beispielsweise als Handout für die Sitzungen einer sich regelmäßig treffenden Arbeitsgruppe dienen oder als Download auf einer Informations-Website zum Ganztagsangebot der Kommune angeboten werden.

Um den Bericht zu erstellen, bauen wir auf dem Skript auf, welches wir in Schritt 1 zur Aufbereitung unserer Datentabelle entwickelt hatten, und erweitern es nun so, dass für eine auszuwählende Gebietskörperschaft automatisiert mehrere Liniendiagramme zur Entwicklung der Ganztagsbildung erstellt werden. Jedes dieser Diagramme widmet sich einer bestimmten Schulform und stellt die zeitliche Entwicklung der Anzahl der Schüler*innen im gebundenen Ganztag, differenziert nach öffentlicher und privater Trägerschaft, dar. Zusätzlich wird jedes Diagramm mit einem dynamisch generierten Text versehen, der beschreibt, ob die Gesamtzahl der Schüler*innen im gebundenen Ganztag in der jeweiligen Schulform im betrachteten Zeitraum gestiegen, gesunken oder gleichgeblieben ist. Das Besondere dabei: Jedes dieser automatisch generierten Diagramme (inkl. der Erläuterungstexte) wird zum Schluss als eigene Seite in einem mehrseitigen PDF-Bericht platziert. Wir erstellen also mit einem einzigen Skript-Durchlauf gleich mehrere aussagekräftige Analysen zur Entwicklung der Ganztagsbildung in unterschiedlichen Schulformen und fassen diese in einem handlichen, ansprechend gestalteten Bericht zusammen. 

Das folgende Schaubild zeigt, welche Best Practices bei der Formulierung entsprechender Prompts für KI-Tools berücksichtigt werden sollten. Dabei gehen wir davon aus, dass – wie in unserem Fall – zuvor bereits der Code zur Aufbereitung unserer Rohdaten erzeugt wurde und dieser nun erweitert werden soll, um die Analysen für unseren PDF-Bericht zu erstellen. Der Beispiel-Prompt illustriert das Vorgehen und setzt die Konversation mit unserem KI-Assistenten an dem Punkt fort, wo der Beispiel-Prompt in Abb. 2 (zur Aufbereitung der Rohdaten) aufgehört hatte.

Best Practice

Struktur klar definieren

Legen Sie fest, welche Inhalte der Bericht enthalten soll, welche Elemente auf jeder Seite erscheinen (z. B. Diagramm, Erläuterungstext) und in welcher Reihenfolge die Inhalte dargestellt werden sollen.

Per „Schleifen“ mehrere Diagramme in einem Durchgang generieren

Nutzen Sie eine Schleifenlogik, um für jeden Wert in einer Spalte (z. B. jede Schulform) automatisch ein eigenes Diagramm zu erstellen. Mit einer klar formulierten Anweisung wie „Iteriere über alle eindeutigen Werte in der Spalte ‚Schulform‘“ geben Sie der KI die notwendige Logik vor, um diese Wiederholungen umzusetzen.

Gestaltung des Berichts vorgeben

Eine klare Beschreibung der Layout-Vorgaben hilft, Probleme wie überlappende Texte oder unleserliche Inhalte zu vermeiden. Weisen Sie im Prompt darauf hin, dass ausreichend Platz für Diagramme, Texte und Quellenangaben vorhanden sein soll, ohne dass Inhalte über den Seitenrand hinausragen. Erwägen Sie außerdem, Mindestabstände zwischen Elementen, Schriftgrößen und Farbschemata explizit vorzugeben.

Beispiel-Prompt

Ich habe ein Python-Dataframe namens "df_pivot" mit den Spalten "Schuljahr", "Gebietskoerperschaft", "Ganztag", "Schulform", "Traegerschaft", “Männlich” und "Weiblich". Die Spalten "Männlich" und "Weiblich" enthalten Zahlen, die übrigen Spalten differenzierende Merkmale in Textform. Filtere das Dataframe in der Spalte "Gebietskoerperschaft" auf den Wert "Düsseldorf, Kreisfreie Stadt" und in der Spalte "Ganztag" auf den Wert "Schüler/-innen im gebundenen Ganztagesbetrieb".

Iteriere dann über die Spalte „Schulform“ und erstelle ein Liniendiagramm für jeden distinkten Wert in dieser Spalte. Jedes dieser Diagramme trägt auf der x-Achse die Werte aus der Spalte "Schuljahr" ab und auf der y-Achse die Summen der Werte in den Spalten "Männlich" und "Weiblich" anhand von zwei Linien: Eine für jeden der zwei distinkten Werte in der Spalte "Traegerschaft" (nämlich "Öffentlich" und Privat").

Unterhalb jedes dieser einzelnen Diagramme sollte ein kurzer Erläuterungstext stehen, der die Entwicklung der Zahlen im Diagramm erläutert. Achte auf ausreichend viel Platz zwischen dem Text und dem Diagramm, sodass sich der erläuternde Text und die Achsenbeschriftung des Diagramms keinesfalls überlagern! Der Text sollte benennen, ob die Entwicklung steigend oder sinkend war. Wichtig dabei: Es soll ein "dynamischer" Text werden, der auch funktioniert, wenn ich statt "Düsseldorf, Kreisfreie Stadt" eine andere Gebietskörperschaft betrachte. Also am besten sollten die entsprechenden Werte aus der gefilterten Tabelle entnommen werden und nicht statisch definiert sein.

Für die Gestaltung der Diagramme sind mir außerdem folgende Aspekte wichtig:

  • Jedes Diagramm sollte eine Überschrift nach dem folgenden Schema haben: Anzahl der Schüler*innen im gebundenen Ganztag an [Schulform] in [Gebietskoerperschaft] nach Trägerschaft (Schuljahr [auf der x-Achse abgetragener Zeitraum])
  • Eine Bildunterschrift, die auf folgende Quelle verweist: Statistische Ämter des Bundes und der Länder, 2024: Kommunale Bildungsdatenbank, online abrufbar unter www.bildungsmonitoring.de"
  • Verwendung eines Farbschemas, welches folgende Farben verwendet: #9AB688, #2EA8D9, #FAC240
  • Diagramme sollen auf der y-Achse beim Nullpunkt beginnen, d.h. nicht skalieren oder "reinzoomen"
  • Die Beschriftung der x-Achse mit den Jahreszahlen bitte um 45 Grad anwinkeln, damit die Beschriftungen sich nicht überlagern und gut lesbar bleiben.

Die generierten Diagramme sollen in Form eines PDF-Berichts im Hochformat ausgegeben werden, wobei auf jeder Seite ein Diagramm platziert werden soll. Achte hier erneut darauf, dass die Diagramme und die darunter platzierten Erläuterungstexte sowie die Bildunterschrift mit der Quellenangabe sich keinesfalls überlagern und ausreichend Platz zwischen Diagramm und Texten bestehen bleibt. Achte außerdem darauf, dass die Textelemente unterhalb der Diagramme keinesfalls über den Seitenrand des PDF hinausgehen, sondern durch Zeilenumbrüche gewährleistet ist, dass der Text vollständig auf der jeweiligen PDF-Seite zu lesen ist.


Abbildung 10: Best Practice bei der Formulierung von Prompts zur automatisierten Erstellung von PDF-Berichten. 

Ähnlich wie bei der Entwicklung einer interaktiven Web-App im vorangegangenen Beispiel handelt es sich bei der Erstellung eines mehrseitigen PDF-Berichts für die KI um eine vergleichsweise komplexe Aufgabe. Auch in diesem Kontext raten wir daher zur Nutzung von KI-Modellen der neuesten Generation (im April 2025 sind dies z. B. ChatGPT o3, Gemini 2.5 Pro Experimental oder Claude 3.7 Sonnet), die den entsprechenden Python-Code weitaus schneller und zuverlässiger generieren. Ausgehend von dem oben vorgestellten Beispiel-Prompt und mit Unterstützung von ChatGPT o1 waren in unserem Versuch sieben Überarbeitungsschleifen notwendig, bis ein den Anforderungen genügender Python-Code generiert war. Er kann im folgenden Aufklappelement eingesehen werden:

Python-Code mit Farbcodierung
import textwrap
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages

###############################################################################
# 1) Hilfsfunktionen
###############################################################################
def parse_schuljahr(s):
    try:
        parts = s.split("/")
        first_year = int(parts[0])
        second_year = int(parts[1])
        return first_year * 100 + second_year
    except:
        return None

def entwicklung_text(df_sub, region, schulform, traegerschaft):
    if df_sub.empty:
        return f"Keine Daten für Trägerschaft '{traegerschaft}' verfügbar."
    df_sub['sort_key'] = df_sub['Schuljahr'].apply(parse_schuljahr)
    df_sub = df_sub.dropna(subset=['sort_key']).sort_values('sort_key')
    if df_sub.empty:
        return f"Keine Daten für Trägerschaft '{traegerschaft}' verfügbar."

    first_year = df_sub["Schuljahr"].iloc[0]
    last_year  = df_sub["Schuljahr"].iloc[-1]
    first_val  = df_sub["Summe"].iloc[0]
    last_val   = df_sub["Summe"].iloc[-1]
    diff = last_val - first_val
    if diff > 0:
        trend_phrase = "gestiegen"
    elif diff < 0:
        trend_phrase = "gesunken"
    else:
        trend_phrase = "konstant geblieben"
    if first_val == 0:
        diff_pct_str = "(keine prozentuale Angabe möglich, da Startwert=0)"
    else:
        diff_pct = diff / first_val * 100
        diff_pct_str = f"{diff_pct:+.1f}%"
    diff_abs_str = f"{diff:+.0f}"

    return (
        f"Für die Trägerschaft '{traegerschaft}' ist die Zahl der Schüler/-innen "
        f"im gebundenen Ganztag an {schulform} von {int(first_val)} im Schuljahr {first_year} "
        f"auf {int(last_val)} im Schuljahr {last_year} {trend_phrase} "
        f"(Diff.: {diff_abs_str} bzw. {diff_pct_str})."
    )

###############################################################################
# 2) Ausgangsdaten (DataFrame df_pivot)
###############################################################################
# Beispielhaft: df_pivot existiert bereits
df_filtered = df_pivot[
    (df_pivot["Gebietskoerperschaft"] == "Düsseldorf, Kreisfreie Stadt") &
    (df_pivot["Ganztagsform"] == "Schüler/-innen im gebundenen Ganztagesbetrieb")
].copy()

df_filtered["Summe"] = df_filtered["Männlich"] + df_filtered["Weiblich"]

###############################################################################
# 3) PDF-Erstellung
###############################################################################
pdf_filename = "auswertung_gebundener_ganztag_duesseldorf.pdf"
pp = PdfPages(pdf_filename)

colors_for_traeger = {
    "Öffentlich": "#9AB688",
    "Privat": "#2EA8D9"
}

alle_schulformen = df_filtered["Schulform"].unique()

for schulform in alle_schulformen:
    df_schulform = df_filtered[df_filtered["Schulform"] == schulform].copy()
    if df_schulform.empty:
        continue

    df_schulform["sort_key"] = df_schulform["Schuljahr"].apply(parse_schuljahr)
    df_schulform = df_schulform.dropna(subset=["sort_key"]).sort_values("sort_key")
    region = df_schulform["Gebietskoerperschaft"].iloc[0]
    min_schuljahr = df_schulform["Schuljahr"].iloc[0]
    max_schuljahr = df_schulform["Schuljahr"].iloc[-1]

    # Figure erzeugen (A4 hoch)
    fig = plt.figure(figsize=(8.27, 11.69))

    # Haupt-Achse für das Diagramm
    ax = fig.add_subplot(111)

    # Titel
    ax.set_title(
        f"Anzahl der Schüler*innen im gebundenen Ganztag an {schulform}\n"
        f"in {region} nach Trägerschaft (Schuljahr {min_schuljahr} bis {max_schuljahr})",
        fontsize=11, pad=20
    )

    # Liniendiagramm
    for traeger in ["Öffentlich", "Privat"]:
        df_traeger = df_schulform[df_schulform["Traegerschaft"] == traeger]
        if df_traeger.empty:
            continue
        x = df_traeger["sort_key"]
        y = df_traeger["Summe"]
        color = colors_for_traeger.get(traeger, "#FAC240")
        ax.plot(x, y, label=traeger, marker="o", color=color)

    ax.set_xlabel("Schuljahr")
    ax.set_ylabel("Anzahl Schüler*innen")
    ax.set_ylim(0)

    # X-Ticks
    unique_pairs = df_schulform[["sort_key", "Schuljahr"]].drop_duplicates()
    unique_pairs = unique_pairs.sort_values("sort_key")
    ax.set_xticks(unique_pairs["sort_key"])
    ax.set_xticklabels(unique_pairs["Schuljahr"], rotation=45)
    ax.legend(loc="upper left")

    # Entwicklungstexte zusammensetzen
    text_liste = []
    for traeger in ["Öffentlich", "Privat"]:
        df_traeger = df_schulform[df_schulform["Traegerschaft"] == traeger]
        text_liste.append(entwicklung_text(df_traeger, region, schulform, traeger))
    erlaeuterung_text = "\n".join(text_liste)

    quellen_text = (
        "Quelle: Statistische Ämter des Bundes und der Länder, 2024: Kommunale Bildungsdatenbank, "
        "online abrufbar unter https://www.bildungsmonitoring.de/"
    )

    # --------------------------------------------------
    # 3.7) Layout: Diagramm oben, separater Bereich unten
    # --------------------------------------------------
    # Reserviere den oberen Teil für das Diagramm:
    fig.subplots_adjust(left=0.10, right=0.90, bottom=0.40, top=0.9)

    # Eine zweite Achse am unteren Rand, unsichtbar:
    # left=0.10, width=0.80 => gleicher linker+rechter Rand wie Diagramm
    text_ax = fig.add_axes([0.10, 0.00, 0.80, 0.28])
    text_ax.set_axis_off()  # Keine sichtbare Achse

    # --------------------------------------------------
    # 3.8) Manueller Zeilenumbruch mittels textwrap
    # --------------------------------------------------
    # z.B. Zeilenbreite 70 Zeichen (kann man anpassen)
    wrapped_erlaeuterung = "\n".join(textwrap.wrap(erlaeuterung_text, width=90))
    wrapped_quellen      = "\n".join(textwrap.wrap(quellen_text,      width=90))# Wir platzieren erst den Erläuterungstext am oberen Rand dieser Achse
    text_ax.text(
        0,          # x=0 => linker Rand
        1.0,        # y=1.0 => oberer Rand in Achsen-Koord.
        wrapped_erlaeuterung,
        transform=text_ax.transAxes,
        va="top", ha="left",
        fontsize=10
    )

    # Quellenangabe etwas tiefer:
    text_ax.text(
        0,
        0.5,  # z.B. 0.5 => in der Mitte
        wrapped_quellen,
        transform=text_ax.transAxes,
        va="top", ha="left",
        fontsize=9
    )

    # --------------------------------------------------
    # 3.9) Speichern
    # --------------------------------------------------
    pp.savefig(fig)
    plt.close(fig)

pp.close()
print(f"Fertig! PDF gespeichert als: '{pdf_filename}'")

Kopieren wir diesen Code anschließend in unsere Python-Umgebung, um ihn auf unsere Tabelle auszuführen, erhalten wir im Ergebnis den exakt nach unseren Vorgaben erstellten Bericht im PDF-Format: 

Und genau wie oben im Kontext der Diagrammdarstellung gilt auch hier: Wenn anschließend z. B. ein PDF-Bericht für eine andere Kommune erstellt werden soll, genügt es, im Code einfach den Namen „Düsseldorf, Kreisfreie Stadt“ durch den Namen der gewünschten Kommune zu ersetzen. Das Skript erzeugt dann automatisch einen angepassten Bericht (einschließlich aller Diagramme und Erläuterungstexte) für diese Kommune, sofern die entsprechenden Daten in der Tabelle vorhanden sind.

Mit der richtigen Entwicklungsumgebung in die KI-unterstützte Generierung von Python-Code einsteigen

Nachdem Sie mithilfe eines KI-Tools Ihren ersten Python-Code generiert haben, stellt sich die Frage: Wie und womit kann dieser Code nun ausgeführt werden? Hier kommen Anwendungen wie Google Colab und Jupyter Notebook ins Spiel.

Warum sind solche Anwendungen notwendig?

Als Programmiersprache muss Python in einer dafür vorgesehenen Benutzeroberfläche („Entwicklungsumgebung“) ausgeführt werden, welche die Befehle des Anwenders interpretiert und ausführt. Anwendungen wie Jupyter Notebook und Google Colab bieten solche Benutzeroberflächen und sind besonders für Datenanalysen geeignet, da sie die Arbeit mit interaktiven Code-Blöcken ermöglichen. Diese können einzeln nacheinander ausgeführt werden und zeigen dem Anwender unmittelbar das Ergebnis des ausgeführten Codes (also eine Tabelle, ein Diagramm etc.) an.

Was leisten diese Anwendungen?

Interaktive Umgebung

Sie können Code in kleinen Blöcken schreiben und ausführen, was das schrittweise Entwickeln und Testen erleichtert.

Dokumentation und Visualisierung

Sie können Ihren Code, Ihre Erläuterungen und die generierten Ergebnisse in einem einzigen Dokument („Notebook“) kombinieren, was die Dokumentation und Präsentation Ihrer Arbeit vereinfacht.

Wiederholbarkeit

Sie können Ihren gesamten, im Notebook gespeicherten Workflow zu einem späteren Zeitpunkt erneut ausführen, um konsistente Ergebnisse zu erzielen und repetitive Arbeitsschritte zu automatisieren.

Sowohl Jupyter Notebook als auch Google Colab sind für Einsteiger*innen gut geeignet, unterscheiden sich jedoch in einigen wichtigen Punkten. Während Google Colab eine besonders einfache und niedrigschwellige Lösung darstellt, da es ohne Installation direkt über den Browser genutzt werden kann, punktet Jupyter Notebook mit mehr Kontrolle und Datenschutz, da es lokal auf dem eigenen Rechner läuft. Diese Abwägung zwischen Nutzerfreundlichkeit und Datenschutz ist gerade bei datenbezogenen Aufgaben im Bildungsmonitoring relevant. Im Folgenden skizzieren wir die wesentlichen Unterschiede, damit Sie eine informierte Entscheidung treffen können.

Jupyter Notebook: Die lokale Lösung für mehr Kontrolle und Datenschutz

Für die Entwicklung von Python-Code im Kontext der Datenaufbereitung und -analyse hat sich die Open-Source-Anwendung Jupyter Notebook als Standard etabliert. Sie bietet:

  • Lokale Verarbeitung: Die Anwendung wird auf Ihrem eigenen Computer installiert, sodass keine Daten in eine externe Cloud geladen werden.
  • Hohe Flexibilität: Sie haben volle Kontrolle über Ihre Python-Umgebung und können beliebige Bibliotheken individuell installieren.
  • Datenschutzfreundlich: Da keine Daten über das Internet an externe Server gesendet werden, eignet sich Jupyter Notebook besonders für sensible oder personenbezogene Daten.

Das Problem dabei: Die Einrichtung von Jupyter Notebook kann für Einsteiger*innen technisch anspruchsvoll sein. Insbesondere die Erstinstallation des Tools sowie die Installation von ergänzenden Bibliotheken erfordern ein gewisses Maß an technischem Verständnis. Wer sich dies grundsätzlich zutraut (und vielleicht die Unterstützung einer IT-Abteilung in Anspruch nehmen kann), findet im Internet jedoch zahlreiche Anleitungen und Tutorials, die den Prozess erläutern.

Google Colab: Die einfache Cloud-Lösung für einen schnellen Einstieg

Google Colab hingegen ist eine cloudbasierte Version von Jupyter Notebook, die viele dieser Hürden beseitigt:

  • Keine Installation notwendig: Sie müssen keine Software installieren. Alles, was Sie brauchen, ist ein Google-Konto und ein Webbrowser.
  • Sofortiger Start: Google Colab enthält viele der gängigen Python-Bibliotheken wie pandas, numpy und matplotlib, die Sie sofort nutzen können.
  • Cloud-Rechenleistung: Sie nutzen die Rechenleistung der Google-Server, was besonders bei großen Datensätzen und komplexen Berechnungen von Vorteil ist.
  • Einfaches Teilen und Zusammenarbeiten: Sie können Ihre Notebooks einfach mit anderen teilen und gemeinsam daran arbeiten, ähnlich wie bei Google Docs.

Aus Sicht von Python-Einsteiger*innen liegen die Stärken von Google Colab somit eindeutig in seiner Niedrigschwelligkeit: Wer über ein Google-Konto verfügt, kann sich innerhalb von Minuten und ohne jeden Installationsaufwand eine lauffähige Python-Umgebung anlegen. Dabei ist jedoch unbedingt zu berücksichtigen: Da Google Colab eine cloudbasierte Lösung ist, werden Ihre Daten auf den Servern von Google gespeichert. Daher sollten hier keine personenbezogenen oder anderweitig hochsensiblen Daten verarbeitet werden. Falls also der Datenschutz eine zentrale Rolle spielt, ist die lokale Nutzung von Jupyter Notebook die bessere Wahl.


Martin Franger

Kommunikation & Öffentlichkeitsarbeit

KOSMO-Newsletter

Mit allen Infos rund um das kommunale Bildungsmonitoring!

Kontakt

Standort Potsdam

kobra.net, Kooperation in Brandenburg, gemeinnützige GmbH
Benzstr. 8/9, 14482 Potsdam

Ansprechpartner:
Tim Siepke, Leitung

0331 / 2378 5331
info@kommunales-bildungsmonitoring.de

Standort Trier

Kommunales Bildungsmanagement Rheinland-Pfalz - Saarland e.V.
Domfreihof 1a | 54290 Trier

Ansprechpartner:
Dr. Tobias Vetterle, Leitung

0651 / 4627 8443
info@kommunales-bildungsmonitoring.de

Dieses Vorhaben wird aus Mitteln des Bundesministeriums für Bildung und Forschung gefördert.