4  Daten transformieren

Bevor wir mit den Rohdaten aus einer Erhebung arbeiten können, sind oft verschiedene Aufbreitungs- oder Transformationsschritte notwendig. Auf die wichtigsten gehen wir im Folgenden am Beispiel des Telegram-Datensatzes ein.

Wir laden das tidyverse-Paket und öffnen den Datensatz der Telegram-Inhaltsanalyse.

library(tidyverse)
tg <- read_tsv("data/telegram.tsv")

4.1 Fälle auswählen

Für manche Auswertungen wollen wir vielleicht nicht alle Fälle einbeziehen, sondern unseren Datensatz filtern, d.h. nur die Fälle behalten, die uns für die konkrete Auswertung interessieren. Um von einem Datensatz bestimmte Fälle auszuwählen, gibt es die filter()-Funktion im tidyverse. Nach dem Einsatz von filter() verbleiben nur diejenigen Fälle im Datensatz, die TRUE auf die angewandte logische Operation zurückgeben. Logische Operationen sind > (größer als), >= (größer gleich als), == (ist gleich), != (ist nicht gleich), < (kleiner als) und <= (kleiner gleich als).

Der Telegram-Datensatz enthält in der Variable ideologie Informationen über die ideologische Grundausrichtung des Kanals. Wollen wir unsere Analyse auf das Querdenker-Milieu beschränken, können wir den Datensatz entsprechend filtern. Da es sich bei ideologie um eine character-Variable, also eine Variable mit Text handelt, sollte der Wert für die Filteroperation ebenfalls von diesem Datentyp sein. Deshalb muss der Wert mit Anführungszeichen ” ” versehen werden.

Da durch die filter()-Funktion die anderen Fälle aus dem Datensatz gelöscht werden, ist es wichtig, beim Filtern immer einen neuen Datensatz zu erzeugen, damit keine Daten verloren gehen.

Dies legen wir im folgenden Beispiel fest, indem wir einen neuen Datensatz tg_querdenker anlegen, der nur die Fälle enthält, auf die die folgende Filterbedingung zutrifft. In tg verbleiben so alle Fälle.

tg_querdenker <- tg |>
  filter(ideologie == "Querdenken") # Achtung, zwei Gleichheitszeichen!
Wann ein und wann zwei =?

== Testet auf Gleichheit, prüft also, ob - in diesem Beispiel - in ideologie “Querdenken” steht.

= Weist einem Objekt etwas zu, wie im Beispiel von Funktionen, in denen wir z.B. angeben, dass X = 4 ist.

Die Anzahl der Variablen bleibt bei der Anwendung von filter() unberührt, wie im Output von dim() ersichtlich ist.

dim(tg)
[1] 11405   125
dim(tg_querdenker)
[1] 2630  125

Lediglich die Anzahl der Zeilen hat sich von 11405 auf 2630 reduziert. Ob der neue Datensatz auch wirklich nur noch Querdenker-Kanäle umfasst, lässt sich schnell mit count() feststellen.

tg_querdenker |>
  count(ideologie)
# A tibble: 1 × 2
  ideologie      n
  <chr>      <int>
1 Querdenken  2630

Wir sehen, dass es in der Variable ideologie nur noch eine Ausprägung gibt, nämlich Querdenken.

filter() erlaubt es auch, komplexere Operationen miteinander zu verbinden. Dazu ist es lediglich nötig, sie durch ein & (für UND-Verküpfungen) oder | (für ODER-Verknüpfungen) zu trennen. Wir können auch beliebige filter()-Befehle in einer Pipe nacheinander durchlaufen lassen. Nachfolgend behalten wir lediglich die rechten oder Querdenken-bezogenen Telegram Kanäle, die zu Offlinehandlungen aufrufen, welche in der Variable ziel_1 codiert sind. Alle Offlinehandlungen haben einen numerischen Code größer gleich (>=) 6 (siehe hierzu die entsprechende Dokumentation des Datensatzes).

tg_rechte_quer_offline <- tg |>
  filter(ideologie == "Rechte" | ideologie == "Querdenken") |>
  filter(ziel_1 >= 6)
dim(tg_rechte_quer_offline)
[1] 925 125

Wieder bleibt die Zahl der Spalten (also Variablen) gleich, aber die Zahl der Zeilen reduziert sich weiter auf 925.

4.2 Variablen auswählen

Die analoge Operation zu filter() auf Spaltenebene ist select(). Die Variablen können einfach auf Basis ihres Namens ausgewählt werden. Die Reihenfolge innerhalb der select-Funktion bestimmt ebenfalls die Reihenfolge der Variablen im neuen Objekt, d.h. die erstgenannte Variable bildet auch die erste Spalte im neuen Datensatz.

tg_ideologie <- tg |>
  select(ideologie, monat)
tg_ideologie
# A tibble: 11,405 × 2
  ideologie  monat  
  <chr>      <chr>  
1 Querdenken 2022-12
2 Querdenken 2022-12
3 Querdenken 2022-12
4 Querdenken 2022-12
5 Querdenken 2022-12
# ℹ 11,400 more rows

Der nun verkleinerte Datensatz enthält nur noch 2 statt 125 Spalten bzw. Variablen.

Ein vorangestelltes Minus (-) erlaubt es wiederum, Variablen zu entfernen, d.h. alle anderen Variablen bleiben erhalten.

tg_no_ideology <- tg |>
  select(-ideologie)
dim(tg_no_ideology)
[1] 11405   124

Bislang haben wir nur einige wenige Variablen bearbeitet. Dieser Ansatz stößt bei größeren Datensätzen schnell an seine Grenzen, da häufig zahlreiche Variablen verändert werden müssen. Das tidyverse bietet dafür sogenannte tidy selections, die es ermöglichen, mehrere Spalten gleichzeitig zu bearbeiten. Dafür werden sogenannte selection helpers genutzt. Diese Helferfunktionen erlauben es, Variablen auf Basis ihrer Position (bspw. die letzte Spalte) oder gewisser Zeichenfolgen (bspw. x1, x2) auszuwählen. Häufig verwendet werden:

  1. contains() = Die Variable enthält irgendwo eine festgelegte Zeichenfolge (bspw. “ziel”)
  2. starts_with() = Der Variablennamen beginnt mit einer festgelegten Zeichenfolge (bspw. “elite”)
  3. ends_with() = Der Variablennamen endet mit einer festgelegten Zeichenfolge (bspw. “1”)
  4. where() = Variablen, die auf eine Funktion den Wert TRUE zurückgeben. Diese Funktion wird oft mit einer Abfrage des Datentypes verbunden (etwa mit is_numeric()), um nur Spalten dieses Datentypes zu verändern.

In unserem Fall können wir zusätzlich zur Variable ideologie mit starts_with("elite") alle Spalten auswählen, die mit dem Wort elite beginnen. Dabei ist zu bedenken, dass R case sensitive ist, d.h. die Groß- und Kleinschreibung wird bei der Auswahl beachtet.

tg |>
  select(ideologie, starts_with("elite")) |>
  names()
 [1] "ideologie"      "elite_string"   "elite_string_1" "elite_string_2"
 [5] "elite_string_3" "elite_string_4" "elite_string_5" "elite_na"      
 [9] "elite_pol"      "elite_media"    "elite_econom"   "elite_science" 
[13] "elite_cult"    

4.3 Daten sortieren

Um den Datensatz zu sortieren, bietet das tidyverse die arrange()-Funktion. Die Standardeinstellung ist, den Datensatz aufsteigend zu sortieren. Für eine absteigende Sortierung muss der Variablenname in die Funktion desc() eingefügt werden. Damit können wir uns beispielsweise die neuesten Telegram-Posts nach Monat in unserem Datensatz anzeigen lassen.

tg_ideologie |>
  arrange(desc(monat))
# A tibble: 11,405 × 2
  ideologie  monat  
  <chr>      <chr>  
1 Querdenken 2022-12
2 Querdenken 2022-12
3 Querdenken 2022-12
4 Querdenken 2022-12
5 Querdenken 2022-12
# ℹ 11,400 more rows

Die meisten Datentransformationen und alle Datenanalysen ignorieren die Reihenfolge der Datenzeilen, daher verwenden wir arrange() meist nur als letzten Schritt um das Lesen der Ergebnisse zu erleichtern, z.B wenn wir Häufigkeitstabellen nach den Häufigkeiten sortieren.

4.4 Fehlende Werte definieren

Bevor wir mit Datentransformationen wie dem Erstellen oder Umcodieren von Variablen beginnen, ist es sinnvoll, sich zunächst mit dem Thema Fehlende Werte zu befassen. Fehlende Werte sind solche, die bei den Auswertungen nicht berücksichtigt werden sollen. Der einfachste Fall ist, dass die entsprechende Zelle einfach leer ist, in R ist sie dann als NA gekennzeichnet. In einigen Fällen - sowohl bei Inhaltsanalyse-Daten als auch bei Befragungs-Daten - gibt es aber auch fehlende Werte, denen zunächst einmal ein Code zugewiesen wurde. In unserem Telegram-Datensatz sind dies z.B. die Werte -1 und -9 (siehe Dokumentation des Datensatzes). Bei Befragungs-Studien wird den Befragten z.B. teilweise als explizite Antwortoption “Weiß nicht/keine Angabe” angeboten und dann ebenfalls mit -9 oder einem anderen Zahlenwert hinterlegt.

Diese Werte sollten in R als NA (fehlender Wert) gekennzeichnet werden, da ansonsten bei der Anwendung von Funktionen Fehler entstehen können, wie dieses simple Beispiel verdeutlicht:

  • In der ersten Zeile ist -9 eigentlich ein fehlender Wert (z.B. Weiß nicht/keine Angabe), das weiß R aber nicht.

  • In der zweiten Zeile ist der einzige Unterschied, dass nun statt -9 NA angegeben wird, also ein fehlender Wert.

kein_na <- c(1, -9)
mit_na <- c(1, NA)
sum(kein_na)
[1] -8

Summieren wir nun den Vektor kein_na auf, sehen wir direkt, dass das Ergebnis inhaltlich falsch ist. Es sollte 1 statt -8 lauten, da die -9 inhaltlich ein fehlender Wert ist. Nun schauen wir uns an, was passiert, wenn wir den Vektor mit_na aufsummieren.

sum(mit_na)
[1] NA

Das produziert auch nicht das gewünschte Ergebnis, der Output ist NA. Das liegt daran, dass viele Funktionen nicht damit umgehen können, wenn es fehlende Werte gibt. Oft müssen wir dann über ein Funktionsargument angeben, dass fehlende Werte bei der Auswertung ignoriert werden sollen. Bei der sum()-Funktion (und vielen anderen tidyverse-Funktionen) lautet das zugehörige Argument na.rm = TRUE. Spezifizieren wir dies, kommt das richtige Ergebnis raus:

sum(mit_na, na.rm = TRUE)
[1] 1

Bei importierten Datensätzen sollten wir also als erstes prüfen, ob es fehlende Werte gibt, die als Zahlenwerte importiert wurden und diese dann als NAs codieren. Hierfür können wir die mutate()-Funktion verwenden. Diese kann für eine ganze Reihe an Datentransformationen verwendet werden und wird sowohl in den folgenden Kapiteln dieses Abschnitts als auch im Rest des Seminars immer wieder auftauchen. Sie kann entweder dazu genutzt werden, um neue Variablen zu generieren oder bestehende Variablen zu überschreiben. Die verwendete Syntax ist denkbar einfach und besteht aus mutate(neuer_name = bestehende_variable).

Wollen wir in einer Variable z.B. die -9 als fehlenden Wert definieren, geht dies wie folgt:

tg <- tg |>
  mutate(art_1 = if_else(art_1 == -9, NA, art_1))

Die mutate()-Funktion ist in diesem Fall kombiniert mit einer if_else-Funktion. Diese ist in vielen Kontexten sehr hilfreich. Sie prüft, ob eine Bedingung erfüllt (true) oder nicht erfüllt (false) ist. Sie ist so aufgebaut, dass als erstes formuliert wird, wann die Bedingung erfüllt ist. In unserem Fall ist die Bedingung erfüllt (d.h. art_1 enthält den als fehlend zu definierenden Wert), wenn art_1 == -9.

Nach dem ersten , folgt, welchen Wert die neue Variable erhält, wenn die Bedingung zutrifft. Das ist bei uns NA. Nach dem zweiten , steht, welchen Wert die neue Variable erhält, wenn die Bedingung nicht zutrifft. Das ist bei uns der Wert, den die Original-art_1 enthält, dieser wird also in die neue Variable übernommen.

In diesem Fall können wir die Original-Variable überschreiben, ansonsten müssen wir vor dem = einen neuen Namen vergeben.

if_else

Die if_else()-Funktion ist gerade mit numerischen Werten zu Beginn nicht einfach zu lesen. Die Funktion lässt sich aber ebenfalls für Text-Daten (und weitere Datentypen) verwenden, die besser illustrieren, was die Funktion macht. Als Beispiel nutzen wir einen fiktiven Datensatz tiere, der die Spalte laute enthält, worin festgehalten ist, ob ein Tier bellt oder nicht. Mit if_else können wir nun festlegen, dass ein bellendes Tier in der neuen Spalte art ein Hund (Wert nach dem ersten ,) ist. Trifft dies nicht zu, ist es kein Hund (Wert nach dem zweiten ,).

tiere |>
  mutate(art = if_else(laute == "bellt", "Hund", "Kein Hund"))

Die if_else()-Funktion können wir nun auch für mehrere Variablen gleichzeitig verwenden. Wir nehmen alle art_-Variablen. Hierfür werden diese mit , abgetrennt oder mehrere mutate() Befehle nacheinander in einer Pipe verbunden.

tg <- tg |>
  mutate(
    art_1 = if_else(art_1 == -9, NA, art_1),
    art_2 = if_else(art_2 == -9, NA, art_2),
    art_3 = if_else(art_3 == -9, NA, art_3)
  )

Ein Blick ins Codebuch verrät, dass auch der Wert -1 eigentlich als fehlend zu behandeln ist. Spätestens jetzt fällt uns auf, dass es eine gute Idee war, fehlende Werte mit negativen Zahlen zu markieren, denn das spart uns die Mühe, verschiedene Missing-Varianten einzeln zu spezifizieren. Stattdessen setzen wir alle negativen Zahlen auf NA, und das gleich für alle art_ Variablen, die wir einfach überschreiben.

tg <- tg |>
  mutate(
    art_1 = if_else(art_1 < 0, NA, art_1),
    art_2 = if_else(art_2 < 0, NA, art_2),
    art_3 = if_else(art_3 < 0, NA, art_3)
  )

In Kombination mit mutate(), across() und selection helpers können schnell fehlende Werte in zahlreichen Spalten gesetzt werden. across() sorgt dafür, dass wir eine Funktion auf mehrere Spalten anwenden können. Die zu verwendende Funktion muss innerhalb von across() mit ~ markiert werden. Zudem benötigen wir den sogenannten tidy dot ., der als Platzhalter für die Variablennamen dient, d.h. im Kern setzt die Funktion nacheinander alle ausgewählten Variablennamen für den tidy dot ein und wendet dann die Funktion auf die Variable an.

tg <- tg |> mutate(across(
  where(is.numeric), # selection helper: alle numerischen Variablen
  ~ if_else(. < 0, NA, .) # ~ plus unsere Recodier-Funktion
))

Sofern wir nicht eine Konvention zur Benennung der bearbeiteten Variablen vorgeben, werden einfach die bestehenden Variablennamen verwendet. Dies führt dazu, dass die bestehenden Variablen einfach überschrieben werden.

4.5 Variablen erstellen

Auch für das Erstellen neuer Variablen kommt die mutate()-Funktion zum Einsatz. Eine einfache Anwendung für mutate() zur Erzeugung einer neuen Variable ist z.B. die Umrechnung von Zahlenwerten. Wenn wir einen Datensatz mit Gewichtsangaben in Gramm haben, können wir eine neue Gewichtsvariable in kg erstellen.

tibble(gewicht = c(1000, 12000, 1250, 2500)) |>
  mutate(gewicht_in_kg = gewicht / 1000)
# A tibble: 4 × 2
  gewicht gewicht_in_kg
    <dbl>         <dbl>
1    1000          1   
2   12000         12   
3    1250          1.25
4    2500          2.5 

Auch hier können wir gleichzeitig mehrere Variablen erstellen, indem wir diese mit , abtrennen oder mehrere mutate()-Befehle nacheinander aufrufen.

tibble(gewicht = c(1000, 12000, 1250, 2500)) |>
  mutate(
    gewicht_in_kg = gewicht / 1000,
    gewicht_in_lbs = gewicht / 453.6 # Gewicht in amerik. Pfund
  )
# A tibble: 4 × 3
  gewicht gewicht_in_kg gewicht_in_lbs
    <dbl>         <dbl>          <dbl>
1    1000          1              2.20
2   12000         12             26.5 
3    1250          1.25           2.76
4    2500          2.5            5.51

4.6 Variablen recodieren

Ein häufiger Anwendungsfall für mutate() ist das Recodieren von Variablen, bei dem einzelne Ausprägungen zusammengefasst oder durch andere Werte ersetzt werden (siehe auch Index und Skalenbildung und Häufigkeiten und Kreuztabellen für weitere Anwendungen).

Beispielsweise könnten wir uns dafür interessieren, ob eine Nachricht einen Appell/eine Handlungsaufforderung enthält (Variable art_1). Im Datensatz wird hierbei unterschieden zwischen einer impliziten (als 1 codiert), einer expliziten (als 2 codiert) sowie keiner (als 3 codiert) Aufforderung. Die Unterscheidung in explizite und implizite Handlungsaufforderung ist für uns aber (aktuell) nicht relevant. Wir wollen also eine sogenannte Dummy-Variable erstellen mit den Ausprägungen “Handlungsaufforderung” (entspricht den Codes 1 und 2 in der Ausgangsvariable art_1) und “keine Handlungsaufforderung” (entspricht dem Code 3 in der Ausgangsvariable art_1).

tg <- tg |>
  mutate(art_1rec = if_else(art_1 == 1 | art_1 == 2, "Handlungsaufforderung", "keine Handlungsaufforderung"))

Auch hier ist die mutate()-Funktion kombiniert mit einer if_else-Funktion. In unserem Fall hier ist die Bedingung erfüllt (d.h. es gab eine Handlungsaufforderung), wenn art_1 also den Code 1 oder den Code 2 enthält (wir erinnern uns | bedeutet oder), wenn es also eine Handlungsaufforderung gab, egal ob implizit (Code 1) oder explizit (Code 2).

Nach dem ersten , folgt, welchen Wert die neue Variable erhält, wenn die Bedingung zutrifft. Das ist bei uns die “Handlungsaufforderung”. Nach dem zweiten , steht, welchen Wert die neue Variable erhält, wenn die Bedingung nicht zutrifft. Das ist bei uns “keine Handlungsaufforderung”.

Etwas einfacher können wir den Code halten, wenn wir - wie in diesem Fall - auch das <= verwenden.

tg <- tg |>
  mutate(art_1rec = if_else(art_1 <= 2, "Handlungsaufforderung", "keine Handlungsaufforderung"))

Nun haben wir aber nicht nur eine art_-Variable, sondern drei. In jeder kann codiert sein, dass es einen Appell/eine Handlungsaufforderung gab. Interessieren wir uns nun dafür, ob es in mindestens einer dieser Variablen eine Handlungsaufforderung gab, sieht der Code so aus:

tg <- tg |>
  mutate(appell_ges = if_else(art_1 <= 2 | art_2 <= 2 | art_3 <= 2, "mindestens eine Handlungsaufforderung", "keine Handlungsaufforderung"))

Wir überprüfen also, ob in den Variablen art_1, art_2 oder art_3 ein Appell vorkommt. Hierfür nutzen wir wieder die if_else()-Funktion. Wir tragen in der neuen Variablen appell_ges “mindestens eine Handlungsaufforderung” ein, wenn in einer (oder mehreren) der art_-Variablen eine Handlungsauffoderung enthalten ist (implizit oder explizit, Code 1 oder 2). Für alle anderen Fälle tragen wir “keine Handlungsaufforderung” ein. Da es reicht, wenn eine der art_-Variablen eine Handlungsauffoderung enthält, verknüpfen wir die Bedingungen mit dem OR-Operator |.

Für die Recodierung kategorieller Variablen gibt es eine Reihe weiterer Funktionen, die wir in der Sitzung zu Häufigkeiten und Kreuztabellen kennenlernen.

4.7 Glossar

Funktion Definition
filter() Auswählen von Fällen, d.h. Zeilen eines Dataframes/Tibbles, auf Basis bestimmter Bedingungen
if_else() Erstellung einer dichotomen Variable, je nachdem ob ein logisches Statement wahr oder falsch ist
mutate() Verändern oder Erstellen einer oder mehrerer Variablen in einem Dataframe/Tibble
select() Auswählen von Variablen, d.h. Spalten eines Dataframes/Tibbles

4.8 Hausaufgabe

Für die Hausaufgabe analysieren wir den Datensatz gewohnheiten.xlsx.

  1. Erstellen Sie einen neuen Datensatz, der nur die weiblichen Befragten und nur die Variablen Geschlecht, Alter, Bildung und Wohnsituation enthält.

  2. Erstellen Sie eine neue Variable, aus der hervorgeht, ob ein:e Befragte:r einen hohen (mindestens Fachhochschulreife) oder niedrigen (Realschule und darunter) Schulabschluss hat.

  3. Erstellen Sie eine neue Variable, die enthält, ob ein:e Befragte:r mit der Familie zusammenlebt (also mit der:dem (Ehe-)Partner:in, Kindern und/oder den Eltern) oder nicht.