Oft wird ein großer Teil der Projektzeit nicht für die spannenden Modelle, sondern für die meist etwas weniger spannend empfundene Datenaufbereitung verwendet. Ein typischer Stolperstein dabei ist die Codierung von Textantworten (offene Nennungen). Wie können wir uns diese Arbeit mit R erleichtern?
Anhand eines einfachen Beispiels („Warum treiben Sie Sport?“) beginnen wir mit einer Zuordnung der Antworten zu festen Kategorien mittels stringr::str_detect. Im Folgenden verbessern wir die erste Lösung schrittweise, indem wir Unterschiede in der Groß- und Kleinschreibung berücksichtigen, den Code mit einer eigenen Funktion straffen und schließlich einen kleinen Blick auf die mächtigen regulären Ausdrücke werfen. Den R-Code finden Sie auch hier auf github.
Datenbeispiel: Warum treiben Sie Sport?
Nehmen wir an, es wurde gefragt, warum jemand Sport treibt. Hier einige Antworten (alphabetisch sortiert, mit ID):
22 | Aus verschiedenen Gründen |
12 | Ausdauer verbessern |
4 | Aussehen |
3 | besser aussehen |
23 | Diverse Gründe |
17 | fitness |
9 | Fitness |
18 | formaufbau |
14 | Formaufbau |
8 | gesund bleiben |
2 | Ich will gut aussehen |
5 | Ich will Muskeln aufbauen |
13 | In Form kommen |
24 | Mehrere Gründe |
1 | Muskelaufbau |
19 | Muskeltraining |
6 | muskulär verbessern |
7 | Prävention |
10 | Reha |
15 | Reha-Training |
16 | Rehabilitation |
20 | Rückenprävention |
21 | Rückenschmerzen vorbeugen |
11 | will ich ausdauernder werden |
Vorgegebene Kategorien zuordnen mit stringr
Wir greifen auf das R-Paket stringr zurück, das zum tidyverse gehört – einer Sammlung von Paketen, die gut auf einander abgestimmt sind und die Datenaufbereitung, Visualisierung und Analyse wesentlich erleichtern. stringr unterstützt die Orientierung über die verfügbaren Funktionen: sie beginnen mit str_ – somit kann man in RStudio über ?str_ ein Kontextmenü erhalten, das die Funktionen auflistet.
Nehmen wir an, wir haben uns schon ein paar Gedanken gemacht (das ist bei der Textanalyse häufig unvermeidlich) und uns Kategorien überlegt, in die die Antworten zusammengefasst werden sollen:
Ausdauer |
Aussehen |
Gesundheit / Prävention allg. |
Muskeln aufbauen |
Fitness / Form allg. |
Rehabilitation |
Rückenschmerzen vorbeugen / lindern |
Sonstiges |
Es liegt nahe, nach Stichworten zu suchen und die entsprechende Zuordnung damit vorzunehmen. Hier ein Code-Beispiel:
library(tidyverse) daten <- daten %>% mutate(warum_sport_c = case_when( str_detect(warum_sport, "Ausdauer") ~ "Ausdauer", str_detect(warum_sport, "Aussehen") ~ "Aussehen", str_detect(warum_sport, "Fitness") ~ "Fitness / Form allg.", str_detect(warum_sport, "Form") ~ "Fitness / Form allg.", str_detect(warum_sport, "gesund") ~ "Gesundheit / Prävention allg.", str_detect(warum_sport, "Prävention") ~ "Gesundheit / Prävention allg.", str_detect(warum_sport, "Muskel") ~ "Muskeln aufbauen", str_detect(warum_sport, "Reha") ~ "Rehabilitation", str_detect(warum_sport, "Rücken") ~ "Rückenschmerzen vorbeugen / lindern", TRUE ~ "Sonstiges") )
Wer das sonderbar anmutende Zeichen %>% noch nicht kennt, kann hier mehr darüber erfahren.
Mit str_detect() können wir nach Zeichenketten suchen. Mit der dplyr-Funktion mutate() wird eine neue Variable erstellt: warum_sport_c. Das „c“ am Ende soll für „categories“ – die kategorisierte Version der offenen Antworten – stehen. case_when stammt ebenfalls aus dplyr und ermöglicht uns eine Zuordnung in Abhängigkeit der gefundenen Stichworte.
Was haben wir damit erreicht?
id | warum_sport | warum_sport_c |
---|---|---|
12 | Ausdauer verbessern | Ausdauer |
4 | Aussehen | Aussehen |
9 | Fitness | Fitness / Form allg. |
14 | Formaufbau | Fitness / Form allg. |
13 | In Form kommen | Fitness / Form allg. |
8 | gesund bleiben | Gesundheit / Prävention allg. |
7 | Prävention | Gesundheit / Prävention allg. |
5 | Ich will Muskeln aufbauen | Muskeln aufbauen |
1 | Muskelaufbau | Muskeln aufbauen |
19 | Muskeltraining | Muskeln aufbauen |
10 | Reha | Rehabilitation |
15 | Reha-Training | Rehabilitation |
16 | Rehabilitation | Rehabilitation |
20 | Rückenprävention | Rückenschmerzen vorbeugen / lindern |
21 | Rückenschmerzen vorbeugen | Rückenschmerzen vorbeugen / lindern |
22 | Aus verschiedenen Gründen | Sonstiges |
3 | besser aussehen | Sonstiges |
23 | Diverse Gründe | Sonstiges |
17 | fitness | Sonstiges |
18 | formaufbau | Sonstiges |
2 | Ich will gut aussehen | Sonstiges |
24 | Mehrere Gründe | Sonstiges |
6 | muskulär verbessern | Sonstiges |
11 | will ich ausdauernder werden | Sonstiges |
Ein guter Anfang, aber nicht perfekt:
- Groß- und Kleinschrift wird nicht beachtet. Während „Aussehen“ und „Fitness“ korrekt zugeordnet werden, gelingt das bei „aussehen“ und „fitness“ nicht.
- „muskulär“ statt „Muskel“ wird nicht zugeordnet
- Für „gesund“ und „Prävention“, die in der gleichen Kategorie landen sollen, benötigen wir zwei separate Codezeilen, ebenso für „Fitness“ und „Form“.
Erste Verbesserung: Groß- und Kleinschrift
Bisher haben wir der str_detect-Funktion als zweites Argument das jeweilige Stichwort übergeben. Dieses wird als regulärer Ausdruck interpretiert – dazu gleich mehr. Hier spielte es keine Rolle, da wir nur „normalen“ Text verwendeten. Wir können stringr sagen, dass es unseren Text „wörtlich“ nehmen soll, also nicht auf Sonderzeichen der regulären Ausdrücke untersuchen. Dazu stehen uns die Funktionen fixed (schnell und ungenau) und coll (berücksichtigt Gebietsschema, daher genauer und langsamer) zur Verfügung. Wir nehmen coll und nutzen die Möglichkeit, zusätzliche Argumente anzugeben, hier vor allem: ignore_case = TRUE. Das bedeutet: Ignoriere Unterschiede in der Groß- und Kleinschreibung.
Code-Beispiel:
daten <- daten %>% mutate(warum_sport_c = case_when( str_detect(warum_sport, "Ausdauer") ~ "Ausdauer", str_detect(warum_sport, coll("Aussehen", ignore_case = TRUE, locale = "de_DE")) ~ "Aussehen", str_detect(warum_sport, "Fitness") ~ "Fitness / Form allg.", str_detect(warum_sport, "Form") ~ "Fitness / Form allg.", str_detect(warum_sport, "gesund") ~ "Gesundheit / Prävention allg.", str_detect(warum_sport, "Prävention") ~ "Gesundheit / Prävention allg.", str_detect(warum_sport, "Muskel") ~ "Muskeln aufbauen", str_detect(warum_sport, "Reha") ~ "Rehabilitation", str_detect(warum_sport, "Rücken") ~ "Rückenschmerzen vorbeugen / lindern", TRUE ~ "Sonstiges" ) )
Eine alternative Lösung besteht darin, die Antworten vor der Zuordnung zu Kategorien durchgängig in Kleinschrift (weniger üblich: Großschrift) umzuwandeln. Das geht mittels der Base R-Funktionen tolower() bzw. toupper() für Großschrift.
Ich habe die Änderung nur beim Stichwort „Aussehen“ implementiert. Um sie durchgängig zu verwenden, müsste ich sie in weiteren, ggf. allen Zeilen einbauen. Das ist mir zu umständlich.
Zweite Verbesserung: Eigene Funktion
Eleganter ist es, eine eigene Funktion zu schreiben. Damit vermeiden wir Code-Duplikation, was nicht nur angenehmer zu schreiben und kürzer ist, sondern den Code auch leichter pflegbar macht: Eine künftige Anpassung muss nur an einer Stelle vorgenommen werden anstatt in vielen einzelnen Zeilen.
Der Code sieht jetzt so aus:
str_detect_sport <- function(Muster) { str_detect(daten$warum_sport, coll(Muster, ignore_case = TRUE, locale = "de_DE")) } daten$warum_sport_c <- NA daten <- daten %>% mutate(warum_sport_c = case_when( str_detect_sport("Ausdauer") ~ "Ausdauer", str_detect_sport("Aussehen") ~ "Aussehen", str_detect_sport("Fitness") ~ "Fitness / Form allg.", str_detect_sport("Form") ~ "Fitness / Form allg.", str_detect_sport("gesund") ~ "Gesundheit / Prävention allg.", str_detect_sport("Prävention") ~ "Gesundheit / Prävention allg.", str_detect_sport("Muskel") ~ "Muskeln aufbauen", str_detect_sport("Reha") ~ "Rehabilitation", str_detect_sport("Rücken") ~ "Rückenschmerzen vorbeugen / lindern", TRUE ~ "Sonstiges" ) )
Der Code für die Zuordnung ist jetzt kürzer. Nur oben in unserer neuen Funktion str_detect_sport steht ein Mal das Argument ignore_case = TRUE. Nun werden alle Stichworte erkannt, unabhängig von Unterschieden in der Groß- und Kleinschreibung:
12 | Ausdauer verbessern | Ausdauer |
11 | will ich ausdauernder werden | Ausdauer |
4 | Aussehen | Aussehen |
3 | besser aussehen | Aussehen |
2 | Ich will gut aussehen | Aussehen |
17 | fitness | Fitness / Form allg. |
9 | Fitness | Fitness / Form allg. |
18 | formaufbau | Fitness / Form allg. |
14 | Formaufbau | Fitness / Form allg. |
13 | In Form kommen | Fitness / Form allg. |
8 | gesund bleiben | Gesundheit / Prävention allg. |
7 | Prävention | Gesundheit / Prävention allg. |
20 | Rückenprävention | Gesundheit / Prävention allg. |
5 | Ich will Muskeln aufbauen | Muskeln aufbauen |
1 | Muskelaufbau | Muskeln aufbauen |
19 | Muskeltraining | Muskeln aufbauen |
10 | Reha | Rehabilitation |
15 | Reha-Training | Rehabilitation |
16 | Rehabilitation | Rehabilitation |
21 | Rückenschmerzen vorbeugen | Rückenschmerzen vorbeugen / lindern |
22 | Aus verschiedenen Gründen | Sonstiges |
23 | Diverse Gründe | Sonstiges |
24 | Mehrere Gründe | Sonstiges |
6 | muskulär verbessern | Sonstiges |
Dritte Verbesserung: Reguläre Ausdrücke
Reguläre Ausdrücke (engl. regular expressions) sind ein mächtiges, flexibles Werkzeug, um Muster in Texten zu finden. Darin sind Sonderzeichen für bestimmte Kriterien definiert, z. B. Kennzeichen für den Textanfang, das Textende, Verallgemeinerungen für „Zahlen“ oder „Buchstaben“, Möglichkeiten, optionale Zeichen anzugeben („0 mal oder öfter“, „1 mal oder öfter“, „3 bis 5 Wiederholungen“ etc.). Eine gute Einführung findet Ihr hier sowie in einer Vignette des stringr-Pakets, erreichbar über help(package = „stringr“) bzw. vignette(„regular-expressions“, package = „stringr“).
Wir wollen es nicht zu kompliziert treiben – wir nutzen die regulären Ausdrücke nur, um zwei Herausforderungen von oben zu lösen:
- Für die Alternativen „gesund“ / „Prävention“ sowie „Fitness“ / „Form“ haben wir jeweils zwei Codezeilen gebraucht – das geht eleganter
- Wir haben „Muskel“ zugeordnet, aber bisher nicht „muskulär“
Neuer Code:
str_detect_regex <- function(Muster) { str_detect(daten$warum_sport, regex(Muster, ignore_case = TRUE)) } daten$warum_sport_c <- NA daten <- daten %>% mutate(warum_sport_c = case_when( str_detect_regex("Ausdauer") ~ "Ausdauer", str_detect_regex("Aussehen") ~ "Aussehen", str_detect_regex("(Fitness|Form)") ~ "Fitness / Form allg.", str_detect_regex("Rücken") ~ "Rückenschmerzen vorbeugen / lindern", str_detect_regex("(gesund|Prävention)") ~ "Gesundheit / Prävention allg.", str_detect_regex("Musk(e|u)l") ~ "Muskeln aufbauen", str_detect_regex("Reha") ~ "Rehabilitation", TRUE ~ "Sonstiges" ) )
Die entscheidenden Änderungen sind fett hervorgehoben. Statt coll() verwenden wir regex(). Wir nutzen hier nur eine der vielen Möglichkeiten der regulären Ausdrücke: Alternativen anzugeben. Innerhalb runder Klammern kann man mit dem Oder-Zeichen „|“ Alternativen spezifizieren. So benötigen wir weniger Codezeilen (Fitness / Form, gesund / Prävention) und können bequem sowohl „Muskel“ als auch „muskulär“ finden.
Ausblick: Texte zuordnen auf Basis von Ähnlichkeiten
Beim nächsten Mal sehen wir uns eine Möglichkeit an, Textantworten automatisch zu codieren, wenn sie viele abweichende Schreibweisen enthalten. Das könnte man umständlich mit regulären Ausdrücken lösen. Einfacher ist es, Texte auf Basis von Ähnlichkeiten zuzuordnen, nach der Idee der Korrelation: Ordne die Textantwort der Kategorie zu, mit der die Schreibweise am stärksten korreliert. Klingt spannend?
Den R-Code zu diesem Thema finden Sie hier zum Download.
Wenn Sie tiefer in das Text Mining mit R einsteigen wollen, empfehle ich das Werk von Julia Silge und David Robinson (der u. a. auch gganimate initiiert hat, das von Thomas Pederson weitergeführt wurde):
Text Mining With R: A Tidy Approach
Und nun viel Erfolg bei Ihrer Datenaufbereitung!
Hab wieder was dazu gelernt – danke, Wolf! 🙂
Warte auf Teil 2: Automatische Zuordnung auf Basis von Text-Ähnlichkeiten. Demnächst in diesem Theater.
Teil 2 ist online! Textantworten (offene Nennungen) automatisch zuordnen in R nach Ähnlichkeit
https://statistik-dresden.de/textantworten-offene-nennungen-automatisch-zuordnen-in-r-nach-aehnlichkeit/