How to: Texte mit Python analysieren Teil 2

Im ersten Teil haben wir das theoretische Grundkonzept von Text Mining erläutert. In diesem Teil wollen wir ein paar Dinge praktisch anwenden und uns mal ansehen, wie man so Informationen an einem konkreten Beispiel aus den Texten herauskristallisieren kann.

Den ersten Teil könnt ihr unter diesem Link finden. Es wird kurz angerissen, was man mit Text Mining machen kann. Welche Machine Learning Verfahren es gibt und wie die grundsätzlichen Pre-Processing Schritte aussehen. Außerdem werden 2 Packages zur Textverarbeitung vorgestellt. In diesem Artikel werden wir nun die folgenden Themen, am Beispiel eines Trump Twitter Datensatzes, behandeln:

  • Erklärung des Datensatzes
  • Erste Untersuchung des  Datensatzes
  • Preprocessing Schritte
  • Gewinnen der ersten Informationen & Erkenntnisse
  • Ausblick auf weitere Analysen



 Erklärung des Datensatzes

Für diesen Artikel habe ich mir einen bestehenden Datensatz von Kaggle rausgesucht. Ihr könnt ihn direkt hier downloaden: Trump Tweets | Kaggle. Er enthält eine Sammlung von allen Trump Tweets über mehrere Jahre verteilt. Dabei besteht der Datensatz aus den folgenden Spalten.

  • id – enthält die ID des tweets im Datensatz
  • link – enthält den Link zum Tweet auf Twitter
  • content – der Textinhalt des Tweets
  • date – Datum wann der Tweet getwittert wurde
  • retweets – Wie häufig der Tweet geretweetet wurde
  • favorites – Wie viele den Tweet als Favorit markiert haben
  • Hashtags – enthält die Hashtags zum jeweiligen Tweet
  • geo – enhält die Geo Daten wenn welche übermittelt wurden

Nach dem Download des Datensatzes und dem Anlegen eines Jupyter Notebooks müssen erstmal die wichtigsten Packages geladen werden.

import pandas as pd
import spacy
from collections import Counter
import matplotlib.pyplot as plt
import seaborn as sns
import re

Pandas werden wir zum einlesen der Daten verwenden. SpaCy ist das Tool um verschiedene NLP Methoden auf den Datensatz anwenden zu können. collections bzw. Counter wird uns helfen die Wörter zu zählen. Für die Visualisierung der Erkenntnisse werden wir vor allem matplotlib und seaborn verwenden. Das RegEx Modul wird benötigt um die Texte zu standardisieren und bestimmte Begriffe zu entfernen.

Wenn ihr mehr über RegEx erfahren wollt. Dann lest euch am besten mal die Dokumentation des Moduls durch: Python RegEx Dokumentation.

Nun müssen wir für Spacy noch ein entsprechendes Sprachmodell laden. Diese könnt ihr über euer Jupyter Lab Terminal downloaden. wie das funktioniert wird auf der SpaCy Webseite erklärt: Spacy.io

Nun könnt ihr euer heruntergeladenes Sprachmodell und die dazugehörigen Stoppwörter laden. Ich habe mir dabei das Sprachmodell en_core_web_md ausgesucht. Danach lade ich die Daten in mein Programm und filtere das Dataframe auf Datensätze die in 2019 und danach gepostet wurden.

nlp = spacy.load('en_core_web_md')
spacy_stopwords = spacy.lang.en.stop_words.STOP_WORDS

# data load and filter on the beginning of 2019
df = pd.read_csv("./archive/trumptweets.csv",sep=',')
df = df[df.date> '2019-01-01']

Prima nun können wir in die Untersuchung unseres Datensatzes einsteigen.



Untersuchung des Datensatzes

als erstes wollen wir uns mal ansehen, wie viele Einträge es in der Spalte Content gibt, wie viele davon Null sind und was der letzte Tweet im Datensatz ist.

# overview about number of tweets
print('Anzahl Datensätze: ', df['content'].count())
print('Anzahl Null Datensätze: ', df['content'].isnull().sum())
print('Datum letzter Tweet: ', df['date'].max())

Wie man sieht haben wir 4.769 Zeilen in unseren Datensatz. Davon sind alle befüllt und keiner hat einen Nullwert. Das ist gut. Dann müssen wir an dieser Stelle schonmal keine Bereinigung vornehmen. Der letzte Tweet wurde am 20.01.2020 um 02:57:49 veröffentlicht. Das bedeutet aus 2020 haben wir eigentlich keine Daten und die Daten beschränken sich auf 2019. Denkt dran, dass wir den Datensatz zu Beginn schon auf alles ab 01.01.2019 eingegrenzt haben. Im nächsten Schritt können wir den content mit dem SpaCy Package bearbeiten und danach den Output ansehen.

# process the data with SpaCy and view a sample
df['doc'] =[nlp(text) for text in df.content]
df.sample(3)

In der Spalte Doc ist nun der Tokenisierte Text eingelesen. Dabei bildet jedes Wort ein eigenes Token, bei dem zusätzliche Informationen integriert sind (z.B. besitzt ein Token eine Information ob es sich um ein Nomen oder Verb handelt. Oder weitere Informationen je nach Sprachmodell das eingesetzt wird). Dann lasst uns doch mal gucken, wie viele Wörter wir in dem Datensatz haben und wie viele davon Unique sind. Ich habe das Ergebnis am Ende noch in eigene Variablen geschrieben, da wir diese in der Visualisierung wieder verwenden wollen.

all_words = []
for text in df.doc:
    wordrow = [word.text for word in text]
    all_words.extend(wordrow)
print ('Number of Words: ', len(all_words))
# word count
word_freq = Counter(all_words)
print ('Numer of Unique Words: ', len(word_freq))

word_count_org = len(all_words)
unique_words_org = len(word_freq)

Was wir sehen können ist, dass der Datensatz 168.987 Wörter in Summe enthält und 13.891 Unique Wörter. Wenn ich mir nun die 50 am häufigsten vorkommenden Wörter betrachte stellen wir fest, dass die meisten Wörter eher wenig Information über den Sinn des Textes geben.

common_words = word_freq.most_common(50)
# plot it for nice overview
temp = pd.DataFrame(common_words)
temp.columns = ['Common_words','count']
temp.style.background_gradient(cmap='Blues')

Hier hilft uns aber nun SpaCy und das RegEx Package dabei, den Text zu standardisieren und die Komplexität zu reduzieren.



Preprocessing Schritte

Jetzt verändern wir  aktiv die Daten in unserem Datensatz. Dazu definieren wir vor allem 3 Funktionen.

  1. clean_text: bereinigt den text von Sonderzeichen, Punkt und Komma, typischen HTML Tags etc. Hier wird vor allem das RegEx Modul angewandt.
  2. remove_stopwords: Entfernt Füllwörter wie „The“ oder „and“ die kaum Informationswert für den Inhalt des Textes haben.
  3. wordcounter: Zählt für jeden Text der übermittelt wird die Anzahl der Wörter
# clean text from emojis and hashtags
def clean_text(text):
    if isinstance((text), (str)):
        text = re.sub('<[^>]*>', '', text)
        text = re.sub('[\d]+', '', text.lower())
        text = re.sub('"', '', text.lower())
        text = re.sub('\[.*?\]', '', text)
        text = re.sub('https?://\S+|www\.\S+', '', text)
        text = re.sub('<.*?>+', '', text)
        text = re.sub('\n', '', text)
        text = re.sub('\w*\d\w*', '', text)
        text = re.sub('\'','', text)
        return text
    if isinstance((text), (list)):
        return_list = []
        for i in range(len(text)):
            temp_text = re.sub('<[^>]*>', '', text[i])
            temp_text = re.sub('[\d]+', '', temp_text.lower())
            temp_text = re.sub('"', '', temp_text.lower())
            temp_text = re.sub('\[.*?\]', '', temp_text)
            temp_text = re.sub('https?://\S+|www\.\S+', '', temp_text)
            temp_text = re.sub('<.*?>+', '', temp_text)
            temp_text = re.sub('\n', '', temp_text)
            temp_text = re.sub('\w*\d\w*', '', temp_text)
            temp_text = re.sub('\'','', temp_text)
            return_list.append(temp_text)
        return(return_list)
    else:
        pass
# Remove stopwords from the text
def remove_stopwords(text):
    wordrow = [word for word in text if word.is_stop != True and word.is_punct != True]
    return wordrow

# get number of words of the given text back
def wordcounter(text):
    freq = len(text)
    return freq

Diese Funktionen rufen wir nun einfach noch mit den folgenden Codezeilen auf und hängen die Ergebnisse ebenfalls an unseren Datensatz an.

df['processed_txt'] = df['content'].apply(lambda x : clean_text(x))
df['processed_txt'] = [nlp(text) for text in df.processed_txt]

df['processed_txt'] = df['processed_txt'].apply(lambda x : remove_stopwords(x))
df['word_numb_orig'] = df['doc'].apply(lambda x : wordcounter(x))
df['word_numb_cleaned'] = df['processed_txt'].apply(lambda x : wordcounter(x))
df.head()

Damit sind wir auch mit dem Preprocessing soweit schon durch und können direkt mit den Visualisierungen starten.



Gewinnen der ersten Informationen & Erkenntnisse

So dann schauen wir uns doch mal zuerst die Ergebnisse vom Pre-Processing an. Zuerst mal die häufigsten Wörter im Datensatz. Hierbei sehen wir, dass die Wörter nun auch schon erste Rückschlüsse auf die Themen bzw. Inhalte geben lassen. Das häufigste Wort das vorkommt ist dabei „great“, was nicht wirklich überraschend ist. Außerdem scheinen die Tweets sehr häufig von den Demokraten zu handeln.

# get all words, word freq and most common words
all_words = []
for text in df.processed_txt:
    wordrow = [word.text for word in text if word.is_stop != True and word.is_punct != True and word.text != ' ']
    all_words.extend(wordrow)
print ('Number of Words: ', len(all_words))
# word count
word_freq = Counter(all_words)
print ('Numer of Unique Words: ', len(word_freq))

word_count_clean = len(all_words)
unique_words_clean = len(word_freq)

common_words = word_freq.most_common(50)
# plot it for nice overview
temp = pd.DataFrame(common_words)
temp.columns = ['Common_words','count']
temp.style.background_gradient(cmap='Blues')

Als nächstes schauen wir uns die Auswirkungen unserer Pre Processing Schritte auf die Komplexität der Texte an. Dies dient einfach nochmal der Verdeutlichung wie stark die Wörter vereinfacht wurden. Dies ist auch wichtig für spätere Machine Learning Algorithmen die wir in einem anderen Artikel darauf anwenden werden.

# Number of words and unique words in dataset
liste = [word_count_org,word_count_clean]
# color palette
clrs = ['green' if (x < max(liste)) else 'red' for x in liste ]
# Creating our own dataframe 
data = {"Label": ['Orig','Cleaned'], 
        "W_Counts": liste} 
p_df = pd.DataFrame(data, columns=['Label', 'W_Counts']) 

# Defining the plot size 
plt.figure(figsize=(8, 8)) 
plots = sns.barplot(x="Label", y="W_Counts", data=p_df, palette = clrs) 
  
# Iterrating over the bars one-by-one 
for bar in plots.patches:  
    plots.annotate(format(bar.get_height(), '.2f'),  
                   (bar.get_x() + bar.get_width() / 2,
                    bar.get_height()), ha='center', va='center', 
                   size=15, xytext=(0, 8),
                   textcoords='offset points') 
# Setting the title for the graph 
plt.title('Wordcount Original vs Cleaned') 
plt.show()

liste = [unique_words_org,unique_words_clean]
# color palette
clrs = ['green' if (x < max(liste)) else 'red' for x in liste ]
# Creating our own dataframe 
data = {"Label": ['Orig','Cleaned'], 
        "W_Counts": liste} 
p_df = pd.DataFrame(data, columns=['Label', 'W_Counts']) 

# Defining the plot size 
plt.figure(figsize=(8, 8)) 
plots = sns.barplot(x="Label", y="W_Counts", data=p_df, palette = clrs) 
  
# Iterrating over the bars one-by-one 
for bar in plots.patches:  
    plots.annotate(format(bar.get_height(), '.2f'),  
                   (bar.get_x() + bar.get_width() / 2,
                    bar.get_height()), ha='center', va='center', 
                   size=15, xytext=(0, 8),
                   textcoords='offset points') 
# Setting the title for the graph 
plt.title('UNIQUE Wordcount Original vs Cleaned') 
plt.show()

Man kann sehen, dass wir die Anzahl der Wörter in den Texten mehr als halbiert haben und gleichzeitig die Komplexität um ca. ein Drittel reduziert haben. Ähnlich sieht es aus wenn wir die durchschnittliche Wörternanzahl je Tweet messen ( Original & Cleaned Text)

# Number of words and unique words in dataset
liste = [df['word_numb_orig'].mean(),df['word_numb_cleaned'].mean()]
# color palette
clrs = ['green' if (x < max(liste)) else 'red' for x in liste ]
# Creating our own dataframe 
data = {"Label": ['Orig','Cleaned'], 
        "W_Counts": liste} 
p_df = pd.DataFrame(data, columns=['Label', 'W_Counts']) 

# Defining the plot size 
plt.figure(figsize=(8, 8)) 
plots = sns.barplot(x="Label", y="W_Counts", data=p_df, palette = clrs) 
  
# Iterrating over the bars one-by-one 
for bar in plots.patches:  
    plots.annotate(format(bar.get_height(), '.2f'),  
                   (bar.get_x() + bar.get_width() / 2,
                    bar.get_height()), ha='center', va='center', 
                   size=15, xytext=(0, 8),
                   textcoords='offset points') 
# Setting the title for the graph 
plt.title('MEAN - Wordcount each Tweet') 
plt.show()

Auch hier lässt sich eine deutliche Reduktion des Wortumfangs erkennen. Nun wollen wir aber zum Schluss auch schonmal inhaltlich gucken ob wir was erkennen können.

# Plot the Timeline number of tweets
serie = df['content'].groupby(pd.to_datetime(df['date']).dt.to_period('M').astype('str')).count()

plt.figure(figsize=(20, 8))
clrs = 'red'
plots = sns.lineplot(data=serie, color=clrs)

plt.title('Timeline Number of tweets per month')
plt.show()

Hier sieht man, dass die Anzahl der Tweets im 3. und 4. Quartal am höchsten war. Das ist erstmal eine Auffälligkeit die darauf hindeutet, dass es hier ein wichtiges Event für Trump gab. Mit den häufigsten Wörtern könnten wir nun schauen ob es Tweets mit diesen Wörtern gibt die mit dem Anstieg korrelieren. Da die komplette Liste etwas unübersichtlich ist habe ich direkt mal die Auffälligsten als Beispiel herausgesucht.

# Calculate for the top common words if it applies in each text
import numpy as np 
def wordchecker(text, checkword):
    erg = np.NaN
    for word in text:
        if word.text == checkword:
            erg = True
            break
    return erg

templist = temp['Common_words'].to_list()

for obj in templist:
    df[obj] = np.NaN

for columnname in df:
    for check in templist:
        if columnname == check:
            # iterate over all rows and check if the word is in it
            df[columnname] = df['processed_txt'].apply(lambda x: wordchecker(x, check))

# Grouping and plotting with most interesting words
serie = df[['witch','impeachment','china','border','wall']].groupby(pd.to_datetime(df['date']).dt.to_period('M').astype('str')).count()
plt.figure(figsize=(25, 15))
clrs = 'red'
plots = sns.lineplot(data=serie, color=clrs, dashes=False)

plt.title('Timeline Number of tweets per month')
plt.show()

Für diese Sicht müssen wir erst einmal für jedes der Top Wörter eine eigene Spalte hinzufügen und dann prüfen ob es in dem jeweiligen Tweet gehört.

Wir sehen deutlich das zum Jahresende der Begriff Impeachment stark zunimmt. Ebenfalls verzeichnet das Wort „witch“ einen leichten Anstieg in den Tweets. Wenn ich „hunt“ mit hinzunehmen würde, sieht man dass diese Worte fast komplett mit einander korrelieren. Gleichzeitig nehmen andere Wörter wie „border“,“wall“ oder „china“ deutlich ab. Es scheint also, dass andere Themen von dem Thema Impeachment verdrängt wurden. Dies sind noch relativ einfache Analysen ohne Machine Learning etc. Was wir uns in weiteren Beiträgen anschauen möchten werde ich nun im letzten Abschnitt nochmal erläutern.

Ausblick auf weitere Analysen

In weiteren Artikeln werde ich auf diesem Datensatz noch weitere Analysen fahren. Der nächste Artikel wird zum Beispiel die Clusterverfahren zum finden von Clustern auf diesen Datensatz anwenden und miteinander vergleichen.

Noch ein weiterer Artikel wird eine Semantik Analyse auf diesen Daten anwenden. Ihr seht also.. das war nur der Anfang und Ich werde mich noch etwas mit den Trump Tweets aus 2019 beschäftigen. Ich hoffe nur, dass ich dazu auch genügend Zeit finde. Bis dahin hoffe ich konnte ich euch ein paar Ansätze für eure eigenen Text Mining Projekte geben.