🚀 Korpusanalyse – Visualisierung der Textkomplexität#

🔔 Feinlernziel(e) dieses Kapitels
Sie können die Konzeption der Analyse beschreiben und andere Möglichkeiten des Korpus-Splitting entwerfen.
Sie können das Konzept eines Balkendiagramms erklären und das erstellte Diagramm interpretieren sowie die Gründe für Ihre Interpretation nennen.

Hinweise zur Ausführung des Notebooks#

Dieses Notebook kann auf unterschiedlichen Levels erarbeitet werden (siehe Abschnitt “Technische Voraussetzungen”):

  1. Book-Only Mode

  2. Cloud Mode: Dafür auf 🚀 klicken und z.B. in Colab ausführen.

  3. Local Mode: Dafür auf Herunterladen ↓ klicken und “.ipynb” wählen.

Übersicht#

Im Folgenden wird die Textkomplexität der einzelnen Pressemitteilungen für unterschiedliche Zeitabschnitte (Monate, Jahre) zusammengefasst und visualisiert.

  1. Einlesen der Tabelle mit den Textkomplexitätsscores

  2. Zusammenfassung für Zeitabschnitte

  3. Ergebnisse in einem Liniendiagramm visualisieren

Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️

Voraussetzungen zur Ausführung des Jupyter Notebooks

  1. Installieren der Bibliotheken
  2. 2. Laden der Daten (z.B. über den Command `wget` (s.u.))
  3. 3. Pfad zu den Daten setzen
Zum Testen: Ausführen der Zelle "load libraries" und der Sektion "Einlesen des Korpus".
Alle Zellen, die mit 🚀 gekennzeichnet sind, werden nur bei der Ausführung des Noteboos in Colab / JupyterHub bzw. lokal ausgeführt.
Hide code cell content
# 🚀 Install libraries
! pip install pandas matplotlib bokeh
Hide code cell content
# import libraries for table processing and for compuation of readability metrics
from pathlib import Path

import pandas as pd
import matplotlib.pyplot as plt

from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.layouts import column, row, layout
from bokeh.models import ColumnDataSource, CustomJS, TextInput, Div, RadioButtonGroup, Switch, TableColumn, DataTable
# Ensure Bokeh output is displayed in the notebook
output_notebook()
Loading BokehJS ...

Einlesen der Textkomplexitätsscores#

Informationen zum Ausführen des Notebooks – Zum Ausklappen klicken ⬇️ Zuerst wird der Ordner angelegt, in dem die Textdateien gespeichert werden. Der Einfachheit halber wird die gleich Datenablagestruktur wie in dem GitHub Repository, in dem die Daten gespeichert sind, vorausgesetzt.
Der Text wird aus GitHub heruntergeladen und in dem Ordner ../data/txt/ abgespeichert.
Der Pfad kann in der Variable text_path angepasst werden. Die einzulesenden Daten müssen die Endung `.txt` haben.
# 🚀 Create result directory path
result_dir = Path(r"../results")
if not result_dir.exists():
    result_dir.mkdir()
Hide code cell content
# 🚀 Load the results file from GitHub 
! wget https://raw.githubusercontent.com/quadriga-dk/Text-Fallstudie-2/refs/heads/main/results/metadata_with_readability_scores.csv -P ../results/
result_path = result_dir / r"metadata_with_readability_scores.csv"
result_df = pd.read_csv(result_path, sep=",")

# Convert date to datetime
result_df['date'] = pd.to_datetime(result_df['date'], dayfirst=True)

Die Anzahl der Einträge anzeigen lassen:

len(result_df)
51826

Erste 100 Zeilen der Daten angucken:

# Too slow if done with entire data frame

# Convert DataFrame to ColumnDataSource
source = ColumnDataSource(result_df[:100])
        
# Create Table Columns
columns = [TableColumn(field=col, title=col) for col in result_df.head().columns]

# Create DataTable
data_table = DataTable(source=source, columns=columns)

# Display DataTable
output_notebook()  # Use this to render in Jupyter Notebook
show(layout([data_table]))
Loading BokehJS ...

Analyse#

Übersicht über die Daten erhalten#

Wie viele Pressemitteilungen gab es pro Jahr?

result_df.groupby(pd.PeriodIndex(result_df['date'], freq="Y"))['n_tokens'].count().plot(kind="bar")
<Axes: xlabel='date'>
../_images/c5ffa11214e069663c6b08c9be9ffd8ad7a7c5d0709589c1a002fe95a8b6cbf8.png

Wie lang sind die Pressemitteilungen im Schnitt in Wörtern?

result_df.groupby(pd.PeriodIndex(result_df['date'], freq="Y"))['n_tokens'].mean().plot(kind="bar")
<Axes: xlabel='date'>
../_images/143803a34483c159013ca48d48c251b86ba08344fd53131f66b8660f3c487c6b.png

Wie viele Mitteilungen aus welcher Kategorie sind vorhanden? Da die Anzahl der Kategorien hoch ist, zeigen wir nur die Kategorien, die mit 100 Mitteilungen oder mehr vertreten sind.

counts = result_df['source'].value_counts()
counts[counts > 100].plot(kind="bar")
<Axes: xlabel='source'>
../_images/c239cb956aa72f7fbd826c8aa59d3f1ef7479d766dd77229c7a734fdbb280bde.png

Extrahieren von schwierigsten und einfachsten Texten#

metrics = ["Flesch", "ARI", "Coleman_Liau", "Wiener_Sachtextformel"]
# TODO: prettify
for metric in metrics:
    print(metric)
    if metric == "Flesch":
        print(f"Schwierigster Text: {result_df[result_df[metric] == result_df[metric].min()]['id']}")
        print(f"Leichester Text: {result_df[result_df[metric] == result_df[metric].max()]['id']}\n")
    else:
        print(f"Schwierigster Text: {result_df[result_df[metric] == result_df[metric].max()]['id']}")
        print(f"Leichester Text: {result_df[result_df[metric] == result_df[metric].min()]['id']}\n")
Flesch
Schwierigster Text: 22932    1139775
Name: id, dtype: int64
Leichester Text: 41925    761243
Name: id, dtype: int64

ARI
Schwierigster Text: 9444    1403106
Name: id, dtype: int64
Leichester Text: 46803    334269
Name: id, dtype: int64

Coleman_Liau
Schwierigster Text: 22932    1139775
Name: id, dtype: int64
Leichester Text: 40337    796065
Name: id, dtype: int64

Wiener_Sachtextformel
Schwierigster Text: 9444    1403106
Name: id, dtype: int64
Leichester Text: 44727    627469
Name: id, dtype: int64

Wie zu sehen ist, überschneiden sich die Metriken in Bezug auf die Extremwerte (niedrigster und höchster Wert) nicht.

Entwicklung über Zeit#

  • Vergleich der Metriken-Entwicklung über die Jahre

metric_mean_year_df = result_df.groupby(pd.PeriodIndex(result_df['date'], freq="Y"))[metrics].mean()
metric_mean_year_df.plot()
<Axes: xlabel='date'>
../_images/1bd60e24934dad6936703e7fe5397922c3d96e5f6c189d1f819e6d87c18b07cb.png
# Create subplots -- attention, they don't start with 0!
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12))
axes = axes.flatten()
for i, col in enumerate(metrics):
    axes[i].plot(metric_mean_year_df.index.year, metric_mean_year_df[col], marker='o', label=col)
    axes[i].set_title(f'{col}')
    axes[i].set_xlabel('Jahre')
    axes[i].set_ylabel(col)
    axes[i].legend()
    axes[i].grid(True)

plt.tight_layout()  # Adjust layout to prevent overlap
plt.show()
../_images/2b4fe5e82d2c2ac9702e8f61cb6b2d7787c5f39c3dc0c4aca103c017ef364516.png

3.2 Interaktive Analysedaten vorbereiten#

Wir wollen die Daten nun nach den Parametern der Metrik und der Granularität der Zeitschiene einteilen.

columns_to_keep = []
columns_to_keep.extend(metrics)
columns_to_keep.append("date")
print(columns_to_keep)
['Flesch', 'ARI', 'Coleman_Liau', 'Wiener_Sachtextformel', 'date']
results_filtered = result_df[columns_to_keep]
Hide code cell content
def plot_with_js(results_filtered_df: pd.DataFrame) -> None:
    """ 
    :param pd.DataFrame merged_df: The merged dataframe of all annotations
    """
    frequency_parameters = ["Y", "M", "W-MON"]
    
    metrics_to_time_frame = {}
    for metric in metrics:
        metrics_to_time_frame[metric] = {}
        for option in frequency_parameters:
            result = results_filtered.groupby(pd.PeriodIndex(results_filtered['date'], freq=option))[metric].mean()
            metrics_to_time_frame[metric][option] = {"x": result.index.to_timestamp(), "y": result.values}
    
    # Set year as default for the plot
    line_source = ColumnDataSource(data=metrics_to_time_frame["Flesch"]["Y"])

    # Create a plot
    p = figure(title=f"Lesbarkeitsmaß", x_axis_type="datetime", x_axis_label='Zeit', 
               y_axis_label='Durschnittlicher Score', width=700, height=400)
    line = p.line('x', 'y', source=line_source, line_width=2, color='blue')

    # RadioButtonGroup to select mode
    radio_button_group_time = RadioButtonGroup(labels=["Yearly", "Monthly", "Weekly"], active=0)
    radio_button_group_metric = RadioButtonGroup(labels=["Flesch", "ARI", "Coleman_Liau", "Wiener_Sachtextformel"], active=0)


    # Callback to update the data based on selected mode
    callback = CustomJS(
        args=dict(
            line=line,
            sources=metrics_to_time_frame,
            radio_button_group_time=radio_button_group_time,
            radio_button_group_metric=radio_button_group_metric
        ),
        code="""                
        // Access the value of the switch
        // const sources = switch_element.active ? relative_sources : absolute_sources;

        // Access the value of the RadioButtonGroup Time
        const mode = radio_button_group_time.active;
        
        // Access the value of the RadioButtonGroup
        const base = radio_button_group_metric.active;
    
        // Retrieve the selected frequency
        const freq = ["Y", "M", "W-MON"][mode];

        // Retrieve the selected metric
        const metric = ["Flesch", "ARI", "Coleman_Liau", "Wiener_Sachtextformel"][base];
    
        // update data source and emit change event
        line.data_source.data = sources[metric][freq];
        line.data_source.change.emit();
    """,
    )

    # Attach the callback to both widgets
    radio_button_group_time.js_on_change('active', callback)
    radio_button_group_metric.js_on_change('active', callback)

    # Layout the RadioButtonGroup and plot
    layout = column(row(radio_button_group_metric, radio_button_group_time), p)
    show(layout)
plot_with_js(results_filtered)