Explorative Datenanalyse

EDA
Visualisierung
R
Python
Verteilungen, Ausreisser und Zusammenhänge systematisch sichten.

Kernideen

  • Explorative Analyse erzeugt Fragen, sie beantwortet keine.
  • Die Erwartung wird vor dem Blick in die Daten formuliert, sonst bestätigt man, was man ohnehin vermutet hat.
  • Variation: ein Merkmal für sich ansehen. Kovariation: zwei Merkmale gemeinsam.
  • Auffällige Muster verraten meist etwas über die Erhebung, nicht über den untersuchten Gegenstand.
  • Platzhalter für fehlende Werte sind gefährlicher als offene Lücken, weil sie mitgerechnet werden.
  • Die Grafiken dieser Phase sind Arbeitsmittel und dürfen hässlich sein.
  • Jeder Befund ist vorläufig, bis er an neuen Daten geprüft ist.

Erklärung

Vorwissen: Data Wrangling für die Aufbereitung, Skalenniveaus und Lage- und Streuungsmasse für die Kennzahlen sowie ggplot2 für die Grafiken. Diese Seite bringt keine neue Technik, sondern ordnet die bisherigen zu einem Vorgehen. Was dabei an Mängeln auffällt, behandelt die Datenqualität.

Was EDA ist und was nicht

Zwischen Rohdaten und Modell liegt eine Phase, die in Lehrbüchern oft fehlt und in der Praxis den Ausschlag gibt. Sie besteht nicht darin, eine Liste vorgeschriebener Kennzahlen abzuarbeiten. Sie besteht aus einer Schleife: eine Frage an die Daten stellen, sie mit einer Kennzahl oder Grafik beantworten, aus der Antwort die nächste Frage ableiten.

Daraus folgt etwas Praktisches. Die Grafiken dieser Phase sind Arbeitsmittel und dürfen hässlich sein. Wer hier schon an Farben und Beschriftung feilt, verlangsamt sich genau dort, wo Tempo zählt. Sorgfalt in der Gestaltung kommt später, wenn feststeht, was überhaupt gezeigt werden soll.

Die Erwartung kommt vor dem Blick

Der menschliche Verstand findet für fast jedes Bild eine Erklärung, auch für Zufall. Wer eine Grafik ansieht und danach überlegt, was sie bedeutet, bestätigt oft nur, was er ohnehin vermutet hat.

Der Gegenzug ist billig und wirksam: vorher aufschreiben, was man erwartet. Etwa so, für das Merkmal Blutdruck: Werte zwischen 90 und 180, eingipflig, leicht rechtsschief, Mittelwert um 130. Weicht die Grafik davon ab, fällt es sofort auf, und die Abweichung ist der interessante Teil.

Die Reihenfolge

Schritt Frage Werkzeug
1. Struktur Wie viele Zeilen, welche Spalten, welche Typen? str(), info()
2. Vollständigkeit Wo fehlen Werte, wo stehen Platzhalter? Zählen, Minimum und Maximum
3. Variation Wertebereich, Form, Gipfel, Absonderlichkeiten je Merkmal Histogramm, Boxplot, Kennzahlen
4. Kovariation Welche Merkmale hängen zusammen? Streudiagramm, Boxplots je Gruppe, Kreuztabelle
5. Aufteilung Ändert sich das Bild je Gruppe? dieselben Grafiken, nach Gruppen getrennt

Schritt 2 vor Schritt 3 ist wichtig: Ein Platzhalter von -1 oder 999 verzerrt jede Kennzahl, und zwar unauffällig.

Welche Darstellung wozu

Kombination Darstellung
ein metrisches Merkmal Histogramm, Boxplot, Verteilungsfunktion
ein kategoriales Merkmal Häufigkeitstabelle, Stabdiagramm
metrisch und metrisch Streudiagramm
metrisch und kategorial Boxplots nebeneinander
kategorial und kategorial Kreuztabelle, gestapelte Balken

Beispiele

Alle Beispiele arbeiten mit demselben konstruierten Datensatz: 400 Untersuchungen mit Gewicht, Alter, Standort und Blutdruck. Eingebaut sind drei Auffälligkeiten, wie sie in echten Exporten vorkommen, welche, verrät die Auswertung selbst.

Frage und Datenlage

Der erste Blick gilt nicht der Verteilung, sondern der Vollständigkeit: Wie viele Zeilen, welche Typen, wo fehlen Werte und wo stehen Platzhalter, die wie Werte aussehen?

Rechnung

str(roh)
'data.frame':   400 obs. of  4 variables:
 $ gewicht  : num  65.7 104.5 85.9 71.3 73.2 ...
 $ alter    : num  21 24 40 53 60 44 49 26 72 47 ...
 $ standort : chr  "Bern" "Bern" "Bern" "Bern" ...
 $ blutdruck: num  105 124 140 135 140 145 142 115 144 130 ...
summary(roh)
    gewicht           alter            standort     blutdruck    
 Min.   : -1.00   Min.   :18.00   Length   :400   Min.   : 92.0  
 1st Qu.: 73.95   1st Qu.:32.00   N.unique :  3   1st Qu.:123.0  
 Median : 80.50   Median :45.00   N.blank  :  0   Median :130.5  
 Mean   : 80.53   Mean   :45.35   Min.nchar:  4   Mean   :131.8  
 3rd Qu.: 87.80   3rd Qu.:60.00   Max.nchar:  4   3rd Qu.:141.0  
 Max.   :124.50   Max.   :74.00                   Max.   :160.0  
                                                  NAs    :18     
# Wo fehlt etwas, und wo sieht etwas nach Platzhalter aus?
colSums(is.na(roh))
  gewicht     alter  standort blutdruck 
        0         0         0        18 
sapply(roh[, c("gewicht", "alter", "blutdruck")], range, na.rm = TRUE)
     gewicht alter blutdruck
[1,]    -1.0    18        92
[2,]   124.5    74       160
print(roh.dtypes)
gewicht      float64
alter          int64
standort      object
blutdruck    float64
dtype: object
print(roh.describe().round(2))
       gewicht   alter  blutdruck
count   400.00  400.00     382.00
mean     80.53   45.35     131.85
std      13.20   16.16      11.66
min      -1.00   18.00      92.00
25%      73.95   32.00     123.00
50%      80.50   45.00     130.50
75%      87.80   60.00     141.00
max     124.50   74.00     160.00
print(roh.isna().sum())
gewicht       0
alter         0
standort      0
blutdruck    18
dtype: int64
print(roh[["gewicht", "alter", "blutdruck"]].agg(["min", "max"]))
     gewicht  alter  blutdruck
min     -1.0     18       92.0
max    124.5     74      160.0

Output Zeile für Zeile

Merkmal Typ fehlend Minimum Maximum Auffällig?
gewicht numerisch 0 -1.0 124.5 ja, ein Gewicht von -1 kg gibt es nicht
alter ganzzahlig 0 18 74 nein
standort Text 0 nein (Bern 200, Chur 129, Sion 71)
blutdruck numerisch 18 92 160 die Lücken sind offen deklariert
Beobachtung Erklärung
gewicht hat null fehlende Werte und trotzdem ein Problem Die -1 ist ein Platzhalter: Irgendeine Erfassungsstelle hat “unbekannt” als -1 codiert. Für R und Python ist das eine gültige Zahl.
blutdruck hat 18 offene Lücken Das ist der harmlosere Fall: NA beziehungsweise NaN wird von jeder Kennzahl ausgeschlossen oder erzeugt eine Fehlermeldung.
Der Blick auf Minimum und Maximum findet beides Deshalb steht er vor jeder Verteilungsgrafik. Eine Kennzahl allein verrät den Platzhalter nicht, bei drei Fällen von 400 verschiebt er den Mittelwert um weniger als ein Prozent.
Die Standorte sind ungleich besetzt 200, 129 und 71. Für alle Gruppenvergleiche heisst das: relative Häufigkeiten verwenden.

Die üblichen Platzhalter, nach denen sich zu suchen lohnt:

Wert Herkunft
-1, -99, -999 Codierung für “unbekannt” in älteren Erfassungssystemen
0 bei Merkmalen, die nicht null sein können Gewicht, Blutdruck, Alter
999, 9999 dasselbe wie oben, nur nach oben
1900-01-01, 1970-01-01 Standarddatum bei fehlender Eingabe
leere Zeichenkette gegen NA zwei verschiedene Zustände, die gleich aussehen

Sie alle haben eine Eigenschaft gemeinsam: Sie werden mitgerechnet. Eine offene Lücke fällt auf, ein Platzhalter nicht.

Interpretation und Ergebnissatz

Der Datensatz umfasst 400 Beobachtungen. Beim Blutdruck fehlen 18 Werte (4.5 Prozent), beim Gewicht sind drei Werte mit dem Platzhalter -1 codiert. Beide werden vor der Auswertung als fehlend gekennzeichnet.

Frage und Datenlage

Drei Werte von 400, kann das viel ausmachen? Die Antwort hängt davon ab, welche Kennzahl man ansieht.

Rechnung

daten <- roh
daten$gewicht[daten$gewicht < 0] <- NA

par(mfrow = c(1, 2), mar = c(4, 4, 3, 1))
hist(roh$gewicht, breaks = 30, col = "grey75", border = "white",
     main = "mit Platzhaltern", xlab = "Gewicht (kg)")
hist(daten$gewicht, breaks = 30, col = "grey75", border = "white",
     main = "bereinigt", xlab = "Gewicht (kg)")
par(mfrow = c(1, 1))

round(rbind(
  mit_platzhalter = c(n = sum(!is.na(roh$gewicht)), mittel = mean(roh$gewicht),
                      sd = sd(roh$gewicht), min = min(roh$gewicht),
                      median = median(roh$gewicht)),
  bereinigt = c(n = sum(!is.na(daten$gewicht)),
                mittel = mean(daten$gewicht, na.rm = TRUE),
                sd = sd(daten$gewicht, na.rm = TRUE),
                min = min(daten$gewicht, na.rm = TRUE),
                median = median(daten$gewicht, na.rm = TRUE))
), 2)
                  n mittel    sd  min median
mit_platzhalter 400  80.53 13.20 -1.0   80.5
bereinigt       397  81.15 11.17 49.7   80.6
Abbildung 1: Beispiel 2: links das Gewicht mit den Platzhaltern, rechts bereinigt.
daten = roh.copy()
daten.loc[daten["gewicht"] < 0, "gewicht"] = np.nan

fig, achsen = plt.subplots(1, 2, figsize=(7, 3.2))
achsen[0].hist(roh["gewicht"], bins=30, color="0.75", edgecolor="white")
achsen[0].set_title("mit Platzhaltern")
achsen[1].hist(daten["gewicht"].dropna(), bins=30, color="0.75",
               edgecolor="white")
achsen[1].set_title("bereinigt")
for achse in achsen:
    achse.set_xlabel("Gewicht (kg)")
plt.tight_layout()
plt.show()
Abbildung 2: Beispiel 2: dieselbe Gegenüberstellung in Python.
def kennzahlen(reihe):
    return {"n": int(reihe.notna().sum()), "mittel": reihe.mean(),
            "sd": reihe.std(), "min": reihe.min(), "median": reihe.median()}


print(pd.DataFrame([kennzahlen(roh["gewicht"]), kennzahlen(daten["gewicht"])],
                   index=["mit_platzhalter", "bereinigt"]).round(2))
                   n  mittel     sd   min  median
mit_platzhalter  400   80.53  13.20  -1.0    80.5
bereinigt        397   81.15  11.17  49.7    80.6

Output Zeile für Zeile

Kennzahl mit Platzhaltern bereinigt Abweichung
n 400 397
Mittelwert 80.53 81.15 -0.8 %
Standardabweichung 13.20 11.17 +18 %
Minimum -1.0 49.7
Median 80.5 80.6 +0.1
Beobachtung Erklärung
Der Mittelwert verschiebt sich kaum Drei von 400 Werten, jeder rund 82 kg zu tief, das macht 0.6 kg auf den Mittelwert. Wer nur ihn ansieht, merkt nichts.
Die Standardabweichung ist 18 Prozent zu gross Weil sie quadriert: \((80.5 - (-1))^2 = 6642\) je Platzhalter. Drei davon beherrschen die Summe.
Der Median bewegt sich um 0.1 kg Er zählt nur ab. Drei Extremwerte am unteren Ende verschieben ihn um anderthalb Rangpositionen, bei 400 Werten praktisch nichts.
Das Minimum verrät alles -1.0 gegen 49.7. Es ist die billigste und wirksamste Prüfung, die es gibt.
Im linken Histogramm sitzt ein winziger Balken bei -1 Bei 30 Klassen über einen Bereich von 125 kg ist er kaum zu sehen und die Hauptverteilung wird zusammengedrückt, weil die Achse bis -1 reicht.

Warum das die gefährlichste Sorte Datenfehler ist: Er erzeugt keine Fehlermeldung, verändert die auffälligste Kennzahl kaum und die unauffälligste stark. Ein Bericht mit “Mittelwert 80.5 kg, Standardabweichung 13.2” sieht vollkommen plausibel aus.

Und die Folge zieht sich durch: Jedes Konfidenzintervall, jeder t-Test und jede Regression, die auf dieser Streuung beruhen, sind zu breit beziehungsweise zu schwach.

Was nicht geht: die drei Zeilen stillschweigend löschen. Berichtet gehört, wie viele Werte aus welchem Grund entfernt wurden.

Interpretation und Ergebnissatz

Drei Gewichtsangaben waren mit dem Platzhalter -1 codiert und wurden als fehlend gekennzeichnet (n = 397 statt 400). Dadurch sinkt die Standardabweichung von 13.20 auf 11.17 kg, während sich der Mittelwert nur um 0.6 kg ändert.

Frage und Datenlage

Die Erwartung für den Blutdruck war notiert: Werte zwischen 90 und 180, eingipflig, leicht rechtsschief, Mittelwert um 130. Stimmt das Bild damit überein?

Rechnung

ggplot(roh, aes(blutdruck)) +
  geom_histogram(binwidth = 1, fill = "grey70", colour = "white") +
  labs(x = "Blutdruck", y = "Anzahl")

round(c(n = sum(!is.na(roh$blutdruck)),
        mittel = mean(roh$blutdruck, na.rm = TRUE),
        median = median(roh$blutdruck, na.rm = TRUE),
        minimum = min(roh$blutdruck, na.rm = TRUE),
        maximum = max(roh$blutdruck, na.rm = TRUE)), 2)
      n  mittel  median minimum maximum 
 382.00  131.85  130.50   92.00  160.00 
# Der Verdacht: wie oft enden die Werte auf 0 oder 5?
endziffern <- table(roh$blutdruck %% 10)
round(prop.table(endziffern), 3)

    0     1     2     3     4     5     6     7     8     9 
0.204 0.081 0.076 0.055 0.058 0.259 0.065 0.058 0.073 0.071 
round(sum(prop.table(endziffern)[c("0", "5")]), 3)
[1] 0.463
Abbildung 3: Beispiel 3: Verteilung des Blutdrucks mit Klassenbreite 1. Der Bereich stimmt, die Form ist auffällig gezackt.
werte = roh["blutdruck"].dropna()

fig, achse = plt.subplots(figsize=(7, 3.2))
achse.hist(werte, bins=int(werte.max() - werte.min()),
           color="0.7", edgecolor="white")
achse.set_xlabel("Blutdruck")
achse.set_ylabel("Anzahl")
plt.tight_layout()
plt.show()
Abbildung 4: Beispiel 3: dieselbe Verteilung mit matplotlib.
print({"n": len(werte), "mittel": round(werte.mean(), 2),
       "median": werte.median(), "minimum": werte.min(),
       "maximum": werte.max()})
{'n': 382, 'mittel': np.float64(131.85), 'median': np.float64(130.5), 'minimum': np.float64(92.0), 'maximum': np.float64(160.0)}
endziffern = (werte % 10).value_counts(normalize=True).sort_index().round(3)
print(endziffern)
blutdruck
0.0    0.204
1.0    0.081
2.0    0.076
3.0    0.055
4.0    0.058
5.0    0.259
6.0    0.065
7.0    0.058
8.0    0.073
9.0    0.071
Name: proportion, dtype: float64
print("Anteil auf 0 oder 5:",
      round(float(endziffern[0.0] + endziffern[5.0]), 3))
Anteil auf 0 oder 5: 0.463

Output Zeile für Zeile

Die Erwartung gegen den Befund:

Erwartet Beobachtet Stimmt?
Werte zwischen 90 und 180 92 bis 160 ja
eingipflig ja ja
Mittelwert um 130 131.85 ja
glatte Form stark gezackt nein

Und die Endziffern, die den Zacken erklären:

Endziffer 0 1 2 3 4 5 6 7 8 9
Anteil 0.204 0.081 0.076 0.055 0.058 0.259 0.065 0.058 0.073 0.071
Beobachtung Erklärung
Erwartet wären 10 Prozent je Endziffer Bei sauber gemessenen Werten.
Beobachtet: 20.4 und 25.9 Prozent auf 0 und 5 Zusammen 46.3 Prozent statt der erwarteten 20.
Die übrigen acht Ziffern liegen bei 5.5 bis 8.1 Prozent Also unter 10, genau um den Betrag, der auf 0 und 5 zusätzlich landet.
Das ist eine Rundungspräferenz Ein Teil der Werte wurde nicht abgelesen, sondern geschätzt und auf fünf gerundet. Hier betrifft es rund 150 der 400 Beobachtungen.

Was das für die Auswertung heisst: Die Genauigkeit ist geringer, als die Zahlen suggerieren. Konkret:

  • Kennzahlen wie Mittelwert und Median bleiben brauchbar, die Rundung streut in beide Richtungen und hebt sich weitgehend auf.
  • Der Anteil unter einem Grenzwert wird unzuverlässig, wenn der Grenzwert selbst auf 0 oder 5 endet. Ein Schwellenwert von 140 trifft mitten in eine Häufung.
  • Feine Unterschiede zwischen Gruppen verschwinden im Rundungsrauschen.

Solche Muster sind fast immer Hinweise auf die Erhebung, nicht auf den untersuchten Gegenstand. Gehäufte Rundwerte, verdächtig glatte Mittelwerte oder identische Wiederholungen entstehen beim Erfassen, nicht in der Natur.

Und sie fallen nur bei Klassenbreite 1 auf. Ein Histogramm mit der üblichen Klassenzahl hätte die Zacken weggeglättet, ein Beispiel dafür, dass man in der Erkundungsphase absichtlich zu fein auflöst.

Interpretation und Ergebnissatz

Die Blutdruckwerte enden zu 46.3 Prozent auf 0 oder 5, erwartet wären 20 Prozent. Ein erheblicher Teil der Werte wurde offenbar gerundet erfasst; die tatsächliche Messgenauigkeit liegt bei etwa 5 mmHg.

Frage und Datenlage

Blutdruck gegen Alter und ändert sich das Bild, wenn man nach Standort trennt?

Rechnung

ggplot(daten, aes(alter, blutdruck)) +
  geom_point(alpha = 0.5, size = 1.3) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE, colour = "black") +
  facet_wrap(~ standort) +
  labs(x = "Alter", y = "Blutdruck")

# Steigung und Niveau je Standort
for (ort in c("Bern", "Chur", "Sion")) {
  teil <- subset(daten, standort == ort & !is.na(blutdruck))
  modell <- lm(blutdruck ~ alter, data = teil)
  cat(sprintf("%-5s n = %3d | Steigung %.3f | Achsenabschnitt %.1f | Mittel %.1f\n",
              ort, nrow(teil), coef(modell)[2], coef(modell)[1],
              mean(teil$blutdruck)))
}
Bern  n = 190 | Steigung 0.406 | Achsenabschnitt 111.8 | Mittel 130.3
Chur  n = 123 | Steigung 0.426 | Achsenabschnitt 118.1 | Mittel 137.6
Sion  n =  69 | Steigung 0.447 | Achsenabschnitt 105.5 | Mittel 125.9
# Und die Korrelationen insgesamt
vollstaendig <- na.omit(daten[, c("alter", "gewicht", "blutdruck")])
round(cor(vollstaendig), 3)
           alter gewicht blutdruck
alter      1.000  -0.034     0.581
gewicht   -0.034   1.000     0.230
blutdruck  0.581   0.230     1.000
Abbildung 5: Beispiel 4: Blutdruck gegen Alter, getrennt nach Standort.
orte = ["Bern", "Chur", "Sion"]
fig, achsen = plt.subplots(1, len(orte), figsize=(7, 3.2), sharey=True)
for achse, ort in zip(achsen, orte):
    teil = daten[daten["standort"] == ort].dropna(subset=["blutdruck"])
    achse.scatter(teil["alter"], teil["blutdruck"], alpha=0.5, s=12,
                  color="0.3")
    k = np.polyfit(teil["alter"], teil["blutdruck"], 1)
    gitter = np.linspace(teil["alter"].min(), teil["alter"].max(), 2)
    achse.plot(gitter, np.polyval(k, gitter), color="black")
    achse.set_title(ort)
    achse.set_xlabel("Alter")
    print(f"{ort:5s} n = {len(teil):3d} | Steigung {k[0]:.3f}"
          f" | Achsenabschnitt {k[1]:.1f} | Mittel {teil['blutdruck'].mean():.1f}")
achsen[0].set_ylabel("Blutdruck")
plt.tight_layout()
plt.show()
Abbildung 6: Beispiel 4: dieselbe Aufteilung mit matplotlib.
vollstaendig = daten[["alter", "gewicht", "blutdruck"]].dropna()
print(vollstaendig.corr().round(3))
           alter  gewicht  blutdruck
alter      1.000   -0.034      0.581
gewicht   -0.034    1.000      0.230
blutdruck  0.581    0.230      1.000

Output Zeile für Zeile

Standort n Steigung je Jahr Achsenabschnitt mittlerer Blutdruck
Bern 190 0.406 111.8 130.3
Chur 123 0.426 118.1 137.6
Sion 69 0.447 105.5 125.9
Beobachtung Erklärung
Die Steigungen sind praktisch gleich 0.41 bis 0.45 mmHg je Lebensjahr. Der Zusammenhang zwischen Alter und Blutdruck ist an allen drei Standorten derselbe.
Die Niveaus unterscheiden sich Chur liegt rund 12 mmHg über Sion. Die drei Geraden verlaufen parallel, nur versetzt.
Das ist der Unterschied zwischen Versatz und Wechselwirkung Verschiedene Achsenabschnitte bei gleicher Steigung heissen: Der Standort verschiebt das Niveau, ändert aber nicht den Alterseffekt. Verschiedene Steigungen wären eine Wechselwirkung, dann müsste man je Standort getrennt berichten.
Die Korrelationen Alter mit Blutdruck 0.581, Gewicht mit Blutdruck 0.230

Warum die Aufteilung nach einer dritten Variablen zum Pflichtprogramm gehört: Ohne sie hätte man einen Gesamtzusammenhang zwischen Alter und Blutdruck gesehen und die Standortunterschiede in der Streuung versteckt. Im Extremfall kann eine Aufteilung den Zusammenhang sogar umkehren, das ist Simpsons Paradoxon, siehe multiple Regression.

Die Faustregel: Nach jeder Variablen aufteilen, die inhaltlich als Erklärung in Frage kommt, und zwar bevor man einen Gesamtbefund formuliert. Bei drei bis fünf Kandidaten ist das schnell erledigt; bei zwanzig braucht es eine Auswahl aus dem Fachwissen.

Und die Gegenwarnung: Wer lange genug aufteilt, findet immer eine Gruppe, in der etwas auffällig aussieht. Deshalb ist jeder so gefundene Befund vorläufig, bis er an neuen Daten geprüft ist.

Interpretation und Ergebnissatz

Der Blutdruck steigt an allen drei Standorten mit etwa 0.4 mmHg je Lebensjahr (0.41 bis 0.45). Die Standorte unterscheiden sich im Niveau: Chur liegt im Mittel bei 137.6 mmHg, Bern bei 130.3 und Sion bei 125.9. Der Befund ist explorativ und für eine Prüfung an neuen Daten vorzumerken.

Frage und Datenlage

Sind die Altersgruppen über die Standorte gleich verteilt? Wenn nicht, wäre der Standortunterschied aus Beispiel 4 teilweise ein Alterseffekt.

Rechnung

daten$altersgruppe <- cut(daten$alter, breaks = c(17, 35, 55, 75),
                          labels = c("18-35", "36-55", "56-74"))

tab <- table(daten$standort, daten$altersgruppe)
addmargins(tab)
      
       18-35 36-55 56-74 Sum
  Bern    67    69    64 200
  Chur    43    43    43 129
  Sion    25    21    25  71
  Sum    135   133   132 400
round(prop.table(tab, margin = 1), 2)      # Anteile je Standort
      
       18-35 36-55 56-74
  Bern  0.34  0.34  0.32
  Chur  0.33  0.33  0.33
  Sion  0.35  0.30  0.35
par(mar = c(4, 4, 2, 6))
barplot(t(prop.table(tab, margin = 1)), col = grey.colors(3),
        legend.text = TRUE, xlab = "Standort", ylab = "Anteil",
        args.legend = list(x = "right", inset = -0.22, bty = "n"))
par(mar = c(5, 4, 4, 2) + 0.1)

# Und die Boxplots des Blutdrucks je Standort
round(tapply(daten$blutdruck, daten$standort, median, na.rm = TRUE), 1)
Bern Chur Sion 
 130  140  122 
Abbildung 7: Beispiel 5: Altersgruppen je Standort als gestapelte Anteile.
daten["altersgruppe"] = pd.cut(daten["alter"], bins=[17, 35, 55, 75],
                               labels=["18-35", "36-55", "56-74"])

tab = pd.crosstab(daten["standort"], daten["altersgruppe"], margins=True)
print(tab)
altersgruppe  18-35  36-55  56-74  All
standort                              
Bern             67     69     64  200
Chur             43     43     43  129
Sion             25     21     25   71
All             135    133    132  400
roh_tab = pd.crosstab(daten["standort"], daten["altersgruppe"])
anteile = roh_tab.div(roh_tab.sum(axis=1), axis=0)
print(anteile.round(2))
altersgruppe  18-35  36-55  56-74
standort                         
Bern           0.34   0.34   0.32
Chur           0.33   0.33   0.33
Sion           0.35   0.30   0.35
fig, achse = plt.subplots(figsize=(7, 3))
unten = np.zeros(len(anteile))
for spalte, farbe in zip(anteile.columns, ["0.3", "0.55", "0.8"]):
    achse.bar(anteile.index, anteile[spalte], bottom=unten, color=farbe,
              label=spalte)
    unten += anteile[spalte].values
achse.set_xlabel("Standort")
achse.set_ylabel("Anteil")
achse.legend(frameon=False, bbox_to_anchor=(1.02, 1), loc="upper left")
plt.tight_layout()
plt.show()
Abbildung 8: Beispiel 5: dieselbe Darstellung in Python.
print(daten.groupby("standort")["blutdruck"].median().round(1))
standort
Bern    130.0
Chur    140.0
Sion    122.0
Name: blutdruck, dtype: float64

Output Zeile für Zeile

Absolut:

Standort 18 bis 35 36 bis 55 56 bis 74 Summe
Bern 67 69 64 200
Chur 43 43 43 129
Sion 25 21 25 71

Als Zeilenanteile:

Standort 18 bis 35 36 bis 55 56 bis 74
Bern 0.34 0.34 0.32
Chur 0.33 0.33 0.33
Sion 0.35 0.30 0.35
Beobachtung Erklärung
Die absoluten Zahlen sind nicht vergleichbar Bern hat in jeder Altersgruppe die meisten Fälle, weil Bern insgesamt die meisten hat.
Die Anteile sind es Und sie liegen alle bei rund einem Drittel. Die Altersverteilung ist über die Standorte praktisch identisch.
Was daraus für Beispiel 4 folgt Der Standortunterschied im Blutdruck ist kein verkappter Alterseffekt. Wären in Chur überwiegend ältere Personen erfasst, hätte man den Unterschied damit erklären können.
Die Mediane des Blutdrucks je Standort 130, 140 und 122 bestätigen das Bild aus Beispiel 4 auch robust, also ohne Einfluss einzelner Extremwerte.

Die Reihenfolge, in der diese beiden Beispiele stehen, ist kein Zufall. Erst fällt ein Unterschied auf (Beispiel 4), dann wird nach der naheliegendsten Alternativerklärung gesucht (Beispiel 5). So sieht die Schleife aus, aus der EDA besteht:

Frage → Grafik oder Kennzahl → Befund → nächste Frage aus dem Befund

Und sie hört nicht auf, weil die Daten erschöpft wären, sondern weil einem keine Alternativerklärung mehr einfällt, die man prüfen könnte. Genau deshalb ist das Ergebnis eine Hypothese und kein Nachweis.

Interpretation und Ergebnissatz

Die Altersgruppen sind über die drei Standorte gleichmässig verteilt (je rund ein Drittel je Gruppe, n = 200, 129 und 71). Der beobachtete Blutdruckunterschied zwischen den Standorten lässt sich damit nicht durch eine unterschiedliche Altersstruktur erklären.

Verständnisfragen

Ein Merkmal hat null fehlende Werte, aber ein Minimum von -1 bei einer Grösse, die nicht negativ sein kann. Was liegt vor?

Ein Platzhalter für “unbekannt”, der als Zahl gespeichert wurde
Richtig. Er wird von jeder Kennzahl mitgerechnet, ohne eine Warnung auszulösen. Der Blick auf Minimum und Maximum jedes Merkmals findet solche Fälle.
Ein Messfehler
Möglich, aber -1 bei genau drei Fällen sieht nach Codierung aus, nicht nach Messung.
Nichts Besonderes, drei von 400 fallen nicht ins Gewicht
Für den Mittelwert stimmt das ungefähr; die Standardabweichung wächst um 18 Prozent.

Warum fällt ein Platzhalter beim Mittelwert kaum auf, bei der Standardabweichung aber deutlich?

Weil die Standardabweichung die Abweichungen quadriert
Richtig. Ein Wert 80 Einheiten daneben trägt zum Mittelwert 80/n bei, zur Varianz aber \(80^2/n\). Deshalb ist die Streuung die empfindlichere Anzeige.
Weil der Mittelwert robuster ist als die Standardabweichung
Robust ist keines von beiden; sie sind nur verschieden empfindlich.
Weil der Platzhalter negativ ist
Das Vorzeichen spielt keine Rolle, nur der Abstand zum Rest.

Die Endziffern eines Messwerts verteilen sich zu 46 Prozent auf 0 und 5. Was folgt daraus?

Ein Teil der Werte wurde gerundet erfasst
Richtig. Erwartet wären 20 Prozent. Solche Muster verraten etwas über die Erhebung, nicht über den untersuchten Gegenstand und die tatsächliche Genauigkeit ist geringer, als die Zahlen suggerieren.
Die Daten sind gefälscht
Das ist eine viel stärkere Behauptung. Rundung beim Ablesen ist die harmlose und häufigste Erklärung.
Die Stichprobe ist zu klein
Bei 382 Werten wäre die Abweichung von 20 auf 46 Prozent durch Zufall ausgeschlossen.

Drei Streudiagramme zeigen für drei Standorte praktisch dieselbe Steigung, aber verschiedene Achsenabschnitte. Wie ist das zu deuten?

Der Standort verschiebt das Niveau, ändert aber den Alterseffekt nicht
Richtig. Parallele Geraden heissen Versatz ohne Wechselwirkung. Verschiedene Steigungen wären eine Wechselwirkung, und dann müsste man je Standort getrennt berichten.
Der Standort hat keinen Einfluss
Er hat einen, nämlich auf das Niveau.
Die Standorte sind nicht vergleichbar
Gerade weil die Steigungen übereinstimmen, sind sie es.

Eine explorative Analyse findet in einer von zwölf Untergruppen einen deutlichen Unterschied. Was ist damit gezeigt?

Eine Hypothese, die an neuen Daten zu prüfen ist
Richtig. Wer lange genug aufteilt, findet immer eine auffällige Gruppe. Ein Test auf denselben Daten prüft nichts, weil die Fragestellung aus ihnen stammt.
Ein Effekt in dieser Untergruppe
Möglich, aber nicht belegt.
Nichts, Untergruppenanalysen sind wertlos
Sie sind das Werkzeug, um Hypothesen zu finden. Nur nachweisen können sie nichts.

Verlinkte Ressourcen