5.5. đ Analyse 2: Syntaktischer N-Gramme#
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.
5.5.1. Ăbersicht#
Im Folgenden werden unsere beiden Korpora â Korpus I und Korpus II, die beiden Zufallsstichproben aus dem Abschnitt Sampling und Filterung des Korpus â mithilfe syntaktischer n-Gramme analysiert. Wie schon in der semantischen Feldanalyse werten wir beide Korpora parallel aus und vergleichen die Ergebnisse, um einschĂ€tzen zu können, welche Muster robust sind und welche von der konkreten Zufallsauswahl abhĂ€ngen. Ziel ist es, wiederkehrende grammatische Muster, insbesondere adjektivische Modifikationen von Substantiven, im Verlauf der Zeit zu untersuchen und damit ĂŒber rein lexikalische HĂ€ufigkeiten hinauszugehen. Im Unterschied zur Analyse semantischer Felder stehen hier syntaktisch motivierte Wortkombinationen auf Basis von AbhĂ€ngigkeitsbeziehungen im Fokus, etwa Adjektiv â Substantiv-Relationen wie âverdorbene â Luftâ, âfrischer â Windâ oder âdicker â Qualmâ. Untersucht wird, wie hĂ€ufig solche Konstruktionen in den beiden Korpora auftreten und wie sich ihr Vorkommen zeitlich entwickelt â um stilistische und diskursive VerĂ€nderungen in der Beschreibung bestimmter Konzepte sichtbar zu machen.
In diesem Notebook-Walkthrough beschrĂ€nken wir uns aus GrĂŒnden der Ăbersichtlichkeit auf das Substantiv âLuftâ, der Code funktioniert jedoch ebenso mit einer ganzen Liste mehrerer Substantive.
Dazu werden die folgenden Schritte durchgefĂŒhrt:
Einlesen des Korpus, der Metadaten sowie der bereits erzeugten spaCy-Annotationen
Auswahl relevanter POS-tags & Dependency-Relationen zur Bildung syntaktischer n-Gramme
Extraktion syntaktischer n-Gramme auf Basis der vorhandenen AbhÀngigkeitsinformationen
Identifikation von AusreiĂertexten, die auffĂ€llige Peaks in den Zeitreihen (mit-)verursachen
KWIC-/Kontextansicht ausgewÀhlter Belegstellen aus diesen Texten zur qualitativen Einordnung der syntaktischen Muster
VerÀnderungsorientierte Analyse: Identifikation aufkommender und verschwindender Modifikatoren sowie der ZeitrÀume mit dem stÀrksten Vokabularwandel
Diskussion der Ergebnisse
5.5.2. Import der Bibliotheken#
import sys
!{sys.executable} -m pip install numpy pandas plotly itables requests tqdm spacy click nbformat
import requests
from pathlib import Path
import re
from typing import Dict, List, Union, Tuple
from collections import OrderedDict, Counter
from time import time
import numpy as np
import pandas as pd
import nbformat
import plotly.express as px
import plotly.io as pio
import plotly.graph_objects as go
from itables import show
from tqdm import tqdm
import spacy
from spacy.tokens import DocBin, Doc
if 'google.colab' in sys.modules:
pio.renderers.default = "colab"
else:
pio.renderers.default = "notebook+plotly_mimetype"
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
Laden des spaCy-Modells
Hinweis
Nach dem Herunterladen des Sprachmodells zeigt spaCy in Jupyter- und Colab-Umgebungen automatisch eine gelbe Warnung âRestart to reload dependenciesâ an. Diese Warnung erscheint an dieser Stelle grundsĂ€tzlich immer â unabhĂ€ngig davon, ob ein Neustart tatsĂ€chlich nötig ist â und kann in aller Regel ignoriert werden: Fahren Sie einfach mit der nĂ€chsten Zelle fort.
Nur falls beim AusfĂŒhren der nĂ€chsten Zelle (spacy.load(...)) tatsĂ€chlich ein Fehler auftritt, wĂ€hlen Sie âLaufzeit neu startenâ (Colab) bzw. âKernel neu startenâ (Jupyter) und fĂŒhren Sie anschlieĂend alle Zellen dieses Notebooks von oben bis zu dieser Stelle erneut aus.
! {sys.executable} -m spacy download de_core_news_sm
nlp = spacy.load("de_core_news_sm")
5.5.3. Einlesen der Annotationen#
In diesem Schritt greifen wir auf die Vorarbeit aus dem Kapitel Korpusverarbeitung â von Strings zu Token zurĂŒck: Dort haben wir jeden Text unseres Korpus mit spaCy annotiert und die Ergebnisse gespeichert (siehe insbesondere Annotation mit spaCy â Dateien schreiben). Diese gespeicherten Annotationen lesen wir nun wieder ein, um darauf aufzubauen.
Zur Erinnerung: Beim Annotieren erzeugt spaCy fĂŒr jeden Text ein sogenanntes Doc-Objekt â den Text samt aller dort ermittelten linguistischen Annotationen (Tokens, Wortarten, Grundformen, syntaktische AbhĂ€ngigkeiten), so wie spaCy ihn im Arbeitsspeicher hĂ€lt. Um diese Doc-Objekte dauerhaft auf der Festplatte zu speichern, haben wir sie im Kapitel Korpusverarbeitung in ein kompaktes, spaCy-eigenes Containerformat namens DocBin gepackt â das sind unsere .spacy-Dateien, wobei eine einzelne Datei mehrere Docs enthalten kann. Beim Einlesen kehren wir diesen Vorgang nun um: Die gespeicherten DocBin-Dateien werden wieder zu vollstĂ€ndigen Doc-Objekten âentpacktâ (rekonstruiert) und in einer Datenstruktur abgelegt, die eine weitere Analyse erlaubt.
In der folgenden Zelle definieren wir den Pfad zum Quellordner, in dem die spacy-Annotationen als Dateien gespeichert sind. AnschlieĂend erstellen wir diesen Ordner (mit der Methode mkdir, die Ordner anlegen kann):
# đ Create data directory path
# Change path here when using another corpus
annotation_dir = Path("../data/spacy")
annotation_dir.mkdir(parents=True, exist_ok=True)
In der folgenden Zelle erhalten wir die Liste der Dateien in dem Ordner auf GitHub, in dem die wÀhrend der Korpusverarbeitung erzeugten spaCy-Annotationen gespeichert sind. Die Liste der direkten URLs zu diesen Dateien wird in der Variable url_list gespeichert.
# đ Create download list
github_api_txt_dir_path = "https://api.github.com/repos/quadriga-dk/Text-Fallstudie-3/contents/data/spacy"
txt_dir_info = requests.get(github_api_txt_dir_path).json()
url_list = [entry["download_url"] for entry in txt_dir_info]
In der folgenden Zelle laden wir jede Datei herunter, indem wir ĂŒber die zuvor erstellte Liste url_list iterieren und jede Datei mit unserer Funktion download_file speichern.
# â ïž Only execute, if you haven't downloaded the files yet!
# đ Download all spacy files from github â this step will take a while
for url in tqdm(url_list, desc="Downloading spaCy files"):
download_file(url, "../data/spacy")
In der folgenden Zelle iterieren wir ĂŒber die .spacy-Dateien und öffnen jede einzelne als spaCy-Objekt in Python (damit alle linguistischen Merkmale des Textes im weiteren Code programmatisch genutzt werden können). â ïž Die Verarbeitung aller Annotationsdateien dauert je nach Computer bzw. Colab-Sitzung etwa 5â10 Minuten.
# Create dictionary to save the corpus data (filenames and tables)
annotated_docs = {}
start = time()
# Iterate over spacy files
for fp in tqdm(annotation_dir.iterdir(), desc="Reading annotated data"):
# check if the entry is a file, not a directory
if fp.is_file():
# check if the file has the correct suffix spacy
if fp.suffix == '.spacy':
print( f"Loading file: {fp.name}" )
# load spacy DocBin objects
doc_bin = DocBin().from_disk(fp)
chunk_docs = list(doc_bin.get_docs(nlp.vocab))
# merge bins into one single document
full_doc = Doc.from_docs(chunk_docs)
# save the data frame to the dictionary, key=filename (without suffix), value=spacy.Doc
annotated_docs[fp.stem] = full_doc
took = time() - start
print(f"Loading the data took: {round(took, 4)} seconds")
Loading the data took: 280.346 seconds
Diese Zelle gibt die ersten 20 Zeilen der Annotation des Textes aus, der zufÀllig der erste im Python-Dictionary ist, das die Annotationen aktuell im Arbeitsspeicher speichert.
print(f"Annotations of first 20 lines of the text: {list(annotated_docs.keys())[0]}:\n")
print("Token\tLemma\tPoS")
for token in annotated_docs[list(annotated_docs.keys())[0]][:20]:
print(f"{token.text}\t{token.lemma_}\t{token.pos_}")
Annotations of first 20 lines of the text: Karl_Immermann_-_Die_Epigonen_(1836):
Token Lemma PoS
Die der DET
Epigonen Epigon NOUN
. -- PUNCT
Familien-Memoiren Familien-Memoir NOUN
in in ADP
neun neun NUM
BĂŒchern Buch NOUN
. -- PUNCT
Herausgegeben Herausgegebe VERB
von von ADP
Karl Karl PROPN
Immermann Immermann PROPN
. -- PUNCT
â â DET
â â PROPN
â â PROPN
â â DET
â â DET
â â DET
â â DET
5.5.4. Einlesen der Metadaten#
AnschlieĂend werden die zugehörigen Metadaten geladen und auf diejenigen Texte beschrĂ€nkt, fĂŒr die Annotationen vorliegen. Als zeitliche Angabe verwenden wir die Spalte DC.date, die fĂŒr jeden Text nur das Publikationsjahr als ganze Zahl enthĂ€lt â ein genaueres Datum liegt nicht vor. Darauf stĂŒtzen sich die spĂ€teren zeitbasierten Aggregationen und Visualisierungen.
Warum lesen wir hier ĂŒberhaupt Metadaten ein? Die spaCy-Annotationen aus dem vorigen Schritt enthalten nur sprachliche Information (Wortarten, Grundformen, syntaktische AbhĂ€ngigkeiten), aber keine bibliografischen Angaben dazu, wann ein Text erschienen ist oder von wem er stammt. Genau diese Angaben brauchen wir aber, um unsere Forschungsfrage nach dem Wandel ĂŒber die Zeit zu beantworten: Erst die Metadaten erlauben es uns, jedem Text sein Erscheinungsjahr bzw. Jahrzehnt zuzuordnen und die gefundenen Adjektive so ĂŒber die Zeit hinweg zu aggregieren. DarĂŒber hinaus liefern sie Titel und Autor:innennamen, die wir spĂ€ter in den KWIC-Tabellen (Belegstellen im Kontext) mit anzeigen. VerknĂŒpft werden Annotationen und Metadaten ĂŒber die Spalte DC.identifier, die in beiden DatensĂ€tzen dieselbe Text-ID enthĂ€lt.
Wie sind diese CSV-Dateien entstanden? Es handelt sich nicht um Rohdaten aus einer externen Quelle, sondern um Zwischenergebnisse, die wir in dieser Fallstudie selbst erzeugt haben. Im Abschnitt âSampling und Filterung des Korpusâ sind wir von der vollstĂ€ndigen Metadatentabelle des Corpus of German-Language Fiction ausgegangen, haben sie auf das 19. Jahrhundert eingegrenzt und daraus pro Jahrzehnt jeweils bis zu 50 Texte zufĂ€llig gezogen. Das Ergebnis dieser Ziehung â zwei zeitlich balancierte Stichproben â wurde dort mit to_csv als Tabelle gespeichert. Genau diese beiden Dateien laden wir jetzt wieder ein, damit die Analyse auf demselben, reproduzierbar zusammengestellten Korpus aufsetzt.
Zur Erinnerung: Zwei-Stichproben-Ansatz#
FĂŒr die Robustheit der Analyse arbeiten wir mit zwei verschiedenen Stichproben (siehe Abschnitt âSampling und Filterung des Korpusâ) â unseren beiden Korpora:
Korpus I: ZufÀllige Auswahl von 50 Texten pro Jahrzehnt (random_state=42)
Korpus II: Alternative zufÀllige Auswahl von 50 Texten pro Jahrzehnt (random_state=31415)
Dieser Ansatz ermöglicht es, zu ĂŒberprĂŒfen, ob die beobachteten Muster robust gegenĂŒber unterschiedlichen Stichproben sind oder ob sie durch die spezifische Textauswahl beeinflusst werden.
In der folgenden Zelle definieren wir den Pfad zu dem Ordner, in dem die Metadatentabellen abgelegt werden sollen.
# đ Create metadata directory path
metadata_dir = Path("../metadata")
metadata_dir.mkdir(parents=True, exist_ok=True)
In der folgenden Zelle laden wir die Metadaten-Dateien fĂŒr die beiden Korpora (Korpus I und Korpus II) mit download_file aus dem GitHub-Repository der Fallstudie herunter und legen sie im dafĂŒr vorgesehenen Ordner ab.
# đ Load the metadata files from GitHub
download_file("https://raw.githubusercontent.com/quadriga-dk/Text-Fallstudie-3/refs/heads/main/metadata/metadata_corpus-german_language_fiction_1810-1900_50-per-decade.csv", "../metadata")
download_file("https://raw.githubusercontent.com/quadriga-dk/Text-Fallstudie-3/refs/heads/main/metadata/metadata_corpus-german_language_fiction_1810-1900_50-per-decade_ALT.csv", "../metadata")
In den folgenden Zellen lesen wir die heruntergeladenen Metadaten mit der Python-Bibliothek pandas fĂŒr die tabellarische Verarbeitung ein und zeigen, wie die Tabellen aussehen.
Korpus I:
metadata_df = pd.read_csv(metadata_dir / "metadata_corpus-german_language_fiction_1810-1900_50-per-decade.csv")
metadata_df = metadata_df[metadata_df['DC.identifier'].isin(annotated_docs.keys())]
show(metadata_df)
| Loading ITables v2.6.1 from the internet... (need help?) |
Korpus II:
metadata_df_alt = pd.read_csv(metadata_dir / "metadata_corpus-german_language_fiction_1810-1900_50-per-decade_ALT.csv")
metadata_df_alt = metadata_df_alt[metadata_df_alt['DC.identifier'].isin(annotated_docs.keys())]
show(metadata_df_alt)
| Loading ITables v2.6.1 from the internet... (need help?) |
5.5.5. Syntaktische N-Gramme extrahieren#
In einem weiteren Schritt können wir die Adjektive extrahieren, die mit dem Nomen Luft in Verbindung stehen (siehe Syntaktische N-Gramme). Wir suchen in den annotierten Texten nach allen FĂ€llen, in denen Luft durch ein Adjektiv nĂ€her bestimmt wird â wie in frische Luft, schlechte Luft oder verdorbene Luft.
Wir machen dabei Gebrauch von den Dependenzstrukturen, die sich durch das spaCy-eigene Doc einfach navigieren lassen â das heiĂt, wir können von einem Wort aus gezielt zu den syntaktisch mit ihm verbundenen Wörtern gelangen (etwa von einem Substantiv zu den Adjektiven, die es nĂ€her bestimmen). In den Standard-spaCy-Modellen fĂŒr die deutsche Sprache wird die syntaktische Position eines solchen adjektivischen Modifikators â anders als im Englischen und einigen anderen Sprachen, wo das Label âamodâ verwendet wird â aus historischen GrĂŒnden mit ânkâ (noun kernel element) annotiert.
In unserer Implementierung stĂŒtzen wir uns daher auf die Kombination aus dem Dependency-Label ânkâ und dem POS-Tag âADJâ, um Adjektive zu identifizieren, die unser Zielsubstantiv âLuftâ nĂ€her bestimmen (in diesem Beispiel-Szenario; weiter unten zeigen wir, wie sich stattdessen auch eigene Wörter untersuchen lassen).
In der folgenden Zelle definieren wir die Funktion, die die eigentliche Extraktion der abhĂ€ngigen Adjektive durchfĂŒhrt. FĂŒr jeden Text im Korpus sammelt sie alle Adjektive, die das Zielsubstantiv ĂŒber die Relation ânkâ nĂ€her bestimmen, und zĂ€hlt deren HĂ€ufigkeit. Das Ergebnis ist eine Tabelle, die fĂŒr jeden Text festhĂ€lt, wie oft jedes beobachtete Adjektiv mit dem Zielsubstantiv auftritt â die Grundlage fĂŒr die spĂ€tere Analyse der HĂ€ufigkeiten im Zeitverlauf.
def extract_dependent_adjective_list(spacy_docs: Dict, metadata_df: pd.DataFrame,
noun_input: Union[str, List[str]], top_n: int = 10) -> Tuple[pd.DataFrame, List[str]]:
"""
Extract adjective modifiers (nk) for a noun or list of nouns and track their frequency over time.
Parameters:
-----------
spacy_docs : dict
Dictionary with file_ids as keys and spaCy Doc objects as values
metadata_df : pd.DataFrame
DataFrame with columns: 'lastname', 'firstname', 'DC.title', 'DC.date', 'volume', 'ID', 'decade'
noun_input : str or list of str
Single noun lemma (e.g., 'liebe') or list of noun lemmata (e.g., ['liebe', 'leidenschaft'])
top_n : int
Size of the returned top_adjectives list (default: 10).
The returned DataFrame always contains ALL observed adjectives â top_n only
controls the convenience list returned alongside it.
Returns:
--------
tuple : (pd.DataFrame, list)
- DataFrame with columns: filename, DC.title, DC.date, adjective, count, noun_count
(one row per (file, observed adjective) for every file in which the target
noun appears at least once; count may be 0)
- List of the top N most frequent adjectives across the corpus
"""
# Convert single noun to list for uniform processing
if isinstance(noun_input, str):
noun_list = [noun_input]
else:
noun_list = noun_input
# Convert to lowercase for case-insensitive matching
noun_list_lower = [noun.lower() for noun in noun_list]
# Single token-scanning pass: for each metadata-matched doc, compute its
# per-document adjective counts once and simultaneously accumulate them into
# the corpus-wide counter. (Previously this token walk was done twice â once
# globally, once per document â which doubled the expensive spaCy work.)
# Building the global vocabulary only from metadata-matched docs also avoids
# phantom always-zero adjective columns from docs that are dropped below.
all_adjectives = Counter()
per_doc = [] # (file_id, meta_row, adjective_counts, noun_count)
for file_id, doc in spacy_docs.items():
# Get metadata for this file
meta_row = metadata_df[metadata_df['DC.identifier'] == file_id]
if meta_row.empty:
continue
# Count adjectives modifying the target nouns in this document
adjective_counts = Counter()
noun_count = 0
for token in doc:
# Check if this token is one of our target nouns
if token.lemma_.lower() in noun_list_lower and token.pos_ == 'NOUN':
noun_count += 1
# Find adjective modifiers (nk dependency)
for child in token.children:
if child.dep_ == 'nk' and child.pos_ == 'ADJ':
adjective_counts[child.lemma_.lower()] += 1
all_adjectives.update(adjective_counts)
per_doc.append((file_id, meta_row, adjective_counts, noun_count))
# Full set of adjectives observed in the corpus (no cutoff â used to populate the DataFrame)
all_observed_adjectives = list(all_adjectives.keys())
# Top N most frequent â returned as a convenience list for downstream filtering
top_adjectives = [adj for adj, count in all_adjectives.most_common(top_n)]
# Densify: emit one row per (document, observed adjective). No spaCy work here â
# we only read the cached per-document counts computed in the pass above.
results = []
for file_id, meta_row, adjective_counts, noun_count in per_doc:
# Create a row for each observed adjective in the corpus when the noun
# appears in this document (count may be 0 â needed by downstream
# time-series averaging, which divides by the number of texts in a year).
for adjective in all_observed_adjectives:
count = adjective_counts.get(adjective, 0)
if noun_count > 0 or count > 0: # Include if we have nouns or this adjective
results.append({
'filename': file_id,
'DC.title': meta_row['DC.title'].values[0],
'DC.date': meta_row['DC.date'].values[0],
'adjective': adjective,
'count': count,
'noun_count': noun_count
})
return pd.DataFrame(results), top_adjectives
Die obige Funktion lĂ€sst sich mit einem einzelnen Substantiv oder einer Liste von Substantiven verwenden. In diesem Beispiel nutzen wir lediglich das einzelne Substantiv âLuftâ:
noun = "Luft"
Wenn Sie dieses Notebook jedoch lokal verwenden, können Sie die Variable fĂŒr das Substantiv neu definieren, zum Beispiel so:
noun = "Nebel"
oder so:
noun = ["Luft", "Nebel", "Rauch"]
In der folgenden Zelle fĂŒhren wir die zuvor definierte Funktion extract_dependent_adjective_list aus und extrahieren die vom ausgewĂ€hlten Substantiv abhĂ€ngigen Adjektive (aus Korpus I). Wir erhalten eine Liste (einen DataFrame, adj_df), in der fĂŒr jedes Buch die HĂ€ufigkeit derjenigen Adjektive aufgefĂŒhrt ist, die in unserem Korpus gemeinsam mit dem Substantiv ein syntaktisches Bigramm bilden. Zudem erhalten wir eine Gesamtliste (top_adjs) der am hĂ€ufigsten vorkommenden Adjektive dieser Art fĂŒr das gesamte Korpus.
â ïž Die AusfĂŒhrung dieser Zelle dauert etwa eine Minute.
adj_df, top_adjs = extract_dependent_adjective_list(annotated_docs,
metadata_df,
noun,
top_n=10)
Nun fĂŒhren wir denselben Vorgang fĂŒr Korpus II durch (die AusfĂŒhrung dauert wiederum etwa eine Minute).
adj_df_alt, top_adjs_alt = extract_dependent_adjective_list(annotated_docs,
metadata_df_alt,
noun,
top_n=10)
Ergebnisse der Extraktion syntaktischer N-Gramme#
Nun liegen fĂŒr jede Stichprobe zwei Arten von Ergebnissen vor. ZunĂ€chst die Liste der top_n hĂ€ufigsten Adjektive in der jeweiligen Stichprobe:
Korpus I:
# Print the top adjectives
for adj in top_adjs:
print('* ', adj)
* frisch
* frei
* rein
* blau
* kalt
* leer
* klar
* still
* warm
* heiĂ
Korpus II:
# Print the top adjectives (alternative sample)
for adj in top_adjs_alt:
print('* ', adj)
* frisch
* frei
* rein
* blau
* leer
* klar
* kalt
* warm
* still
* heiĂ
Zwischenergebnis#
Hier lĂ€sst sich bereits erkennen, dass die beiden Stichproben recht Ă€hnliche Ergebnisse liefern. So enthalten beispielsweise die beiden Listen der zehn hĂ€ufigsten Wörter dieselben Begriffe, wobei sich lediglich die Reihenfolge geringfĂŒgig unterscheidet. Wir werden im Weiteren analysieren, wie stark die HĂ€ufigkeiten jener Wörter variieren, die in dieser Rangliste unterschiedliche PlĂ€tze belegen.
Die folgenden Tabellen enthalten die HĂ€ufigkeiten dieser Adjektive (und des/der Zielsubstantiv(e) â siehe Spalte noun_count) in den einzelnen BĂŒchern â jeweils eine pro Stichprobe:
Korpus I:
show(adj_df)
| Loading ITables v2.6.1 from the internet... (need help?) |
Hinweis: Die interaktive Tabelle wird vollstĂ€ndig in das Notebook eingebettet; bei sehr groĂen Tabellen zeigt itables daher nur eine verkleinerte Auswahl der Zeilen an (âdownsampledâ), damit das Notebook nicht zu groĂ und langsam wird.
Korpus II:
show(adj_df_alt)
| Loading ITables v2.6.1 from the internet... (need help?) |
Speichern der Ergebnisse als Dateien#
Die Ergebnisse einer solchen Extraktion stellen bereits ein gewisses Zwischenergebnis der Forschung dar. Speichern wir sie als Dateien:
# Create intermediate directory
interm_data_synt_ngrams_dir = Path("interm_data_synt_ngrams")
interm_data_synt_ngrams_dir.mkdir(parents=True, exist_ok=True)
# Save adj_df to CSV
adj_df.to_csv(interm_data_synt_ngrams_dir / "adj_df_luft.csv", index=False)
print(f"Saved adj_df to interm_data_synt_ngrams/adj_df_luft.csv ({len(adj_df)} rows)")
# Save adj_df_alt to CSV
adj_df_alt.to_csv(interm_data_synt_ngrams_dir / "adj_df_alt_luft.csv", index=False)
print(f"Saved adj_df_alt to interm_data_synt_ngrams/adj_df_alt_luft.csv ({len(adj_df_alt)} rows)")
Laden gespeicherter Ergebnisse (Optional)#
Falls die Ergebnisse bereits extrahiert und als CSV gespeichert wurden, können sie hier geladen werden, anstatt die Extraktion erneut durchzufĂŒhren:
# Optional: Load previously saved results from CSV
# Load adj_df
adj_df = pd.read_csv(interm_data_synt_ngrams_dir / "adj_df_luft.csv")
# Load adj_df_alt
adj_df_alt = pd.read_csv(interm_data_synt_ngrams_dir / "adj_df_alt_luft.csv")
# Recreate top_adjs lists from the dataframes
top_adjs = adj_df.groupby('adjective')['count'].sum().sort_values(ascending=False).head(10).index.tolist()
top_adjs_alt = adj_df_alt.groupby('adjective')['count'].sum().sort_values(ascending=False).head(10).index.tolist()
5.5.6. Analyse und Visualisierung#
Korpusweite HĂ€ufigkeiten adjektivischer Modifikatoren#
ZunĂ€chst betrachten wir die Gesamtverteilung der adjektivischen Modifikatoren. Bevor zeitliche Entwicklungen analysiert werden, ist es sinnvoll, einen Ăberblick darĂŒber zu gewinnen, welche Adjektive im gesamten Korpus am hĂ€ufigsten als syntaktische Modifikatoren der untersuchten Substantive auftreten. Diese aggregierte Betrachtung erlaubt es, dominante Beschreibungs- und Bewertungsmuster zu identifizieren und dient zugleich als Ausgangspunkt fĂŒr die nachfolgenden diachronen Analysen.
Korpus I:#
totals = plot_top_adjective_ranking(adj_df, top_n=20, noun_input=noun, metric="count")
Alternativ könnten wir dieselben ZÀhlwerte auch in tabellarischer Form darstellen, um die genauen Zahlen besser erkennen zu können:
show_top_adjectives_table(adj_df)
Korpus II:#
totals_alt = plot_top_adjective_ranking(adj_df_alt, top_n=20, noun_input=noun, metric="count")
Tabellarische Form:
show_top_adjectives_table(adj_df_alt)
Hier zeigt sich erneut, dass sich die Ergebnisse der HĂ€ufigkeitsverteilung zwischen den beiden Stichproben nicht wesentlich unterscheiden â oder dass zumindest die allgemeine Rangfolge der Adjektive Ă€hnlich ist. âFrischâ steht eindeutig an der Spitze, und âFreiâ ist in beiden Korpora zweifellos das zweithĂ€ufigste Adjektiv. Die ĂŒbrigen Adjektive weisen deutlich geringere HĂ€ufigkeiten auf, und auch die Unterschiede zwischen ihren HĂ€ufigkeitswerten sind weitaus geringer.
Diachrone Analyse adjektivisch-substantivischer Konstruktionen#
Im nĂ€chsten Schritt wird die Analyse um eine diachrone Perspektive erweitert. Anstatt ausschlieĂlich korpusweite GesamthĂ€ufigkeiten zu betrachten, wird nun untersucht, wie sich die Verwendung der zuvor identifizierten adjektivischen Modifikatoren im Zeitverlauf entwickelt. Die zeitliche Aggregation erlaubt es, Verschiebungen in Beschreibungs- und Bewertungsmustern nachzuzeichnen und diese mit historischen Prozessen in Beziehung zu setzen.
Die y-Achse der folgenden Diagramme gibt fĂŒr jedes Adjektiv an, welcher Anteil der Luft-Vorkommen (in Prozent) jeweils durch dieses Adjektiv nĂ€her bestimmt wird â also etwa, wie viele aller âLuftâ-ErwĂ€hnungen eines Jahres als âfrische Luftâ auftreten. Da die Werte einzelner Jahre stark schwanken, glĂ€tten wir sie mit einem gleitenden Durchschnitt ĂŒber ein Fenster von 10 Jahren: Jeder Punkt ist der Mittelwert der umliegenden Jahre, sodass der lĂ€ngerfristige Trend deutlicher hervortritt als die jĂ€hrlichen AusschlĂ€ge.
Korpus I#
# yearly lineplots + moving average
yearly_1, moving_1 = plot_adjective_trends_moving_avg_plotly(
adj_df, top_adjs, noun_input=noun, window_years=10, n_plot=8
)
Korpus II:#
# NEW: yearly lineplots + moving average
yearly_2, moving_2 = plot_adjective_trends_moving_avg_plotly(
adj_df_alt, top_adjs_alt, noun_input=noun, window_years=10, n_plot=8
)
AusreiĂertexte und Kontextanalyse#
Welche konkreten Texte sind fĂŒr die AusschlĂ€ge in den Zeitreihen verantwortlich?
Um diese Frage transparent zu beantworten, identifizieren wir sogenannte âAusreiĂertexteâ (aber nicht ĂŒber einen komplexen statistischen Test, sondern ĂŒber eine nachvollziehbare ZĂ€hl- und Ranking-Logik). FĂŒr jedes Werk zĂ€hlen wir (1) noun_count, also wie oft das Lemma Luft im Text vorkommt, und (2) count, also wie oft ein bestimmtes Adjektiv (z.B. frisch) in der Dependenzannotation als attributiver Modifikator von Luft erscheint (ADJ â Luft, z.B. frische Luft). Daraus berechnen wir pro Text eine normalisierte Kennzahl rel_freq = (count / noun_count) * 100, also den Anteil der Luft-Vorkommen, die mit diesem Adjektiv modifiziert werden.
AnschlieĂend bestimmen wir fĂŒr jedes Adjektiv zunĂ€chst die Dekaden, in denen der Durchschnitt dieser relativen HĂ€ufigkeit besonders hoch ist (Peak-Dekaden), und listen innerhalb dieser Zeitfenster die Texte mit den höchsten rel_freq-Werten (Top-k) als âAusreiĂertexteâ auf. Um instabile Extremwerte zu vermeiden, filtern wir Texte mit sehr wenigen Luft-Belegen (z.B. noun_count < 5) aus. ErgĂ€nzend berechnen wir pro Dekade den Anteil eines einzelnen Textes an allen AdjektivâLuft-Treffern (share_of_adj_count_in_decade), um sichtbar zu machen, ob ein Ausschlag der Zeitreihe vor allem von wenigen Werken getragen wird. Diese identifizierten Texte werden anschlieĂend mithilfe von KWIC- bzw. Satzkontexten qualitativ ĂŒberprĂŒft, um die semantische Funktion der jeweiligen Konstruktionen genauer einzuordnen.
Was bedeutet KWIC?
KWIC steht fĂŒr Keyword in Context (deutsch etwa âStichwort im Kontextâ) â eine klassische, konkordanzartige Darstellungsform der Korpuslinguistik. Dabei wird der gesuchte Ausdruck â hier die jeweilige AdjektivâNomen-Konstruktion wie frische Luft â in der Mitte einer Zeile angezeigt, flankiert von einigen Wörtern unmittelbar davor und danach. So lĂ€sst sich auf einen Blick lesen, in welchem unmittelbaren sprachlichen Umfeld ein Ausdruck tatsĂ€chlich vorkommt.
Der Vorteil: Wir können die Treffer nicht nur zĂ€hlen, sondern ihre konkrete Verwendung im Satz nachlesen und ihre Bedeutung so qualitativ einordnen. Reicht der enge KWIC-Ausschnitt fĂŒr die Interpretation nicht aus, ziehen wir ergĂ€nzend den gröĂeren Satzkontext heran.
metadata_df.columns
Index(['lastname', 'firstname', 'DC.title', 'DC.date', 'volume',
'DC.identifier', 'decade'],
dtype='object')
Die folgende Tabelle listet die identifizierten AusreiĂertexte fĂŒr die jeweiligen AdjektivâNomen-Konstruktionen auf, also jene Werke, die maĂgeblich zu den beobachteten AusschlĂ€gen in den Zeitreihen beitragen (in Korpus I).
show(outliers_table_1)
| Loading ITables v2.6.1 from the internet... (need help?) |
Die KWIC-Ansicht zeigt ausgewĂ€hlte Satzkontexte der AdjektivâNomen-Konstruktionen aus diesen AusreiĂertexten und ermöglicht eine qualitative Einordnung ihrer semantischen Verwendung.
show(kwic_table_1)
| Loading ITables v2.6.1 from the internet... (need help?) |
Die folgende Tabelle listet die identifizierten AusreiĂertexte fĂŒr die jeweiligen AdjektivâNomen-Konstruktionen auf, also jene Werke, die maĂgeblich zu den beobachteten AusschlĂ€gen in den Zeitreihen beitragen (in Korpus II).
show(outliers_table_2)
| Loading ITables v2.6.1 from the internet... (need help?) |
Die KWIC-Ansicht zeigt ausgewĂ€hlte Satzkontexte der AdjektivâNomen-Konstruktionen aus diesen AusreiĂertexten und ermöglicht eine qualitative Einordnung ihrer semantischen Verwendung.
show(kwic_table_2)
| Loading ITables v2.6.1 from the internet... (need help?) |
5.5.7. Diskussion der syntaktischen N-Gramm-Analyse#
Die syntaktische N-Gramm-Analyse verschiebt den Blick von der bloĂen HĂ€ufigkeit des Begriffs Luft hin zur Frage, wie ĂŒber Luft gesprochen wird â also welche Adjektive als Modifikatoren in Konstruktionen wie frische Luft, reine Luft oder freie Luft auftreten. Ăber beide Stichproben hinweg zeigt sich, dass einige Modifikatoren sehr stabil und dominant sind â insbesondere frisch (und in Teilen auch frei).
Offensichtlich spiegeln sich in dieser Liste der Adjektive keinerlei Anzeichen fĂŒr schlechte LuftqualitĂ€t wider. Vielmehr deutet die Dominanz positiv konnotierter Adjektive wie frisch, rein und frei darauf hin, dass die Literatur eher in jene Richtung reagiert, die wir in der Operationalisierung als utopischen Gegenentwurf beschrieben haben â mit einer herausgehoben positiven Semantik von âLuftâ.