3.4. 🚀 Auswahl des Forschungskorpus#
Diese Fallstudie untersucht, wie deutschsprachige literarische Texte des 19. Jahrhunderts die abnehmende Luftqualität reflektieren und diskursivieren.
Ein zentraler Schritt besteht darin, ein geeignetes Forschungskorpus auszuwählen, das den historischen Zeitraum und die thematische Breite unserer Forschungsfrage abdeckt.
Hinweise zur Ausführung des Notebooks
Dieses Notebook kann auf unterschiedlichen Levels erarbeitet werden (siehe Abschnitt Technische Voraussetzungen):
Book-Only Mode: Sie lesen das Notebook hier im “Jupyter Book”, ohne den Code selbst auszuführen.
Cloud Mode: Klicken Sie oben rechts in der Menüleiste auf das Raketen-Symbol 🚀 und wählen Sie “Colab”, um das Notebook auszuführen.
Local Mode: Klicken Sie oben rechts in der Menüleiste auf das Download-Symbol ↓ und wählen Sie “.ipynb”, um das Notebook lokal auszuführen.
3.4.1. Übersicht#
Im Folgenden wird die Auswahl eines geeigneten Forschungskorpus auf Basis von Metadaten vorgenommen. Ziel dieses Schrittes ist es, vor der eigentlichen Textanalyse zu prüfen, welche Korpora für die vorliegende Forschungsfrage geeignet sind und welche Einschränkungen sie mit sich bringen.
Im Fokus stehen dabei zeitliche Abdeckung, Verteilung der Texte und strukturelle Eigenschaften verschiedener Korpora deutschsprachiger Prosa. Die Analyse dient nicht der inhaltlichen Interpretation einzelner Texte, sondern der methodisch reflektierten Korpusentscheidung als Grundlage für die späteren Analyseschritte.
Dazu werden die folgenden Schritte durchgeführt:
Vorstellung mehrerer verfügbarer Korpora
Einlesen und Aufbereitung der zugehörigen Metadaten
Explorative Analyse der zeitlichen Verteilung der Texte
Vergleich der Korpora hinsichtlich Abdeckung und Balance
Begründete Auswahl eines Korpus für die weitere Analyse
3.4.2. Vom Aufbau zur Auswahl#
Da bereits digitale Korpora deutschsprachiger Prosa vorliegen, stehen wir nicht vor der Aufgabe, Texte selbst zu digitalisieren, sondern müssen reflektiert entscheiden, welches existierende Korpus für unsere Forschungsfrage geeignet ist. Die im Kapitel Korpora als Forschungsobjekte beschriebenen Strategien – Vollständigkeit, Repräsentativität, Balance und Opportunismus – bilden dabei unseren Bewertungsrahmen (Schöch, 2017).
Was wenn noch kein geeignetes Korpus digital vorliegt?
Wenn Ihre Forschungsobjekte noch nicht als Text sondern als Bilddigitalisate, muss der Text z.B. mittels Optical Character Recognition (OCR) extrahiert werden. Wie das geht zeigen wir an Hand von Zeitungsdigitalisaten in der Quadriga-Fallstudie ”Quantitative Analyse der Medienwellen der Spanischen Grippe (1918/19)”
3.4.3. Vorhandene Korpora deutschsprachiger Prosa#
Wir stellen drei frei verfügbare Korpora vor, die sich für literaturwissenschaftliche Analysen deutscher Prosa eignen. Konkret werden das d-Prose-Korpus, das Corpus of German-Language Fiction sowie das German ELTeC-Korpus herangezogen.
3.4.4. Explorative Analyse der Metadaten#
Um die Eignung der Korpora genauer zu prüfen, untersuchen wir zunächst ihre Metadaten. Ziel ist es, ein erstes Gefühl für die zeitliche Verteilung, Vollständigkeit und Struktur der Daten zu gewinnen. Um das zu erreichen, müssen wir mithilfe von Code einige Metadaten von Korpora analysieren.
Warum ist dafür überhaupt Code nötig? Die Metadaten liegen als umfangreiche Tabellen mit teils mehreren Tausend Einträgen vor – das Corpus of German-Language Fiction etwa umfasst über 2.700 Texte. Eine solche Datenmenge lässt sich nicht mehr sinnvoll “von Hand” überblicken. Fragen wie Wie viele Texte entfallen auf jedes Jahrzehnt?, Wie gleichmäßig sind sie über die Zeit verteilt? oder Welchen Zeitraum deckt ein Korpus überhaupt ab? lassen sich erst durch systematisches Auszählen, Aggregieren und Visualisieren beantworten. Genau das übernimmt der Code: Er wertet die Tabellen schnell und fehlerarm aus und stellt das Ergebnis als Diagramm dar. Hinzu kommt ein methodischer Vorteil: Wir wenden auf alle drei Korpora exakt dieselben, nachvollziehbaren und reproduzierbaren Schritte an und erhalten so eine faire, vergleichbare Grundlage für unsere Auswahlentscheidung.
Dieser Code wird unten als ausführbares Notebook präsentiert.
Pakete und Bibliotheken laden#
Zunächst importieren wir die benötigten Python-Bibliotheken: pandas für die Arbeit mit den Metadatentabellen, plotly für die Diagramme und itables für interaktive Tabellenansichten. Außerdem stellen wir die Diagramm-Darstellung passend für die jeweilige Umgebung (lokales Jupyter oder Google Colab) ein.
# install libraries firsrt (if necessary)
! pip install pandas plotly itables nbformat requests
import sys
import requests
from pathlib import Path
import pandas as pd
import plotly.express as px
import plotly.io as pio
from itables import show
# choose the right Plotly renderer depending on the environment (Colab vs. local Jupyter)
if 'google.colab' in sys.modules:
pio.renderers.default = "colab"
else:
pio.renderers.default = "notebook"
Bevor wir Daten herunterladen, definieren wir eine kleine Hilfsfunktion download_file. Sie lädt eine Datei plattformunabhängig – also z.B. unter Windows, macOS oder Linux – aus dem Internet in einen Zielordner herunter und ersetzt damit das Kommando wget, das nicht auf allen Systemen verfügbar ist.
# helper: download a single file (cross-platform replacement for `! wget -P`)
def download_file(url, target_dir):
"""Download the file at `url` into `target_dir`, keeping its original name."""
target_dir = Path(target_dir)
target_dir.mkdir(parents=True, exist_ok=True)
target_path = target_dir / url.split("/")[-1]
response = requests.get(url)
response.raise_for_status()
target_path.write_bytes(response.content)
return target_path
Die if-Abfrage am Ende prüft, ob das Notebook in Google Colab ausgeführt wird, und wählt das dazu passende Plotly-Renderer-Backend, damit die Diagramme sowohl in Colab als auch im lokalen Jupyter korrekt angezeigt werden.
Hilfsfunktionen für die Analyse#
Wir untersuchen gleich drei Korpora nach demselben Schema: Für jedes zählen wir, wie viele Texte pro Jahr bzw. pro Jahrzehnt vorliegen, und stellen das Ergebnis als Balkendiagramm dar. Damit wir diesen Code nicht für jedes Korpus erneut schreiben müssen, definieren wir ihn einmal als zwei kleine Hilfsfunktionen:
summarize_counts(df, column, step)zählt die Texte je Zeiteinheit (Jahr oder Jahrzehnt) und berechnet daraus einige Kennzahlen. Die Funktion gibt zwei Dinge zurück: zum einen die Anzahl der Texte pro Zeiteinheit (counts, die Datengrundlage für das Diagramm), zum anderen eine kompakte Übersicht (stats) mit dem Durchschnitt (avg), dem Maximum (max) und dem Minimum (min) der Textanzahlen. So sehen wir auf einen Blick, wie umfangreich ein Korpus ist und wie gleichmäßig es über die Zeit verteilt ist.plot_counts(counts, ...)zeichnet aus diesen Anzahlen ein Balkendiagramm: Jeder Balken steht für eine Zeiteinheit auf der x-Achse (ein Jahr bzw. ein Jahrzehnt), und seine Höhe gibt an, wie viele Texte des Korpus aus dieser Zeiteinheit stammen (y-Achse). So wird sichtbar, in welchen Zeiträumen das Korpus viele und in welchen es wenige oder gar keine Texte enthält. Über die Argumentetitle,x_labelunddticklassen sich Titel, Achsenbeschriftung und der Abstand der Achsenmarkierungen bei Bedarf an eigene Daten anpassen.
Beide Funktionen sind bewusst einfach gehalten und werden im Folgenden für jedes der drei Korpora wiederverwendet – die späteren Code-Zellen sind also im Kern nur Varianten dieser beiden Aufrufe.
# Helper 1: count texts per time unit (year or decade) and compute summary stats
def summarize_counts(df, column, step=None):
"""
Count how many texts fall into each time unit (year or decade) and compute
a few summary statistics. Prepares the data for the bar chart.
Parameters:
df : table with the corpus metadata
column : column holding the year / decade value
step : optional; if set (e.g. 1 for years), gaps in the covered range are
filled with 0 so the average reflects the whole period, including
years with no texts.
Returns:
counts : Series -- index = time unit, value = number of texts
stats : Series -- avg = mean number of texts, max = highest, min = lowest
"""
values = df[column].dropna().astype(int) # drop missing values, cast to int
counts = values.value_counts().sort_index() # count per time unit, sort chronologically
if step: # fill gaps in the range with 0
counts = counts.reindex(
range(counts.index.min(), counts.index.max() + 1, step), fill_value=0
)
stats = counts.agg(['mean', 'max', 'min']).rename({'mean': 'avg'})
return counts, stats
# Helper 2: bar chart of the text counts per time unit
def plot_counts(counts, title, x_label="Jahr", dtick=10):
"""
Build a bar chart from the counts computed by summarize_counts and return the
figure, which the notebook renders as the cell's output.
Parameters:
counts : Series from summarize_counts (index = time unit, value = count)
title : chart title
x_label : x-axis label ("Jahr" or "Jahrzehnt")
dtick : spacing of axis ticks (e.g. 10 = every 10 years, 50 = every 50)
"""
fig = px.bar(
x=counts.index,
y=counts.values,
labels={"x": x_label, "y": "Anzahl Texte"},
title=title,
)
fig.update_layout(
height=350,
margin=dict(l=40, r=40, t=60, b=40),
xaxis=dict(tickmode="linear", tickformat="d", dtick=dtick),
)
return fig
3.4.5. Option 1. ELTeC-DEU corpus#
Einlesen der Korpusmetadaten in Python#
Die Metadaten des ELTeC-DEU-Korpus liegen als CSV-Datei vor, die auf Zenodo veröffentlicht ist. Wir lesen sie mit pandas direkt ein – das Argument, das wir pd.read_csv() übergeben, ist dabei schlicht die URL dieser Datei. Schlägt der Zugriff auf die Online-Version fehl (etwa ohne Internetverbindung), greift der Code auf eine lokal gespeicherte Kopie zurück.
# ELTeC-DEU metadata are published as a CSV on Zenodo
eltec_published_url = "https://zenodo.org/records/4662482/files/metadata.csv"
eltec_local_copy = Path("../metadata/metadata_eltec_deu.csv")
# load from the online URL; fall back to the local copy if it is not reachable
try:
meta = pd.read_csv(eltec_published_url)
except Exception as e:
print(f"Konnte Metadaten nicht von Zenodo laden. Nutze lokale Kopie: {eltec_local_copy}. Das Problem war: {e}")
meta = pd.read_csv(eltec_local_copy)
show(meta) # interactive preview of the table
| Loading ITables v2.6.1 from the internet... (need help?) |
Daten aus unterschiedlichen Quellen einlesen
pd.read_csv() ist nicht auf diese eine Zenodo-URL festgelegt. Als Quelle lässt sich ebenso gut eine lokale Datei (wie hier die Fallback-Kopie), eine Datei in einem anderen Repositorium oder eine beliebige andere URL angeben. Für eigene Projekte ersetzen Sie also einfach die URL in der Variablen eltec_published_url durch den Pfad oder Link zu Ihrer eigenen Metadatendatei.
Genau das geschieht weiter unten ebenfalls: Das d-Prose-Korpus (Option 2) wird von einer anderen Zenodo-URL geladen, und die Metadaten des Corpus of German-Language Fiction (Option 3) werden zunächst von GitHub heruntergeladen und erst dann eingelesen.
Analyse der zeitlichen Verteilung des Korpus#
Pro Jahr#
Mit der Hilfsfunktion summarize_counts zählen wir, wie viele Texte des ELTeC-DEU auf jedes einzelne Jahr entfallen, und lassen uns Durchschnitt, Maximum und Minimum ausgeben.
# count texts per year (step=1 also counts years without texts as 0)
year_counts, year_stats = summarize_counts(meta, 'year', step=1)
print("Textanzahl der Texte im ELTEC-DEU pro Jahr:")
print(year_stats)
Textanzahl der Texte im ELTEC-DEU pro Jahr:
avg 1.234568
max 5.000000
min 0.000000
Name: count, dtype: float64
Visualisierung der Verteilung pro Jahr. Jeder Balken steht für ein Jahr auf der x-Achse
plot_counts(year_counts,
title="Zeitliche Verteilung der Texte im ELTEC-DEU pro Jahr",
x_label="Jahr", dtick=10)
Pro Jahrzehnt#
Dieselbe Auszählung wiederholen wir auf der Ebene der Jahrzehnte. Dazu bilden wir aus dem Jahr zunächst das zugehörige Jahrzehnt (z.B. wird aus 1857 das Jahrzehnt 1850) und zählen dann die Texte pro Jahrzehnt.
# derive the decade from the year (e.g. 1857 -> 1850), then count texts per decade
meta['decade'] = (meta['year'] // 10) * 10
decade_counts, decade_stats = summarize_counts(meta, 'decade')
print("Textanzahl der Texte im ELTEC-DEU pro Jahrzehnt:")
print(decade_stats)
Textanzahl der Texte im ELTEC-DEU pro Jahrzehnt:
avg 11.111111
max 16.000000
min 1.000000
Name: count, dtype: float64
Visualisierung der Dekadenverteilung (Textanzahl pro Dekade). Jeder Balken steht für ein Jahrzehnt auf der x-Achse
plot_counts(decade_counts,
title="Zeitliche Verteilung der Texte im ELTEC-DEU pro Jahrzehnt",
x_label="Jahrzehnt", dtick=10)
3.4.6. Option 2. d-Prose corpus#
Einlesen der Korpusmetadaten in Python#
Die Metadaten des d-Prose-Korpus liegen ebenfalls als CSV-Datei auf Zenodo vor – allerdings unter einer anderen URL und mit Semikolon (;) als Trennzeichen. Wie zuvor lesen wir die Online-Version ein und greifen bei Bedarf auf eine lokale Kopie zurück.
# d-Prose metadata from Zenodo (semicolon-separated); fall back to the local copy if needed
d_prose_published_url = "https://zenodo.org/records/5015008/files/d-prose_V2_norm_year.csv"
d_prose_local_copy = Path("../metadata/metadata_d-prose.csv")
try:
meta_d_prose = pd.read_csv(d_prose_published_url, sep=';')
except Exception as e:
print(f"Konnte Metadaten nicht von Zenodo laden. Nutze lokale Kopie: {d_prose_local_copy}. Das Problem war: {e}")
meta_d_prose = pd.read_csv(d_prose_local_copy, sep=';')
show(meta_d_prose)
| Loading ITables v2.6.1 from the internet... (need help?) |
Analyse der zeitlichen Verteilung des Korpus#
Pro Jahr#
Wir wenden dieselben Hilfsfunktionen auf das d-Prose-Korpus an. Die Jahresangabe steht hier in der Spalte norm_year.
# count texts per year (year is stored in the column 'norm_year')
year_counts, year_stats = summarize_counts(meta_d_prose, 'norm_year', step=1)
print("Textanzahl der Texte im d-Prose pro Jahr:")
print(year_stats)
Textanzahl der Texte im d-Prose pro Jahr:
avg 49.235294
max 136.000000
min 6.000000
Name: count, dtype: float64
Visualisierung der Verteilung pro Jahr. Jeder Balken steht für ein Jahr auf der x-Achse. Man sieht, dass d-Prose ein wesentlich “dichteres” Korpus ist …
plot_counts(year_counts,
title="Zeitliche Verteilung der Texte im d-Prose pro Jahr",
x_label="Jahr", dtick=10)
Pro Jahrzehnt#
Auch hier fassen wir die Jahre wieder zu Jahrzehnten zusammen und zählen die Texte pro Jahrzehnt.
# derive the decade from 'norm_year', then count texts per decade
meta_d_prose['decade'] = (meta_d_prose['norm_year'] // 10) * 10
decade_counts, decade_stats = summarize_counts(meta_d_prose, 'decade')
print("Textanzahl der Texte im d-Prose pro Jahrzehnt:")
print(decade_stats)
Textanzahl der Texte im d-Prose pro Jahrzehnt:
avg 418.5
max 681.0
min 106.0
Name: count, dtype: float64
Visualisierung der Dekadenverteilung (Textanzahl pro Dekade). Jeder Balken steht für ein Jahrzehnt auf der x-Achse
plot_counts(decade_counts,
title="Zeitliche Verteilung der Texte im d-Prose pro Jahrzehnt",
x_label="Jahrzehnt", dtick=10)
Man sieht jedoch auch, dass dieses Korpus sehr klein ist und den für uns relevanten Zeitraum nicht abdeckt.
3.4.7. Option 3. Corpus of German-Language Fiction#
Für das ‘’Corpus of German-Language Fiction’’ liegt keine fertige Metadatentabelle vor. Sämtliche Metadaten sind hier in den Dateinamen in relativ standardisierter Form kodiert:
Author_name_-_Text_title_(year).txt
e.g.
Abraham_Manuel_Fröhlich_-_Die_Verschüttung_im_Hauenstein_(1858).txt
Daher werden die Korpusdateien in einem separaten Notebook per RegEx in Metadaten überführt. An dieser Stelle arbeiten wir mit den Metadaten, die aus dieser Extraktion hervorgehen.
# 🚀 Create metadata directory path
metadata_dir = Path("../metadata")
metadata_dir.mkdir(parents=True, exist_ok=True)
# Download metadata file from GitHub (if not already present)
metadata_file_path = metadata_dir / "metadata_corpus-german_language_fiction.csv"
if not metadata_file_path.exists():
download_file("https://raw.githubusercontent.com/quadriga-dk/Text-Fallstudie-3/refs/heads/main/metadata/metadata_corpus-german_language_fiction.csv", metadata_dir)
Einlesen und Bereinigen der Metadaten#
Wir lesen die heruntergeladene CSV-Datei ein. Die Jahresangabe steht in der Spalte DC.date. Einige wenige Einträge enthalten offensichtlich fehlerhafte, sehr alte Jahreszahlen; diese filtern wir mit DC.date > 1500 heraus.
meta_gfc = pd.read_csv(metadata_file_path)
meta_gfc = meta_gfc[meta_gfc['DC.date'] > 1500] # drop a few erroneous very-old outliers
show(meta_gfc)
| Loading ITables v2.6.1 from the internet... (need help?) |
Analyse der zeitlichen Verteilung des Korpus#
Pro Jahr#
Auch für das Corpus of German-Language Fiction zählen wir die Texte pro Jahr. Die Jahresangabe steht hier in der Spalte DC.date.
# count texts per year (year is stored in the column 'DC.date')
year_counts, year_stats = summarize_counts(meta_gfc, 'DC.date', step=1)
print("Textanzahl der Texte im 'Corpus of German Fiction' pro Jahr:")
print(year_stats)
Textanzahl der Texte im 'Corpus of German Fiction' pro Jahr:
avg 5.902808
max 66.000000
min 0.000000
Name: count, dtype: float64
Visualisierung der Verteilung pro Jahr
plot_counts(year_counts,
title="Zeitliche Verteilung der Texte im 'Corpus of German-Language Fiction' pro Jahr",
x_label="Jahr", dtick=50)
Pro Jahrzehnt#
Wie bei den anderen Korpora betrachten wir abschließend die Verteilung pro Jahrzehnt.
# derive the decade from 'DC.date', then count texts per decade
meta_gfc['decade'] = (meta_gfc['DC.date'] // 10) * 10
decade_counts, decade_stats = summarize_counts(meta_gfc, 'decade')
print("Textanzahl der Texte im 'Corpus of German-Language Fiction' pro Jahrzehnt:")
print(decade_stats)
Textanzahl der Texte im 'Corpus of German-Language Fiction' pro Jahrzehnt:
avg 82.818182
max 380.000000
min 1.000000
Name: count, dtype: float64
Visualisierung der Dekadenverteilung (Textanzahl pro Dekade)
plot_counts(decade_counts,
title="Zeitliche Verteilung der Texte im 'Corpus of German-Language Fiction' pro Jahrzehnt",
x_label="Jahrzehnt", dtick=10)
3.4.8. Bewertung und Entscheidung#
Die explorative Analyse erlaubt nun eine systematische Bewertung entlang der Kriterien von (Schöch, 2017). Die folgende Tabelle verdichtet die Beobachtungen aus den obigen Diagrammen und Kennzahlen zu einer vergleichenden Übersicht. Die einzelnen Einstufungen ergeben sich dabei so:
Zeitliche Abdeckung lässt sich unmittelbar an den Diagrammen ablesen – sie zeigen, welchen Zeitraum ein Korpus überhaupt umfasst. Für unsere Fragestellung ist das 19. Jahrhundert entscheidend: Das Corpus of German-Language Fiction deckt diesen Zeitraum am umfassendsten ab (hoch). Das ELTeC-DEU enthält zwar Texte aus dem 19. Jahrhundert, deckt es aber nicht durchgängig ab – wie das oben erstellte Diagramm „Zeitliche Verteilung der Texte im ELTEC-DEU pro Jahr” zeigt, bleiben viele Jahre ohne Texte (mittel). d-Prose beschränkt sich auf 1870–1920 und lässt damit den Großteil des Jahrhunderts aus (gering).
Umfang ergibt sich aus der Gesamtzahl der Texte – ablesbar an den Kennzahlen sowie an Höhe und Anzahl der Balken. Das Corpus of German-Language Fiction ist mit über 2.700 Texten mit Abstand das größte (groß), das ELTeC-DEU umfasst am wenigsten Texte (klein).
Repräsentativität und Balance zeigen sich an der Gleichmäßigkeit der Verteilung: Ein bewusst über die Zeit ausbalanciertes Korpus wie das ELTeC-DEU erscheint ausgewogener (hoch), während die stark schwankenden Balken des Corpus of German-Language Fiction eine unbalancierte, nicht gezielt repräsentativ angelegte Sammlung erkennen lassen (mittel).
Datenqualität und Verfügbarkeit lassen sich nicht aus den Diagrammen ablesen, sondern ergeben sich aus der Art der Metadaten und ihrer Bereitstellung: Beim Corpus of German-Language Fiction mussten die Metadaten erst aus den Dateinamen extrahiert und um fehlerhafte Jahresangaben bereinigt werden (daher nur mittel), während ELTeC-DEU und d-Prose bereits gepflegte Metadatentabellen mitliefern (hoch). Frei zugänglich und dokumentiert sind alle drei Korpora.
Kriterium |
ELTeC-German |
d-Prose 1870–1920 |
Corpus of German Fiction |
|---|---|---|---|
Zeitliche Abdeckung |
mittel |
gering |
hoch |
Datenqualität |
hoch |
hoch |
mittel |
Repräsentativität |
hoch |
mittel |
mittel |
Umfang |
klein |
mittel |
groß |
Verfügbarkeit |
sehr gut |
gut |
gut |
Zwischenfazit
Das Corpus of German-Language Fiction bietet die größte zeitliche Breite und damit die besten Voraussetzungen, um Veränderungen im sprachlichen Diskurs über Luftqualität im 19. Jahrhundert zu untersuchen.
Man erkennt, dass das Corpus of German-Language Fiction einerseits das einzige Korpus ist, das den für unsere Forschung notwendigen Zeitraum abdeckt. Andererseits ist es – wie die Verteilung zeigt – eindeutig nicht balanciert. In den Metadaten oder in der Korpusbeschreibung gibt es zudem keinerlei Hinweise darauf, dass dieses Korpus als repräsentativ angelegt wurde.
Für die weiteren Analysen müssen wir das Korpus daher filtern. Dies erfolgt im nächsten Abschnitt.