5.2. 🚀 Analyse 1: Diachrone Frequenzdiagramme des semantischen Felds “Luft”#
5.2.1. Hinweise zur Ausführung des Notebooks#
Dieses Notebook kann auf unterschiedlichen Levels erarbeitet werden (siehe Abschnitt “Technische Voraussetzungen”):
Book-Only Mode
Cloud Mode: Dafür auf 🚀 klicken und z.B. in Colab ausführen.
Local Mode: Dafür auf Herunterladen ↓ klicken und “.ipynb” wählen.
5.2.2. Übersicht#
Im Folgenden werden die annotierten Dateien (CSV-Format) analysiert. Unser Ziel ist es, die Wort-/Lemma-Häufigkeiten des semantischen Felds “Luft” über Zeit zu analysieren und zu visualisieren, um festzustellen, ob es, parallel zur industriellen Revolution, einen Anstieg im Auftreten des Felds gibt.
Dafür werden folgendene Schritte durchgeführt:
Einlesen des Korpus, der Metadaten-Dateien für Korpora I und II und des semantischen Felds “Luft”
Extraktion der Worthäufigkeiten und Plotten der Worthäufigkeiten für Korpus I
Extraktion der Worthäufigkeiten und Plotten der Worthäufigkeiten für Korpus II
Diskussion der Ergebnisse
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Voraussetzungen zur Ausführung des Jupyter Notebooks
- Installieren der Bibliotheken
- Pfad zu den Daten setzen
- Laden der Daten (z.B. über den Command `wget` (s.u.))
5.2.3. Einlesen der Daten, Metadaten und der Grippe-Wortliste#
Um eine/mehrere Dateien mit Python bearbeiten zu können, müssen die Dateien zuerst ausgewählt werden, d.h der Pfad zu den Dateien wird gesetzt, und dann eingelesen werden.
Einlesen des Korpus (CSV-Dateien)#
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Zuerst wird der Ordner angelegt, in dem die CSV-Dateien gespeichert werden. Der Einfachheit halber wird die gleich Datenablagestruktur wie in dem GitHub Repository, in dem die Daten gespeichert sind, vorausgesetzt. Danach werden alle CSV-Dateien im Korpus heruntergeladen und gespeichert. Dafür sind folgende Schritte nötig:- Es wird eine Liste erstellt, die die URLs zu den einzelnen CSV-Dateien beinhaltet.
- Die Liste wird als txt-Datei gespeichert.
- Alle Dateien aus der Liste werden heruntergeladen und in dem Ordner ../data/csv gespeichert.
Setzen des Pfads:
# set the path to csv files to be processed
corpus_dir = Path(r"../data/csv")
Einlesen der CSV-Dateien
annotated_docs = {}
start = time()
for fp in tqdm(corpus_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 == '.csv':
df = pd.read_csv(fp)
annotated_docs[fp.stem] = df
took = time() - start
print(f"Loading the data took: {round(took, 4)} seconds")
Loading the data took: 15.4598 seconds
Wie viele Dateien wurden eingelesen?
len(annotated_docs)
724
Da wir die Dateien für beide Korpora zusammen einlesen und sie erst später anhand der Metadaten in die zwei Korpora aufteilen, liegt die Anzahl der Korpusdateien bei 724. Jedes Korpus besteht aus 450 Texten, davon überschneiden sich 176 Texte, so kommen wir zu einer Gesamtanzahl von 724 Texten.
Wie sieht der Anfang der ersten Datei aus?
annotated_docs[list(annotated_docs.keys())[1]][:15]
| Token | Lemma | POS | |
|---|---|---|---|
| 0 | Edmund | Edmund | PROPN |
| 1 | Hoefer | Hoefer | PROPN |
| 2 | Das | der | DET |
| 3 | Wyler | Wyler | ADJ |
| 4 | Schlößchen | Schlößchen | NOUN |
| 5 | Die | der | DET |
| 6 | rothen | roth | ADJ |
| 7 | Nelken | Nelk | NOUN |
| 8 | . | -- | PUNCT |
| 9 | Ein | ein | DET |
| 10 | Schrei | Schrei | NOUN |
| 11 | Erzählungen | Erzählung | NOUN |
| 12 | Stuttgart | Stuttgart | PROPN |
| 13 | . | -- | PUNCT |
| 14 | Verlag | Verlag | NOUN |
Einlesen der Metadaten#
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Zuerst wird der Ordner angelegt, in dem die Metadaten-Datei gespeichert wird. Wieder wird die gleich Datenablagestruktur wie in dem GitHub Repository vorausgesetzt. Der Text wird aus GitHub heruntergeladen und in dem Ordner ../data/metadata/ abgespeichert. Der Pfad kann in der Variable metadata_path angepasst werden. Die einzulesende Datei muss die Endung `.csv` haben.# set path to metadata file
metadata_path_1 = '../metadata/metadata_corpus-german_language_fiction_1810-1900_50-per-decade.csv'
metadata_path_2 = '../metadata/metadata_corpus-german_language_fiction_1810-1900_50-per-decade_ALT.csv'
def read_replace_metadata(fp):
corpus_metadata = pd.read_csv(fp)
corpus_metadata['year'] = pd.to_datetime(corpus_metadata['year'], format='%Y')
corpus_metadata = corpus_metadata.fillna("-")
return corpus_metadata
# read metadata file to pandas dataframe
corpus_metadata_1 = read_replace_metadata(metadata_path_1)
corpus_metadata_2 = read_replace_metadata(metadata_path_2)
Wie sieht die Metadaten-Datei aus? Hier können Sie nach Texten suchen und die Tabelle nach den Spalten sortieren, z.B. um zu erfahren, welches der frühste Text in Korpus I ist.
⚠️ Da für die Texte nur das Publikationsjahr vorliegt, für die Visualisierung aber ein Datum verwendet wird, wird das Datum in der Spalte year für jeden Text auf den 01.01. gesetzt.
show(corpus_metadata_1)
| Loading ITables v2.6.1 from the internet... (need help?) |
Einlesen der Wortliste (Semantisches Feld “Luft”)#
Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️
Parallel zur Metadaten-Datei wird ein Ordner für die Wortlisten-Datein angelegt, die Datei wird aus GitHub geladen und in dem erstellten Ordner abgelegt.path_to_wordlist = Path("../wordlist/luft_semantisches_feld.txt")
semantic_field_nouns = list(set([word for word in path_to_wordlist.read_text().split("\n") if len(word) > 0]))
Wie sieht die Wortliste aus?
semantic_field_nouns[:20]
['Stickluft',
'Luftzug',
'Himmelsluft',
'Luftdurchzug',
'Ozon',
'Zugluft',
'Lufterschütterung',
'Luftabzug',
'Nebel',
'Lufthauche',
'Abendluft',
'Qualmwolke',
'Lufthieb',
'Luftart',
'Pestluft',
'Faulluft',
'Luftbad',
'Luftfeuchtigkeit',
'Qualm',
'Luftton']
5.2.4. Häufigkeiten der Wörter im semantischen Feld berechnen#
def extract_noun_list_counts(annotated_docs: Dict, metadata_df: pd.DataFrame,
noun_list: List[str]) -> pd.DataFrame:
"""
Calculate the combined relative frequency of a list of nouns for each text.
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', 'title', 'year', 'ID', 'decade'
noun_list : list of str
List of noun lemmata to count together
Returns:
--------
pd.DataFrame
DataFrame with columns: filename, title, year, total_freq, total_count, total_tokens
"""
results = []
for idx, meta_row in metadata_df.iterrows(): #changed from metadata_df.itertuples() to metadata_df.iterrows() to accommodate for our conventional DC names like 'DC.title' (this breaks the .itertuples() method)
file_id = meta_row['DC.identifier']
if file_id in annotated_docs:
doc = annotated_docs[file_id]
else:
print(f"File {file_id} not in the corpus. Skipping...")
continue
# Count total tokens
total_tokens = len(doc)
# Skip empty texts
if total_tokens == 0:
continue
# Count occurrences in the list
lemma_counts = doc.Lemma.value_counts()
counts = lemma_counts.reindex(noun_list, fill_value=0)
specific_nouns = pd.DataFrame([counts.values], columns=counts.index)
specific_nouns['DC.identifier'] = file_id
specific_nouns['total_count_tokens'] = total_tokens
results.append(specific_nouns)
combined_result = pd.concat(results, ignore_index=True)
metadata_result = pd.merge(metadata_df, combined_result, on="DC.identifier")
return metadata_result
def get_relative_frequencies(df, semantic_field_nouns):
df['total_count_semantic_field'] = df[semantic_field_nouns].sum(axis=1)
df['relative_frequency'] = (df['total_count_semantic_field'] / df['total_count_tokens'])*100
return df
# Extract frequencies for Corpus I and II
count_1_df = extract_noun_list_counts(annotated_docs, corpus_metadata_1, semantic_field_nouns)
count_2_df = extract_noun_list_counts(annotated_docs, corpus_metadata_2, semantic_field_nouns)
# Calculate relative frequencies for Corpus I and II
freq_1_df = get_relative_frequencies(count_1_df, semantic_field_nouns)
freq_2_df = get_relative_frequencies(count_2_df, semantic_field_nouns)
show(count_1_df)
| Loading ITables v2.6.1 from the internet... (need help?) |
Ergebnisse für Korpus I angucken#
show(freq_1_df[['lastname', 'firstname', 'DC.title', 'year', 'total_count_tokens', 'total_count_semantic_field', 'relative_frequency']])
| Loading ITables v2.6.1 from the internet... (need help?) |
Ergebnisse für Korpus II angucken#
show(freq_2_df[['lastname', 'firstname', 'DC.title', 'year', 'total_count_tokens', 'total_count_semantic_field', 'relative_frequency']])
| Loading ITables v2.6.1 from the internet... (need help?) |
Häufigkeiten als Streudiagramm mit Trend-Linie darstellen#
def plot_noun_list_scatter(freq_df: pd.DataFrame, noun_list: List[str],
show_trendline: bool = True, verbose: bool = False):
"""
Create a scatter plot showing the combined frequency of a noun list over time.
Parameters:
-----------
freq_df : pd.DataFrame
DataFrame returned by extract_noun_list_frequencies()
noun_list : list of str
The list of nouns being analyzed (for the text)
show_trendline : bool
If True, add a linear regression trendline (default: True)
verbose : bool
If True, print diagnostic information about the trendline (default: False)
Returns:
--------
plotly.graph_objects.Figure
The figure object (will display automatically in Jupyter)
"""
# Create scatter plot
fig = go.Figure()
fig.add_trace(go.Scatter(
x=freq_df['year'],
y=freq_df['relative_frequency'],
mode='markers',
name='Texts',
text=freq_df['DC.title'],
customdata=np.column_stack((freq_df['total_count_semantic_field'], freq_df['lastname'])),
hovertemplate='<b>%{text}</b> (%{customdata[1]})<br>' +
'Year: %{x|%Y}<br>' +
'Frequency: %{y:.2f} per 100 tokens<br>' +
'Total count: %{customdata[0]}<br>' +
'<extra></extra>',
marker=dict(
size=8,
color='steelblue',
opacity=0.7,
line=dict(width=1, color='white')
)
))
# Add trendline if requested
if show_trendline:
# Calculate linear regression
# Convert year to numeric, handling datetime objects
if pd.api.types.is_datetime64_any_dtype(freq_df['year']):
# If datetime, extract the year
x = freq_df['year'].dt.year.values.astype(float)
else:
# Otherwise convert to numeric
x = pd.to_numeric(freq_df['year'], errors='coerce').values
y = freq_df['relative_frequency'].values
# Remove any NaN values
valid_idx = ~(np.isnan(x) | np.isnan(y))
x = x[valid_idx]
y = y[valid_idx]
if verbose:
print(f"Valid data points for trendline: {len(x)}")
print(f"Year range: {x.min():.0f} to {x.max():.0f}")
print(f"Frequency range: {y.min():.2f} to {y.max():.2f}")
if len(x) > 1: # Need at least 2 points for a line
# Fit line: y = mx + b
m, b = np.polyfit(x, y, 1)
if verbose:
print(f"Trendline equation: y = {m:.6f}x + {b:.4f}")
print(f"Slope interpretation: {'increasing' if m > 0 else 'decreasing' if m < 0 else 'flat'} trend")
# Create trendline
x_trend = np.array([x.min(), x.max()])
y_trend = m * x_trend + b
fig.add_trace(go.Scatter(
x=x_trend,
y=y_trend,
mode='lines',
name='Trend',
line=dict(color='red', width=2, dash='dash'),
hovertemplate='Trendline<br>Year: %{x}<br>%{y:.2f}<extra></extra>'
))
else:
if verbose:
print("Warning: Not enough valid data points to draw trendline (need at least 2)")
# Create a readable noun list for the title
noun_list = list(noun_list)
noun_list_str = ', '.join(noun_list[:5])
if len(noun_list) > 5:
noun_list_str += f', ... ({len(noun_list)} total)'
# Update layout
fig.update_layout(
title=f'Relative Frequenz des semantischen Felds "Luft"',
xaxis_title='Jahr',
yaxis_title='Relative Frequenz (pro 100 Tokens)',
hovermode='closest',
height=600,
showlegend=show_trendline
)
return fig
# Create scatter plot
plot_noun_list_scatter(freq_1_df, semantic_field_nouns)
# Create scatter plot
plot_noun_list_scatter(freq_2_df, semantic_field_nouns)
Entwicklung der häufigsten Wörter des semantisches Felds anzeigen#
Es kommen manche Wörter des semantischen Felds sehr häufig und andere gar nicht vor. Im Folgenden wollen wir uns die Wörter im semantischen Feld genauer angucken, die im Schnitt häufiger einmal oder mehr auf 100 Wörter vorkommen. So können wir feststellen, welche Wörter das semantische Feld ausmachen.
Im folgenden Streudiagramm ist jeder Punkt wieder ein Text, allerdings wird nun die relative Häufigkeit eines Wort angezeigt. Dieses Wort ist maßgeblich für die Präsenz des semantischen Felds in diesem Text verantwortlich.
def filter_df_by_mean_threshold(df, semantic_field_nouns, threshold=1):
# Find which checked columns pass the threshold
passing_cols = [col for col in semantic_field_nouns if df[col].mean() > threshold]
# Keep passing columns plus all columns that weren't checked
other_cols = [col for col in df.columns if col not in semantic_field_nouns]
df_filtered = df[other_cols + passing_cols].copy()
return df_filtered
def plot_single_terms(df_filtered, semantic_field_nouns):
# Calculate relative frequencies for each term
terms = [col_name for col_name in df_filtered.columns if col_name in semantic_field_nouns]
for term in terms:
df_filtered[f'{term}_relative'] = (df_filtered[term] / df_filtered['total_count_tokens']) * 100
# Melt the dataframe to long format
plot_data = df_filtered.melt(
id_vars=['year', 'DC.title', 'lastname', 'total_count_tokens'],
value_vars=[f'{term}_relative' for term in terms],
var_name='Term',
value_name='Relative_Frequency'
)
# Clean up term names
plot_data['Term'] = plot_data['Term'].str.replace('_relative', '')
fig = go.Figure()
colors = ['steelblue', 'coral', 'seagreen', 'mediumpurple', 'goldenrod']
term_colors = dict(zip(terms, colors))
for term in terms:
term_data = plot_data[plot_data['Term'] == term].sort_values('year')
# Add scatter points
fig.add_trace(go.Scatter(
x=term_data['year'],
y=term_data['Relative_Frequency'],
mode='markers',
name=term,
text=term_data['DC.title'],
customdata=term_data['lastname'],
hovertemplate='<b>%{text}</b> (%{customdata})<br>' +
'Year: %{x|%Y}<br>' +
f'{term} Frequency: ' + '%{y:.4f} per 100 tokens<br>' +
'<extra></extra>',
marker=dict(size=6, color=term_colors[term], opacity=0.5),
showlegend=True
))
# Add linear trend line (if enough data points)
if len(term_data) > 2:
try:
# Convert year to numeric for regression
x = term_data['year'].astype(int).values
y = term_data['Relative_Frequency'].values
# Calculate linear regression
coeffs = np.polyfit(x, y, 1)
trend_line = np.polyval(coeffs, x)
# Calculate MSE
mse = np.mean((y - trend_line) ** 2)
fig.add_trace(go.Scatter(
x=term_data['year'],
y=trend_line,
mode='lines',
name=f'{term} (Trend)',
line=dict(color=term_colors[term], width=2, dash='dash'),
showlegend=True,
hovertemplate=f'Linearer Trend<br>MSE: {mse:.6f}<extra></extra>'
))
except:
pass
fig.update_layout(
title='Relative Frequenz des gefilterten semantischen Felds über Zeit',
xaxis_title='Jahr',
yaxis_title='Frequenz pro 100 Tokens',
height=600
)
fig.show()
filtered_1 = filter_df_by_mean_threshold(freq_1_df, semantic_field_nouns)
plot_single_terms(filtered_1, semantic_field_nouns)
filtered_2 = filter_df_by_mean_threshold(freq_2_df, semantic_field_nouns)
plot_single_terms(filtered_2, semantic_field_nouns)
5.2.5. Schreiben der Ergebnisse#
Die Ergebnisse werden als Tabellen (.csv-Dateien) gespeichert, so kann die Erstellung der Abbildungen unbahängig von diesem Notebook nachvollzogen werden und die werden Ergebnisse so nachnutzbar.
result_dir = Path("results/")
if not result_dir.exists():
result_dir.mkdir()
result_path_1 = result_dir / "results-corpus-german_language_fiction_1810-1900_50-per-decade-I.csv"
result_path_2 = result_dir / "results-corpus-german_language_fiction_1810-1900_50-per-decade-II.csv"
freq_1_df.to_csv(result_path_1, index=False)
freq_2_df.to_csv(result_path_2, index=False)
5.2.6. Diskussion der Häufigkeitsanalyse#
Die Analyse der relativen Häufigkeiten zeigt, dass deutschsprachige literarische Texte des 19. Jahrhunderts nicht vermehrt über Luft und Luft-verwandte Begriffe sprechen, sondern der Trend in beiden Subkorpora zwar leicht ansteigt, der Anstieg allerdings nicht maßgeblich ist. Es gibt Ausreißer wie z.B. Das Dorf im Gebirge (von Hofmannsthal) oder Das Schattenspiel. Eine Morgenwanderung von Flaischlen, die beide Natur zum Thema haben. Daraus ließe sich ein Ansatz für weitere Analysen ableiten, die z.B. eine Untersuchung der Naturdarstellung unternehmen könnte.
Es gibt mehrere Deutungsansätze dieses Ergebnisses: Zum einen kann es sein, dass deutschsprachige Literatur die durch die industrielle Revolution herbeigeführte Veränderung der Luftqualität tatsächlich nicht reflektiert. Zum anderen ist es möglich, dass unsere Operationalisierung zu kurz gegriffen ist, z.B. da abnehmende Luftqualität subtiler angedeutet werden könnte, das semantische Feld nicht spezifisch bzw. generell genug ist oder die relative Häufigkeit keine gute Metrik für die Importanz der Thematik ist.