14  Indizieren und Gruppieren

Work in Progress

14.1 Indizieren

Es werden drei Arten von Indizes unterschieden:

  1. Der Primärindex, mit dem ein einzelner Datensatz eindeutig identifiziert werden kann.
  2. Fremdschlüssel sind Sekundärindizes für Querverweise auf eine zweite Datenstruktur (eine sog. Indextabelle oder engl. Lookup-Table).
  3. Gruppenindizes sind Sekundärindizes zur Identifikation von Datensätzen mit gemeinsamen Eigenschaften.

Weil ein Index Werte über einen Datensatz enthält, gehört ein Index zum jeweiligen Datensatz und wird über einen Indexvektor in einer Stichprobe abgebildet.

14.1.1 Hashing eines Primärindex

Die einfachste Technik zur eindeutigen Indizierung ist das Durchnummerieren der Datensätze einer Stichprobe. Bei dieser Technik wird jedem Datensatz eine Nummer zugewiesen. In R verwenden wir dazu die Funktion row_number(). Diese Funktion ist einer Sequenz vorzuziehen, weil diese Funktion auch bei leeren Stichproben fehlerfrei arbeitet.

Warnung

Intuitiv würde man beim Durchnummerieren an seine Sequenz von 1:n() denken. Diese Sequenz führt aber für leere Datenrahmen zu Fehlermeldungen. Die Funktion row_number() kann mit leeren Datenrahmen umgehen und erzeugt dieses Problem nicht.

Beispiel 14.1 (Primärindex erzeugen)  

mtcars |> 
    as_tibble(rownames = "modell") ->
        mtcars_df

mtcars_df |> 
    mutate(
        nr = row_number()
    ) -> 
        mtcars_df_nbr

# Nummerierung zeigen
mtcars_df_nbr |> 
    head()
# A tibble: 6 × 13
  modell   mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb    nr
  <chr>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
1 Mazda…  21       6   160   110  3.9   2.62  16.5     0     1     4     4     1
2 Mazda…  21       6   160   110  3.9   2.88  17.0     0     1     4     4     2
3 Datsu…  22.8     4   108    93  3.85  2.32  18.6     1     1     4     1     3
4 Horne…  21.4     6   258   110  3.08  3.22  19.4     1     0     3     1     4
5 Horne…  18.7     8   360   175  3.15  3.44  17.0     0     0     3     2     5
6 Valia…  18.1     6   225   105  2.76  3.46  20.2     1     0     3     1     6

14.1.2 Hashing zum Gruppieren

Beim Hashing zum Gruppieren müssen wir Werte erzeugen, die eine Zuordnung zu einer Gruppe oder einen Wert in einer anderen Stichprobe ermöglichen. Die Hashing-Funktion orientiert sich dabei an den konkreten Analyseanforderungen.

Vier gängige Techniken können dabei unterschieden werden:

  • Kodieren (alle Datentypen)
  • Reihenfolgen bilden durch Ganzzahldivision (nur Zahlen)
  • Reihenfolgen bilden durch Modulo-Operation (nur Zahlen)
  • Reihenfolgen durch Anfangsbuchstaben (nur Zeichenketten)

14.1.2.1 Beispiel Hashing zum Gruppieren.

Das folgende Beispiel bildet einen Index, um die Motorisierung der Fahrzeugtypen in der Stichprobe mtcars zu bestimmen. Dabei sollen die Modelle in schwach-, mittel-, stark- und sehr starkmotorisierte Typen unterschieden werden. Die Motorisierung richtet sich dabei zum einen nach der Leistung (hp). Zum anderen richtet sich die Motorisierung nach dem Fahrzeuggewicht (wt), weil für ein schweres Fahrzeug mehr Leistung zum Bewegen benötigt wird als für ein leichtes. Um beide Werte zu berücksichtigen, wird das Verhältnis der beiden Werte bestimmt. Ein Verhältnis ist eine Division. In diesem Fall wird das Gewicht als Nenner verwendet und die Leistung als Zähler. So ergeben sich immer Werte grösser als 1, weil die Leistung immer viel grösser als das Gewicht ist.

In diesem Beispiel besteht die Hashing-Funktion aus zwei Teilen:

  1. Das Verhältnis zwischen Leistung und Gewicht wird bestimmt und im Vektor verhaeltnis abgelegt.
  2. Die Leistungsklassen werden durch Kodieren den oben festgelegten Klassen zugewiesen und im Vektor klasse gespeichert.
mtcars_df |> 
    mutate(
        verhaeltnis = hp/wt, 
        klasse = case_when( 
            verhaeltnis > 60 ~ "sehr stark",
            verhaeltnis > 50 ~ "stark", 
            verhaeltnis > 40 ~ "mittel", 
            TRUE ~ "schwach") 
    ) |>
    # nur die relevanten Vektoren zeigen
    select(modell, hp, wt, verhaeltnis, klasse)
# A tibble: 32 × 5
   modell               hp    wt verhaeltnis klasse    
   <chr>             <dbl> <dbl>       <dbl> <chr>     
 1 Mazda RX4           110  2.62        42.0 mittel    
 2 Mazda RX4 Wag       110  2.88        38.3 schwach   
 3 Datsun 710           93  2.32        40.1 mittel    
 4 Hornet 4 Drive      110  3.22        34.2 schwach   
 5 Hornet Sportabout   175  3.44        50.9 stark     
 6 Valiant             105  3.46        30.3 schwach   
 7 Duster 360          245  3.57        68.6 sehr stark
 8 Merc 240D            62  3.19        19.4 schwach   
 9 Merc 230             95  3.15        30.2 schwach   
10 Merc 280            123  3.44        35.8 schwach   
# ℹ 22 more rows

14.1.3 Hashing für Querverweise

Beim Hashing für Querverweise gibt es zwei Stichproben. Die erste Stichprobe ist die Hauptstichprobe mit den eigentlichen Werten. Die zweite Stichprobe ist die Referenzstichprobe, die zusätzliche Information enthält. Ein Indexvektor für Querverweise in der ersten Stichprobe bezieht sich immer auf einen Primärindex aus der zweiten Stichprobe.

Die Hashing-Funktion muss deshalb einen Verweis zur zweiten Stichprobe herstellen. Diese Verbindung kann mit der gleichen Strategie erzeugt werden, wie beim Gruppieren. Dabei muss jedoch darauf geachtet werden, dass alle Zuordnungen des Primärvektors korrekt abgebildet sind.

14.2 Randomisieren

Wenn wir mit Teilstichproben arbeiten und diese mit anderen teilen, müssen wir vermeiden, dass zwei Stichproben leicht zusammengesetzt werden können und so Rückschlüsse über die Probanden möglich werden.

Achtung

Sobald personenbezogene Daten statistisch ausgewertet und zur Publikation vorbereitet werden, müssen die Daten randomisiert werden!

Dieses Rezept beschreibt eine Technik zur Anonymisierung von Daten durch Mischen. Entscheidend bei dieser Technik ist, dass wir die Werte für unsere Analyse zusammenhalten möchten, sodass unsere Ergebnisse nachvollziehbar bleiben. Gleichzeitig soll es unmöglich werden, diese Daten mit anderen Teilen unserer Studien in Verbindung zu bringen.

Die Technik der Anonymisierung durch Mischen besteht aus vier Schritten:

  1. Auswahl der Vektoren, die wir in einer Publikation teilen möchten,
  2. Erzeugung eines eindeutigen Vektors,
  3. zufälliges Mischen,
  4. Entfernen der eindeutigen Vektoren und exportieren der Daten.

14.2.1 Schritt 1: Auswahl der Vektoren

Die zu veröffentlichenden Vektoren werde mit der select()-Funktion selektiert.

14.2.2 Schritt 2: Erzeugung eines eindeutigen Vektors

Alle Werte müssen zusammengehalten werden,damit nachgereihte Analysen nachvollziehbar bleiben. Dazu werden die Datensätze nummeriert.

14.2.3 Schritt 3: Mischen

Dieser Schritt greift auf die Funktion sample() zurück. Wir erzeugen aus den ursprünglichen Nummerierungen eine neue Nummerierung durch daten |> mutate( id_neu = sample(id) ). Nach dieser neuen Nummerierung sind unsere Datensätze aber immer noch in der gleichen Reihenfolge und noch nicht gemischt. Wir müssen also die Reihenfolge so anpassen, dass die neue Nummerierung gilt. Das erreichen wir mit dem Funktionsaufruf daten |> arrange( id_neu ).

14.2.4 Schritt 4: Entfernen des eindeutigen Vektors und exportieren der Daten

Abschliessend müssen wir unbedingt die beiden Hilfsvektoren, die wir zum Mischen verwendet haben, aus unserer Stichprobe wieder entfernen. Das erreichen wir mit einer Vektorauswahl: daten |> select(-c(id, id_neu)).

14.2.5 Vollständige Lösung

Wir greifen hier auf eine Stichprobe zurück, die GeschlechtsInformation, Alter und digitale Nutzungsgewohnheiten umfasst. Wir erstellen zwei getrennte Teilstichproben, von denen eine nur die Nutzungsgewohnheiten und das Geschlecht und eine nur die Nutzungsgewohnheiten und das Alter beinhaltet.

daten = read_csv(
    "daten/befragung_digitales_umfeld/befragung.csv", 
    show_col_types = F)

# mischen Funktion aus einer Funktionskette erstellen, damit 
#    wir nicht so viel tippen müssen.
mischen = . %>% 
    mutate( 
        id = row_number(), 
        id_neu = sample(id) # wählt zufällig eine id aus.
    ) %>%
    arrange(id_neu) %>%
    select(-c(id, id_neu))

daten |>
    select(q00_2, starts_with("q15")) |>
    mischen() |>
    write_csv("teilstichprobe_geschlecht_technik.csv")

daten |>
    select(kurs, starts_with("q18")) |>
    mischen() |>
    write_csv("teilstichprobe_alter_sozial.csv")
Hinweis

Die Definition der Funktion mischen() verwendet die spezielle Funktionsverkettung von tidyverse, weil damit Funktionen erstellt werden können. In diesem Fall dürfen die Operatoren |> und %>% nicht im gleichen Arbeitsschritt gemischt werden.

Die Daten in den beiden Dateien lassen sich nicht mehr zusammenführen. Damit erkennen wir auch die Grenzen dieser Technik: Wenn zwei gemischte Stichproben ausreichend viele gemeinsame oder sehr detaillierte Vektoren haben, die in beiden Teilstichproben vorkommen, dann können diese Stichproben trotz mischen wieder zusammengeführt werden.

Mit den gemischten Daten ist es nun nicht mehr möglich, die Werte mit einem anderen Teil der Stichprobe zu kombinieren und so tiefere Rückschlüsse über die Teilnehmenden (womöglich unwissend) zuzulassen. Nur noch durch den Zugriff auf die ursprünglichen Daten können diese Zusammenhänge hergestellt werden. Daher sind die ursprünglichen Daten oft besonders schützenswert und sollten ohne Randomisierung nicht weitergegeben werden.

14.3 Gruppieren

In R lassen sich grundsätzlich alle Opeationen auch als gruppierte Operationen über einen Datenrahmen durchführen. Dazu wird die group_by()-Funktion verwendet, um gruppierte Operationen anzuzeigen. Dieser Funktion können ein oder mehrere Sekundärindizes als Parameter übergeben werden, die zum Gruppieren verwendet werden.

Beispiel 14.2 (Gruppierte Summe und Mittelwert)  

daten |> 
    group_by(index) |>
    summarise(
        summe = sum(werte)
        mittelwert = mean(werte)
    ) |>
    ungroup() 
Achtung

Wird eine Funktion group_by() ohne eine nachfolgende Operation ausgeführt, dann hat die Funktionen keinen erkennbaren Effekt auf das Ergebnis.

Praxis

Nach eine gruppierten Operation sollte die Gruppierung immer mit ungroup() aufgehoben werden, damit nachfolgende nichtgruppierte Operationen nicht versehentlich als gruppierte Operationen durchgeführt werden.

14.3.1 Gruppiertes Zählen

R verfügt mit der Funktion table() das gemeinsame Auftreten von Werten in zwei diskretskalierten Vektoren zu zählen. Diese Funktion erfordert jedoch zwei voneinander unabhängige Vektoren. Die Funktion count() bietet eine elegante Alternative für Vektoren in Datenrahmen. In diesem Fall werden die Vektoren als Sekundärindizes behandelt.

Beispiel 14.3 (Gruppiertes Zählen)  

daten |> 
    count(index)

Das gruppierte Zählen hat gegenüber der Funktion table() den Vorteil, dass mehr als zwei Vektoren berücksichtigt werden können.

14.3.2 Gruppiertes Nummerieren

Nummerieren ist eine hilfreiche Funktion, um die Position von Werten zu bestimmen. Das Ergebnis ist ein eindeutige Nummer für jeden Datensatz. Beim Gruppierten Nummerieren werden die Datensätze innerhalb ihrer Gruppe nummeriert.

Beispiel 14.4 (Gruppiertes Nummerieren)  

daten |>
    group_by(index) |>
    mutate(
        nr = row_number()
    ) |>
    ungroup()