3  Reliabilität und Validierung

3.1 Intercoder-Reliabilität

Wir beginnen wieder mit dem Laden der benötigten Pakete. Für Reliabilitätsanalysen verwenden wir das Paket tidycomm. Die Validierung führen wir mit dem Paket yardstick durch.

library(tidyverse)
library(tidycomm)
library(yardstick)

Das Paket tidycomm enthält auch bereits einen Datensatz mit Facebook-Posts, den wir für die Reliabilitätsanalyse verwenden können. Wir schauen uns zunächst die ersten Zeilen an.

fbposts
# A tibble: 270 × 7
  post_id coder_id type  n_pictures pop_elite pop_people pop_othering
    <int>    <int> <chr>      <int>     <int>      <int>        <int>
1       1        1 photo          1         0          0            0
2       1        2 photo          1         0          0            0
3       1        3 photo          1         0          0            0
4       1        4 photo          1         0          0            0
5       1        5 photo          1         0          0            0
# ℹ 265 more rows

Tidycomm bietet die Funktion test_icr(), um Intercoder-Reliabilitätstests für mehrere Variablen (Kategorien/Codiereinheiten) gleichzeitig durchzuführen. Die Testdaten müssen im Long-Format vorliegen: eine Spalte für die Einheit (z. B. Artikel 1, Artikel 2), eine für den Codierer (als Name oder numerische ID) und jeweils eine Spalte pro zu testender Variable.

fbposts |>
  tidycomm::test_icr(unit_var = post_id, coder_var = coder_id)
# A tibble: 5 × 8
  Variable     n_Units n_Coders n_Categories Level   Agreement Holstis_CR
* <chr>          <int>    <int>        <int> <chr>       <dbl>      <dbl>
1 type              45        6            4 nominal     1          1    
2 n_pictures        45        6            7 nominal     0.822      0.930
3 pop_elite         45        6            6 nominal     0.733      0.861
4 pop_people        45        6            2 nominal     0.778      0.916
5 pop_othering      45        6            4 nominal     0.867      0.945
# ℹ 1 more variable: Krippendorffs_Alpha <dbl>

Werden keine Skalenniveaus für die Variablen angegeben, dann nimmt die Funktion test_icr an, dass es sich um nominale Variablen handelt. Mit dem Funktionsargument level() können wir in der Form c(Variablenname = “Variablenlevel”) die Skalenniveaus der Variablen anpassen. Dies ändert die Berechnungsweise von Krippendorffs Alpha.

fbposts |>
  tidycomm::test_icr(unit_var = post_id, coder_var = coder_id, levels = c(n_pictures = "ordinal"))
# A tibble: 5 × 8
  Variable     n_Units n_Coders n_Categories Level   Agreement Holstis_CR
* <chr>          <int>    <int>        <int> <chr>       <dbl>      <dbl>
1 type              45        6            4 nominal     1          1    
2 n_pictures        45        6            7 ordinal     0.822      0.930
3 pop_elite         45        6            6 nominal     0.733      0.861
4 pop_people        45        6            2 nominal     0.778      0.916
5 pop_othering      45        6            4 nominal     0.867      0.945
# ℹ 1 more variable: Krippendorffs_Alpha <dbl>

Wenn nicht alle Texte und Kategorien von allen Codierern bearbeitet wurden, d.h. fehlende Werte vorkommen, führt dies dazu, dass test_icr() den Wert NA für alle Variable, die einen fehlenden Wert enthlalten, zurückgibt. Wir simulieren hier den Fall, dass nicht alle Codierer alle Posts bearbeitet haben, indem wir den Datensatz auf 60% der Daten reduziere und führen den Test erneut durch.

fb_incomplete <- fbposts |>
  sample_frac(.6)

fb_incomplete |>
  tidycomm::test_icr(unit_var = post_id, coder_var = coder_id)
# A tibble: 5 × 8
  Variable     n_Units n_Coders n_Categories Level   Agreement Holstis_CR
* <chr>          <int>    <int>        <int> <chr>   <lgl>     <lgl>     
1 type              45        6            4 nominal NA        NA        
2 n_pictures        45        6            7 nominal NA        NA        
3 pop_elite         45        6            6 nominal NA        NA        
4 pop_people        45        6            2 nominal NA        NA        
5 pop_othering      45        6            3 nominal NA        NA        
# ℹ 1 more variable: Krippendorffs_Alpha <dbl>
Wide- und Long-Format

Die Funktion test_icr() erwartet die Daten im Long-Format. Das bedeutet, dass jede Codiereinheit (z. B. ein Text) in einer eigenen Zeile steht und die Codierer in einer Spalte. Die Variablen, die codiert wurden, stehen in einer weiteren Spalte. Hier laden wir einen Datensatz der im Wide-Format vorliegt und wandeln ihn ins Long-Format um.

d_wide <- read_csv2("https://github.com/bachl/raw_data/raw/refs/heads/main/thema.csv") |>
  rownames_to_column("unit_id")
d_wide
# A tibble: 5 × 4
  unit_id  nils katja sonja
  <chr>   <dbl> <dbl> <dbl>
1 1           5     5     5
2 2           3     3     2
3 3           3     3     3
4 4           6     9     6
5 5           5     5     5

Mit der Funktion pivot_longerlässt sich der Datensatz ins Long-Format bringen.

d_long <- d_wide |>
  pivot_longer(nils:sonja, names_to = "coder_id", values_to = "code")
d_long
# A tibble: 15 × 3
  unit_id coder_id  code
  <chr>   <chr>    <dbl>
1 1       nils         5
2 1       katja        5
3 1       sonja        5
4 2       nils         3
5 2       katja        3
# ℹ 10 more rows

Anschließend können wir die Intercoder-Reliabilität mit test_icr testen.

d_long |>
  test_icr(unit_var = unit_id, coder_var = coder_id)
# A tibble: 1 × 8
  Variable n_Units n_Coders n_Categories Level   Agreement Holstis_CR
* <chr>      <int>    <int>        <int> <chr>       <dbl>      <dbl>
1 code           5        3            5 nominal       0.6      0.733
# ℹ 1 more variable: Krippendorffs_Alpha <dbl>

3.2 Validierung mit Goldstandard

Für die Validierung maschineller Codierungen benötigen wir einen Goldstandard, von dem wir annehmen, dass er die korrekte Codierung darstellt. Der nachfolgende Daten enthalten die maschniellen Codierungen (observed) und den Goldstandard (gold). Codiert wurde, ob der Text Sexismus enthält.

d_sexist <- read_csv2("https://raw.githubusercontent.com/bachl/raw_data/refs/heads/main/task1_sample.csv") |>
  mutate(gold = factor(gold), observed = factor(observed))
d_sexist
# A tibble: 200 × 4
  text                                              gold  observed reason_sexist
  <chr>                                             <fct> <fct>    <chr>        
1 My buddy and wife just stopped by to check on me… not … not sex… The comment …
2 You are obviously illiterate and stupid if you t… not … not sex… The comment …
3 Don't fred, you're a human, not a stonecold trp … not … not sex… The comment …
4 Has Huma been falling off any bridges recently o… not … sexist   The comment …
5 [USER]‍ & Anglin are on the same page. But yeah s… not … not sex… The comment …
# ℹ 195 more rows

Mit der Funktion conf_mat() können wir eine Konfusionsmatrix erstellen, um die Übereinstimmung zwischen Goldstandard und maschineller Codierung tabellarisch darzustellen. Diese Matrix ist die Grundlage für die Berechnung verschiedener Validierungsmetriken.

d_sexist |>
  conf_mat(truth = gold, estimate = observed)
            Truth
Prediction   not sexist sexist
  not sexist         72     14
  sexist             28     86

Wir berechnen nun die Metriken Accuracy, Recall, Precision und das F-Maß, um die Qualität der maschinellen Codierung zu bewerten. Hierfür verwenden wir die Funktion metric_set() aus dem Paket yardstick. Wichtig ist, dass wir zuvor die Variablen in Faktoren umgewandelt haben. Das Funktionsargument event_level gibt nämlich an, welche Ausprägung als positiv (Einordnung als “Sexismus”) oder negativ (Einordnung als “kein Sexismus”) betrachtet wird. Die Ergebnisse ändern sich, je nachdem, ob “Sexismus” oder “kein Sexismus” als positiv bewertet wird. In unserem Fall ist das zweite Level des Faktors (hier Sexismus) als positiv zu definieren.

vali_metrics <- metric_set(accuracy, recall, precision, f_meas)
d_sexist |>
  vali_metrics(
    truth = gold, estimate = observed,
    event_level = "second"
  )
# A tibble: 4 × 3
  .metric   .estimator .estimate
  <chr>     <chr>          <dbl>
1 accuracy  binary         0.79 
2 recall    binary         0.86 
3 precision binary         0.754
4 f_meas    binary         0.804

Alternativ zu den Metriken aus yardstick können wir auch Krippendorffs Alpha berechnen, um die Übereinstimmung zwischen Goldstandard und maschineller Codierung zu bewerten. Hierfür müssen wir die Daten wieder in das Long-Format bringen, um anschließend test_icr() anwenden zu können.

d_sexist |>
  pivot_longer(gold:observed, names_to = "coder_id", values_to = "code") |>
  test_icr(unit_var = text, coder_var = coder_id, code)
# A tibble: 1 × 8
  Variable n_Units n_Coders n_Categories Level   Agreement Holstis_CR
* <chr>      <int>    <int>        <int> <chr>       <dbl>      <dbl>
1 code         200        2            2 nominal      0.79       0.79
# ℹ 1 more variable: Krippendorffs_Alpha <dbl>

3.3 Problematische Codiereinheiten finden

Das Auffinden problematischer Codiereinheiten ist bei zwei Codierern oder dem Vergleich zwischen Goldstandard und maschineller Codierung einfach. Es muss nur nach den Einheiten gesucht werden, bei denen die Codierungen voneinander abweichen. Dies ist ganz einfach mittels filter() möglich.

d_sexist |>
  filter(gold != observed)
# A tibble: 42 × 4
  text                                              gold  observed reason_sexist
  <chr>                                             <fct> <fct>    <chr>        
1 "Has Huma been falling off any bridges recently … not … sexist   "The comment…
2 "Yes, and men should not call women out for dres… not … sexist   "The comment…
3 "I've never seen anything like it. At first, i w… not … sexist   "The comment…
4 "They call it rape but I bet most of those poor … not … sexist   "The comment…
5 "So now all the hens can peck each other [URL]"   not … sexist   "The comment…
# ℹ 37 more rows

Für den Fall, dass es mehr als zwei Codierer gibt, können wir die Anzahl verschiedener Codes pro Variable/Codiereinheit zählen (Uneinigkeit) und die Einheiten nach Uneinigkeit sortieren. Die problematischen Codiereinheiten werden dann als erstes angezeigt.

fbposts |>
  group_by(post_id) |>
  summarise(pop_elite_codes = n_distinct(pop_elite)) |>
  arrange(-pop_elite_codes)
# A tibble: 45 × 2
  post_id pop_elite_codes
    <int>           <int>
1      33               4
2      17               3
3      18               2
4      27               2
5      28               2
# ℹ 40 more rows

Annschließend können einzelne Einheiten betrachtet werden.

fbposts |>
  filter(post_id == 33)
# A tibble: 6 × 7
  post_id coder_id type  n_pictures pop_elite pop_people pop_othering
    <int>    <int> <chr>      <int>     <int>      <int>        <int>
1      33        1 photo          1         9          0            0
2      33        2 photo          1         0          0            0
3      33        3 photo          1         1          1            0
4      33        4 photo          1         1          0            0
5      33        5 photo          1         9          0            0
# ℹ 1 more row