7 Die Häufigkeitsanalyse kategorialer Daten

📢 Zielsetzung dieser Einheit

In dieser Einheit wollen wir uns mit der uni- und bivariaten Häufigkeitsanalyse kategorialer Daten beschäftigen. Ausgehend von einem Beispiel zur Wahrnehmung von COVID-19 Schutzmaßnahmen werden wir dazu

  • Strategien zur Datenvalidierung;

  • sowie numerische und graphische Vorgehensweise zur Häufigkeitsanalyse kategorialer Variablen erproben.

tl;dr: Her mit dem Code!


7.1 Kategoriale Daten auswerten

Auch für nominale und ordinale Daten - oftmals zusammenfassend als kategoriale Daten bezeichnet - steht uns eine Reihe von Analysemöglichkeiten zur Verfügung. Diese wollen wir uns hier anhand des folgenden Beispieldatensatzes genauer ansehen:

Im Auftrag des Presse- und Informationsamts der Deutschen Bundesregierung wurden seit Kalenderwoche 12/2020 laufend repräsentative Bevölkerungsbefragungen zum Thema “Corona-Krise” durchgeführt. Eine dieser Befragungen wollen wir uns näher ansehen:

👉 https://search.gesis.org/research_data/ZA7677

Die Daten (inkl. Fragebogen etc.) dieser Befragung aus der Kalenderwoche 45/2020 wurden dem gesis-Repositorium des Leibniz-Instituts für Sozialwissenschaften entnommen und können 👉 hier als ZIP-Datei geladen werden.

Nach dem Download extrahieren wir diese Daten in den Ordner “Trendfragen_Corona_45-20” in unserem “data”-Verzeichnis:

    Projektfolder
    | skript_1.R
    | ...
    | skript_n.R    
    +-- data
        +-- Trendfragen_Corona_45-20
            | GESIS-Suche- Trendfragen Corona (Woche 45-2020).url
            | ...
            | ZA7677_v1-0-0.csv
        | datensatz_1.csv
        | ...
        | datensatz_n.csv

Wie uns der Fragebogen (data/ZA7677_fb.pdf) verrät, wurde eine Vielzahl an Merkmalen abgefragt. Aus dieser Vielzahl picken wir uns diese beiden Merkmale heraus:

  • Frage Nr. 5: Die beurteilte Angemessenheit der Maßnahmen zur Pandemiebekämpfung
    Wie schätzen Sie die aktuellen politischen Maßnahmen ein, um das Corona-Virus einzudämmen: Sind diese getroffenen Maßnahmen Ihrer Meinung nach angemessen, gehen sie zu weit oder gehen sie nicht weit genug?

  • Frage s8: Die Politische Orientierung
    Diese Frage zielt auf das Wahlverhalten der befragten Person der letzten Bundestagswahl ab. Wie genau dieses Wahlverhalten abgefragt wurde ist jedoch nicht dokumentiert 😢.

Ziel unserer Analyse soll sein herauszufinden,

  1. wie sich die beiden Merkmale über die Befragten verteilen;

  2. wie sich die Verteilung dieser beiden Merkmale zueinander verhält.

7.2 Vorbereitendes

Bevor wir uns die Befragungsdaten selbst genauer ansehen, laden wir zunächst das Package tidyverse. Dieses Package besteht wiederum aus mehreren Packages, welche die Analyse von Daten erleichtern und nachvollziehbarer gestalten sollen.

library(tidyverse)

📚 Exkurs:
Das Sammelpackage tidyverse bietet viele praktische Funktionalitäten, beispielsweise werden wir die sgn. Pipes (= Weiterleitungen) des dplyr-Packages nutzen. Mit Pipes kann Code übersichtlicher gestaltet werden, indem Ergebnisse einer Berechnung direkt als Input für einen anderen Schritt genutzt werden:

somedata <- data.frame(zahlen = c(1,2,3,4,5,6,7,8,9,10))

# statt:
mean(somedata$zahlen)
## [1] 5.5
# geht jetzt:
somedata %>%
  summarise(mean(zahlen))
##   mean(zahlen)
## 1          5.5
# und das:
somedata %>%
  summarise(Durchschnitt = mean(zahlen),
            Median = median(zahlen))
##   Durchschnitt Median
## 1          5.5    5.5
# und das:
somedata %>%
  summary(.)
##      zahlen     
##  Min.   : 1.00  
##  1st Qu.: 3.25  
##  Median : 5.50  
##  Mean   : 5.50  
##  3rd Qu.: 7.75  
##  Max.   :10.00

7.3 Der Datenimport, die Datenaufbereitung und -validierung

Der Import der Daten ist schnell erledigt:

Trendfragen <- as_tibble(read.csv2("data/Trendfragen_Corona_45-20/ZA7677_v1-0-0.csv",
                                   encoding = "UTF-8"))

Ein erster Blick auf diese verrät uns, dass den einzelnen Beobachtungen (= Zeilen) noch keine eindeutigen IDs zugewiesen wurden. Das ist durchaus praktisch, um im weiteren Verlauf einer Analyse einzelne Beobachtungen (aka. Records oder ebene Zeilen) direkt ansprechen zu können. Um dies nachzuholen nummerieren wir noch einmal alle Records durch:

Trendfragen <- Trendfragen %>%
  mutate(id = row_number())

Nun wollen wir den Datensatz etwas ausdünnen - sprich unsere Variablen von Interesse (bcor5 und s8) ausfiltern und deren Skalenniveau setzen (= in Factors überführen):

df <- Trendfragen %>%
  select(id, bcor5, s8) %>%
  mutate(bcor5 = as.factor(bcor5), s8 = as.factor(s8))    # oder:
  # mutate(across(c(bcor5, s8), factor))                  # oder:
  # mutate(across(where(is.character), factor))

glimpse(df)
## Rows: 1,506
## Columns: 3
## $ id    <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18~
## $ bcor5 <fct> angemessen, angemessen, gehen nicht weit genug, angemessen, g~
## $ s8    <fct> Grüne/ Bündnis 90, CDU/ CSU, SPD, SPD, CDU/ CSU, Grüne/ Bündn~

Dann wollen wir noch überprüfen, ob sich nicht fehlende Werte - in der R-Logik sgn. NAs oder “Not Available”-Werte - eingeschlichen haben. Dazu können wir für jede unserer Variablen von Interesse die vorhandenen Merkmalsausprägungen zählen lassen:

table(df$bcor5)
## 
##             angemessen gehen nicht weit genug          gehen zu weit 
##                    937                    268                    276 
##                   k.A.             weiß nicht 
##                      5                     20
# oder:
df %>%
  count(s8)
## # A tibble: 9 x 2
##   s8                    n
##   <fct>             <int>
## 1 -1                   56
## 2 AfD                  48
## 3 CDU/ CSU            438
## 4 FDP                  64
## 5 Grüne/ Bündnis 90   235
## 6 Linkspartei          72
## 7 Rest                282
## 8 Sonstige             35
## 9 SPD                 276

Vor allem bei kontinuierlichen Variablen ist diese Vorgehensweise aber suboptimal. Flotter geht es mit:

colSums(is.na(df))
##    id bcor5    s8 
##     0     0     0
# alternativ:
df %>%
  summarise(across(everything(), list(n_miss = ~ sum(is.na(.x)))))
## # A tibble: 1 x 3
##   id_n_miss bcor5_n_miss s8_n_miss
##       <int>        <int>     <int>
## 1         0            0         0

Sieht gut aus - wir haben keine fehlenden (= NA) Werte. Ein Blick auf den Besatz der Kategorien der Frage s8 zum Wahlverhalten zeigt uns das Vorhandensein der Kategorie “-1”. Diese kann NichtwählerInnen kennzeichnen, was wir aber mangels Dokumentation nicht wissen können 😢. Der Übung halber wollen wir alle Records ausschließen, die bei Frage s8 die Merkmalsausprägung “-1” aufweisen:

df2 <- df %>%
  filter(s8 != "-1") %>%
  mutate(s8 = droplevels(s8))

df2 %>%
  count(s8) %>%
  arrange(n)
## # A tibble: 8 x 2
##   s8                    n
##   <fct>             <int>
## 1 Sonstige             35
## 2 AfD                  48
## 3 FDP                  64
## 4 Linkspartei          72
## 5 Grüne/ Bündnis 90   235
## 6 SPD                 276
## 7 Rest                282
## 8 CDU/ CSU            438
  # arrange(desc(n))    # oder:
  # arrange(-n)

7.4 Die Häufikgeitsanalyse einer Variablen

Damit können wir nun unsere erste Frage in Angriff nehmen:
Wie werden aktuelle Maßnahmen zu Pandemiebekämpfung in Deutschland beurteilt. Einen erste Annäherung an diese Frage erhalten wir hiermit:

table(df2$bcor5)
## 
##             angemessen gehen nicht weit genug          gehen zu weit 
##                    908                    254                    265 
##                   k.A.             weiß nicht 
##                      4                     19
prop.table(table(df2$bcor5))
## 
##             angemessen gehen nicht weit genug          gehen zu weit 
##            0.626206897            0.175172414            0.182758621 
##                   k.A.             weiß nicht 
##            0.002758621            0.013103448

Einen kompakteren Überblick auf die absoluten und relativen Gewichte der Merkmalsausprägungen kann man mithilfe des dplyr-Packages gewinnen:

df2 %>%
  group_by(bcor5) %>%
  summarise(n = n()) %>%
  mutate(relFreq = n / sum(n)) %>%
  mutate(relFreq = round(relFreq, 2)) %>%
  arrange(-relFreq)
## # A tibble: 5 x 3
##   bcor5                      n relFreq
##   <fct>                  <int>   <dbl>
## 1 angemessen               908    0.63
## 2 gehen nicht weit genug   254    0.18
## 3 gehen zu weit            265    0.18
## 4 weiß nicht                19    0.01
## 5 k.A.                       4    0

Wir sehen, dass mehr als 60% aller Befragten die gesetzten Maßnahmen zur Pandemiebekämpfung als ausreichend und jeweils ein schwaches Fünftel diese Maßnahmen entweder als zu weitgehend oder als nicht weitgehend genug ansehen.

Nachdem wir nun unsere erste Frage numerisch gelöst haben, wollen wir uns auch an einer graphischen Lösung versuchen:

# cheap trick:
ggplot(df2, aes(x = s8, y = ..count.., group = 1)) +
  geom_bar()

Irgendwie schon und auch wieder nicht. Next try:

# more shiny:
ggplot(df2, aes(x = forcats::fct_infreq(s8), y = ..count.., group = 1)) +
  geom_bar(fill = "blue", color = "black") +
  coord_flip() +
  labs(title = "Catchy Titel",
     subtitle = "Was noch gesagt werden sollte",
     x = "Polit. Orientierung\nletzte Bundestagswahl\n",
     y = "\nabs. Anzahl",
     caption = "\n(Daten: Presse- und Informationsamt der Deutschen Bundesregierung, 2021)") +
  theme_bw() +
  theme(text = element_text(size = 11),
        plot.caption = element_text(hjust = 0.5))

Oder:

# anders shiny:
library(scales)   # zum Formatieren der Achsen etc.

ggplot(df2, aes(x = forcats::fct_infreq(s8), y = ..prop.., group = 1)) +
  geom_bar(fill = "red") +
  labs(title = "Wieder ein catchy Titel",
     subtitle = "Was noch immer gesagt werden sollte",
     x = "\n Polit. Orientierung bei letzter Bundestagswahl",
     y = "Anteile [%]\n",
     caption = "\n(Daten: Presse- und Informationsamt der Deutschen Bundesregierung, 2021)") +
  scale_y_continuous(labels = percent) +
  theme_bw() +
  theme(text = element_text(size = 11),
        axis.text.x=element_text(angle = 45, hjust = 1),
        plot.caption = element_text(hjust = 0.5))

Oder:

# ein letztes mal anders shiny:
library(RColorBrewer)   # Sammlung von diskreten& kontinuierlichen Farbpaletten
display.brewer.all()

ggplot(df2, aes(x = "", y = ..count.., fill = forcats::fct_rev(forcats::fct_infreq(s8)))) +
  geom_bar(position = "fill", color = "black") +
  coord_polar("y", start=0, direction = 1) +
  scale_y_continuous(labels = percent) +
  # scale_fill_brewer(palette = "Set1") +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5), 
        plot.subtitle = element_text(hjust = 0.5),
        plot.caption = element_text(hjust = 0.5),
        axis.title.x = element_blank(), 
        axis.title.y = element_blank()) +
  guides(fill = guide_legend(reverse = TRUE)) +
  labs(title = "Catchy Titel",
     subtitle = "Was noch gesagt werden sollte",
     caption = "\n(Daten: Presse- und Informationsamt der Deutschen Bundesregierung, 2021)",
     fill = "polit. Ausrichtung")

You get it. Das ggplot2-Package bietet unendlich viele Möglichkeiten, sich in der Gestaltung von Abbildungen zu verlieren. Drei gute Startpunkte für die eigene weitere Auseinandersetzung damit:

7.5 Die Häufigkeitsanalyse zweier kategorialer Variablen

Am Beginn dieser Einheit haben wir uns zwei Ziele für diese Analyse gesetzt:

Wir wollten wissen, wie die Befragten Maßnahmen zur Pandemiebekämpfung in Deutschland beurteilen (Frage bcor5) und welche politischen Positionen diese vertreten (Frage s8). Diese beiden Teilfragen haben wir ja bereits behandelt. Ob sich die Beurteilung der Maßnahmen zur Pandemiebekämpfung mit der politischen Ausrichtung ändert oder nicht - who knows? 😉

Die Antwort auf diese letzte Frage ist einfach: Wir natürlich! Dazu wollen wir uns zunächst eine einfache Kontingenztabelle mit den beiden Variablen bcor5 und s8 basteln:

table(df2$bcor5, df2$s8)
##                         
##                          AfD CDU/ CSU FDP Grüne/ Bündnis 90 Linkspartei Rest
##   angemessen              13      301  40               165          49  136
##   gehen nicht weit genug   5       70   8                41           8   73
##   gehen zu weit           25       64  15                27          13   66
##   k.A.                     1        0   0                 1           0    1
##   weiß nicht               4        3   1                 1           2    6
##                         
##                          Sonstige SPD
##   angemessen                   19 185
##   gehen nicht weit genug        5  44
##   gehen zu weit                10  45
##   k.A.                          0   1
##   weiß nicht                    1   1
table(df2$bcor5, df2$s8) %>%
  prop.table(., margin = 2) %>%   # 2 ... Spaltenprozent
  round(., 2)
##                         
##                           AfD CDU/ CSU  FDP Grüne/ Bündnis 90 Linkspartei
##   angemessen             0.27     0.69 0.62              0.70        0.68
##   gehen nicht weit genug 0.10     0.16 0.12              0.17        0.11
##   gehen zu weit          0.52     0.15 0.23              0.11        0.18
##   k.A.                   0.02     0.00 0.00              0.00        0.00
##   weiß nicht             0.08     0.01 0.02              0.00        0.03
##                         
##                          Rest Sonstige  SPD
##   angemessen             0.48     0.54 0.67
##   gehen nicht weit genug 0.26     0.14 0.16
##   gehen zu weit          0.23     0.29 0.16
##   k.A.                   0.00     0.00 0.00
##   weiß nicht             0.02     0.03 0.00

Betrachtet man bei jeder Partei den relativen Besatz der Kategorien “angemessen” und “gehen zu weit”, fallen große Unterschiede auf: Während die Hälfte der AFD-orientierten Befragten die Maßnahmen als zu weitreichend einstufen, tun dies gerade einmal 11% der grün-orientierten Befragten. Die politische Orientierung und die Beurteilung der Maßnahmen zur Pandemiebekämpfung in Deutschland scheinen also miteinander zu korrelieren.

📚 Exkurs:
Wie immer gibt es viele Wege mit R eine Kontingenztabelle zu erzeugen. Beispielsweise etwas ungelenk (= längerer Code, nur Summenprozent) mittels des dplyr-Packages:

df2 %>%
  group_by(bcor5, s8) %>%
  summarise(n = n(), .groups = "drop") %>%
  spread(c(s8), c(n))
## # A tibble: 5 x 9
##   bcor5      AfD `CDU/ CSU`   FDP `Grüne/ Bündni~` Linkspartei  Rest Sonstige
##   <fct>    <int>      <int> <int>            <int>       <int> <int>    <int>
## 1 angemes~    13        301    40              165          49   136       19
## 2 gehen n~     5         70     8               41           8    73        5
## 3 gehen z~    25         64    15               27          13    66       10
## 4 k.A.         1         NA    NA                1          NA     1       NA
## 5 weiß ni~     4          3     1                1           2     6        1
## # ... with 1 more variable: SPD <int>
df2 %>%
  group_by(bcor5, s8) %>%
  summarise(n = n(), .groups = "drop") %>%
  mutate(relFreq = n / sum(n)) %>%
  mutate(relFreq = round(relFreq, 2)) %>%
  subset(select = c(bcor5, s8, relFreq)) %>%
  spread(c(s8), c(relFreq))
## # A tibble: 5 x 9
##   bcor5      AfD `CDU/ CSU`   FDP `Grüne/ Bündni~` Linkspartei  Rest Sonstige
##   <fct>    <dbl>      <dbl> <dbl>            <dbl>       <dbl> <dbl>    <dbl>
## 1 angemes~  0.01       0.21  0.03             0.11        0.03  0.09     0.01
## 2 gehen n~  0          0.05  0.01             0.03        0.01  0.05     0   
## 3 gehen z~  0.02       0.04  0.01             0.02        0.01  0.05     0.01
## 4 k.A.      0         NA    NA                0          NA     0       NA   
## 5 weiß ni~  0          0     0                0           0     0        0   
## # ... with 1 more variable: SPD <dbl>

Eleganter geht es mit zum Beispiel mit dem janitor-Package:

library(janitor)
## 
## Attache Paket: 'janitor'
## Die folgenden Objekte sind maskiert von 'package:stats':
## 
##     chisq.test, fisher.test
janitor::tabyl(df2, bcor5, s8) %>%
  adorn_totals(c('row')) %>%
  adorn_percentages('col') %>% 
  adorn_pct_formatting(digits = 0) %>%
  # adorn_ns() %>%
  adorn_title('combined') %>%
  knitr::kable()
bcor5/s8 AfD CDU/ CSU FDP Grüne/ Bündnis 90 Linkspartei Rest Sonstige SPD
angemessen 27% 69% 62% 70% 68% 48% 54% 67%
gehen nicht weit genug 10% 16% 12% 17% 11% 26% 14% 16%
gehen zu weit 52% 15% 23% 11% 18% 23% 29% 16%
k.A. 2% 0% 0% 0% 0% 0% 0% 0%
weiß nicht 8% 1% 2% 0% 3% 2% 3% 0%
Total 100% 100% 100% 100% 100% 100% 100% 100%

Nach der numerischen wollen wir natürlich auch wieder eine graphische Lösung. Wir starten wieder mit der Minimalvariante:

ggplot(df2, aes(x = s8, fill = bcor5)) +
  geom_bar()

Na ja, noch nicht ganz. Ein neuer Anlauf:

ggplot(df2, aes(x = s8, fill = bcor5)) +
  geom_bar(position = "fill", color = "black", width = 0.7) +
  scale_y_continuous(labels = scales::percent) +
  coord_flip() +
  theme_bw() +
  theme(plot.title = element_text(hjust = 0.5), 
      plot.subtitle = element_text(hjust = 0.5),
      plot.caption = element_text(hjust = 0.5),
      panel.grid.major.x = element_line(color = "gray")) +
  labs(title = "Catchy Titel",
   subtitle = "Was noch gesagt werden sollte\n",
   caption = "\n(Daten: Presse- und Informationsamt der Deutschen Bundesregierung, 2021)",
   x = "polit. Aursrichtung\n",
   y = "",
   fill = "Beurteilung\nMaßnahmen") +
  guides(fill = guide_legend(reverse = TRUE))

📚 Ein Exkurs für PerfektionistInnen:
Die Sortierung der Kategorienachse “polit. Ausrichtung” könnte noch optimiert werden, um LerserInnen beispielsweise die politischen Ausrichtungen (Variable bcor5) anhand der Angemessenheit der Maßnahmen (Ausprägung “angemessen”) absteigend darzustellen:

# Kontingenztabelle ermitteln & als Dataframe ablegen
konttab <- table(df2$bcor5, df2$s8) %>%
  prop.table(., margin = 2) %>%   # 2 ... Spaltenprozent
  round(., 2) %>%
  as.data.frame()

# gewünschte Reihenfolge herstellen & ablegen: 
# Beurteilung angemessen absteigend nach rel. Häufigkeit
vis.order <- konttab %>%
  filter(Var1 == "angemessen") %>%
  arrange(Freq) %>%
  select(Var2)
vis.order.vector <- as.character(vis.order$Var2)

# Datensatz für Visualisierung vorbereiten
vis.data.1 <- df2
vis.data.1$s8 <- factor(df2$s8, levels = vis.order.vector)

# Plotten
ggplot(vis.data.1, aes(x = s8, fill = forcats::fct_rev(bcor5))) +
  geom_bar(position = "fill", color = "black", width = 0.7) +
  scale_y_continuous(labels = scales::percent) +
  coord_flip() +
  theme_bw() +
  theme(plot.title = element_text(hjust = 0.5),
      plot.subtitle = element_text(hjust = 0.5),
      plot.caption = element_text(hjust = 0.5),
      panel.grid.major.x = element_line(color = "gray")) +
  labs(title = "Catchy Titel",
   subtitle = "Was noch gesagt werden sollte\n",
   caption = "\n(Daten: Presse- und Informationsamt der Deutschen Bundesregierung, 2021)",
   x = "polit. Aursrichtung\n",
   y = "",
   fill = "Beurteilung\nMaßnahmen") +
  guides(fill = guide_legend(reverse = TRUE))


🏆 Nun wissen wir, …

  • wie wir mittels des Tidyverse-Packages gut lesbaren Code und Abbildungen erstellen können.
  • wie wir Datensätze filtern können.
  • wie wir uni- und bivariate Häufikgeitsauswertungen tabellarisch und graphisch vornehmen können.
  • dass es in R immer viele Wege gibt, um sein Ziel zu erreichen 😎.

Lassen Sie uns nun in die Zusammenhänge zwischen kategorialen Variablen eintauchen …