Mathematik für Biologiestudierende II¶
Sommersemester 2025
22.04.2025
© 2025 Prof. Dr. Rüdiger W. Braun
import numpy as np
np.set_printoptions(legacy='1.21')
import pandas as pd
from scipy import stats
import seaborn as sns
sns.set_theme()
Post-hoc Analyse¶
- Wenn die ANOVA einen signifikanten Unterschied zwischen den Gruppen gezeigt hat, dann versucht man mit der post-hoc Analyse herauszubekommen, zwischen welchen einzelnen Gruppen signifikante Unterschiede bestehen
- Die post-hoc Analyse muss für multiple Vergleiche korrigiert werden
Beispiel Zitronen¶
df = pd.read_csv("http://reh.math.uni-duesseldorf.de/~braun/bio2425/zitronen.csv")
df.head()
| Vitamin_C_Gehalt | Land | |
|---|---|---|
| 0 | 494.5 | Spanien |
| 1 | 499.2 | Spanien |
| 2 | 494.3 | Spanien |
| 3 | 478.0 | Spanien |
| 4 | 500.1 | Spanien |
Die Tabelle (mit erfundenen Daten) zeigt den Vitamin C Gehalt in [mg] pro [kg] von Zitronen aus verschiedenen Ländern
df.Land.value_counts()
Land Spanien 8 Italien 8 Griechenland 8 Marokko 8 Indien 8 Name: count, dtype: int64
sns.displot(df, x='Vitamin_C_Gehalt', hue='Land', multiple='stack');
spanien = df[df.Land=='Spanien'].Vitamin_C_Gehalt
italien = df[df.Land=='Italien'].Vitamin_C_Gehalt
griechenland = df[df.Land=='Griechenland'].Vitamin_C_Gehalt
marokko = df[df.Land=='Marokko'].Vitamin_C_Gehalt
indien = df[df.Land=='Indien'].Vitamin_C_Gehalt
stats.f_oneway(spanien, italien, griechenland, marokko, indien)
F_onewayResult(statistic=11.873757820342005, pvalue=3.373341669675729e-06)
Die Unterschiede zwischen den Vitamin C Gehalten sind signifikant
Paarvergleiche¶
- Wir könnten zwischen je zwei Gruppen die Paarvergleiche zu Fuß ausrechnen und Bonferroni-korrigieren
- Dieser Prozess ist aber implementiert
from statsmodels.sandbox.stats.multicomp import MultiComparison
Achtung: Hier wird irgendwann der Bestandteil sandbox überflüssig
muc = MultiComparison(df.Vitamin_C_Gehalt, df.Land)
muc = MultiComparison(daten_liste, gruppen_liste)
- das erste Element von
daten_listegehört zur ersten Gruppe in Gruppenliste - das zweite Element von
daten_listegehört zur zweiten Gruppe in Gruppenliste - usw.
muc.allpairtest(test, alpha, method)
- Paarvergleiche zwischen allen Paaren von Gruppen aus der Gruppenliste, mit der
mucangelegt wurde testist der einzusetzende Testalphadas Signifikanzniveau (Standardwert ist 0.05)methoddie Korrekturmethode für das multiple Testen, für uns relevant:bonferroni: Bonferroniholm: Bonferroni-Holm
res = muc.allpairtest(stats.ttest_ind, method='bonferroni')
res[0]
| group1 | group2 | stat | pval | pval_corr | reject |
|---|---|---|---|---|---|
| Griechenland | Indien | -4.9524 | 0.0002 | 0.0021 | True |
| Griechenland | Italien | 1.113 | 0.2845 | 1.0 | False |
| Griechenland | Marokko | -3.5339 | 0.0033 | 0.0331 | True |
| Griechenland | Spanien | -1.9478 | 0.0718 | 0.7178 | False |
| Indien | Italien | 6.2008 | 0.0 | 0.0002 | True |
| Indien | Marokko | 0.3183 | 0.7549 | 1.0 | False |
| Indien | Spanien | 3.3226 | 0.005 | 0.0503 | False |
| Italien | Marokko | -4.3312 | 0.0007 | 0.0069 | True |
| Italien | Spanien | -3.3042 | 0.0052 | 0.0522 | False |
| Marokko | Spanien | 2.2786 | 0.0389 | 0.389 | False |
Nur vier der Paarvergleiche sind signifikant, wenn Bonferroni korrigiert wird
Dasselbe mit Bonferroni-Holm
res = muc.allpairtest(stats.ttest_ind, method='holm')
res[0]
| group1 | group2 | stat | pval | pval_corr | reject |
|---|---|---|---|---|---|
| Griechenland | Indien | -4.9524 | 0.0002 | 0.0019 | True |
| Griechenland | Italien | 1.113 | 0.2845 | 0.5689 | False |
| Griechenland | Marokko | -3.5339 | 0.0033 | 0.0231 | True |
| Griechenland | Spanien | -1.9478 | 0.0718 | 0.2153 | False |
| Indien | Italien | 6.2008 | 0.0 | 0.0002 | True |
| Indien | Marokko | 0.3183 | 0.7549 | 0.7549 | False |
| Indien | Spanien | 3.3226 | 0.005 | 0.0302 | True |
| Italien | Marokko | -4.3312 | 0.0007 | 0.0055 | True |
| Italien | Spanien | -3.3042 | 0.0052 | 0.0302 | True |
| Marokko | Spanien | 2.2786 | 0.0389 | 0.1556 | False |
- Wenn Bonferroni-Holm korrigiert wird, dann sind sechs der Paarvergleiche signifikant
- Es hängt von der Fächerkultur ab, ob Bonferroni-Holm akzeptiert wird
Bonferroni-Holm¶
- n multiple Vergleiche werden durchgeführt
- Die p-Werte werden der Größe nach geordnet
- der kleinste p-Wert muss signifikant zu $\frac\alpha n$ sein
- deswegen wird der $n$-fache $p$-Wert angezeigt
- der zweitkleinste zu $\frac\alpha{n-1}$
- der $n-1$-fache $p$-Wert wird angezeigt
- drittkleinste zu $\frac\alpha{n-2}$
- usw.
- der größte zum Niveau $\alpha$
Bei den Zitronen
- der kleinste p-Wert ist der des Vergleichs zwischen Indien und Italien
- er ist unkorrigiert gleich 2.311e-5
- es gibt 10 Paarvergleiche, also ist der Bonferroni-korrigierte Wert gleich 0.0002311
- die Bonferroni-Holm Korrektur führt zum selben Wert
- der zweitkleinste p-Wert ist der des Vergleichs zwischen Griechenland und Indien
- er ist unkorrigiert gleich 2.125e-4
- also mit Bonferroni-Korrektur gleich 0.002125
- nach Bonferroni-Holm ist der gleich 9 * 2.125e-4 = 0.001913
Ablesung genauerer Werte¶
- Woher weiss ich die genauen p-Werte?
res = muc.allpairtest(stats.ttest_ind, method='bonferroni')ist ein Paar mit zwei Einträgenres[0]ist die leserfreundlich formatierte Tabelle- die genauen Werte stehen in
res[1]
res[1]
(array([[-4.95235073e+00, 2.12516086e-04],
[ 1.11298427e+00, 2.84460819e-01],
[-3.53387275e+00, 3.30511610e-03],
[-1.94781061e+00, 7.17760991e-02],
[ 6.20082296e+00, 2.31091193e-05],
[ 3.18345880e-01, 7.54921930e-01],
[ 3.32261512e+00, 5.03074269e-03],
[-4.33123161e+00, 6.90566981e-04],
[-3.30423221e+00, 5.21815835e-03],
[ 2.27858634e+00, 3.88961763e-02]]),
array([ True, False, True, False, True, False, True, True, True,
False]),
array([1.91264477e-03, 5.68921637e-01, 2.31358127e-02, 2.15328297e-01,
2.31091193e-04, 7.54921930e-01, 3.01844561e-02, 5.52453585e-03,
3.01844561e-02, 1.55584705e-01]),
0.005116196891823743,
0.005)
- Der erste Array ist zweidimensional und enthält die genauen Werte der Statistik und die unkorrigierten p-Werte
- wichtig ist der dritte Array, der die korrigierten p-Werte in der Reihenfolge enthält in der die Paarvergleiche in der Tabelle aufgeführt sind
p_werte_korrigiert = res[1][2]
p_werte_korrigiert
array([1.91264477e-03, 5.68921637e-01, 2.31358127e-02, 2.15328297e-01,
2.31091193e-04, 7.54921930e-01, 3.01844561e-02, 5.52453585e-03,
3.01844561e-02, 1.55584705e-01])
stats.f_oneway(spanien, italien, griechenland, marokko, indien).pvalue
3.373341669675729e-06
Der p-Wert der ANOVA ist kleiner als 5.0E-6. Daher ist zu diesem Signifikanzniveau nachgewiesen, dass Zitronen aus verschiedenen Ländern unterschiedliche Vitamin C Gehalte haben. Wir rechnen die post-hoc Analyse für dieses Signifikanzniveau
res = muc.allpairtest(stats.ttest_ind, alpha=5.0E-6, method='bonferroni')
res[0]
| group1 | group2 | stat | pval | pval_corr | reject |
|---|---|---|---|---|---|
| Griechenland | Indien | -4.9524 | 0.0002 | 0.0021 | False |
| Griechenland | Italien | 1.113 | 0.2845 | 1.0 | False |
| Griechenland | Marokko | -3.5339 | 0.0033 | 0.0331 | False |
| Griechenland | Spanien | -1.9478 | 0.0718 | 0.7178 | False |
| Indien | Italien | 6.2008 | 0.0 | 0.0002 | False |
| Indien | Marokko | 0.3183 | 0.7549 | 1.0 | False |
| Indien | Spanien | 3.3226 | 0.005 | 0.0503 | False |
| Italien | Marokko | -4.3312 | 0.0007 | 0.0069 | False |
| Italien | Spanien | -3.3042 | 0.0052 | 0.0522 | False |
| Marokko | Spanien | 2.2786 | 0.0389 | 0.389 | False |
Keiner der Paarvergleiche ist signifikant.
Behandlung von NaN¶
df = sns.load_dataset('penguins')
df.head()
| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex | |
|---|---|---|---|---|---|---|---|
| 0 | Adelie | Torgersen | 39.1 | 18.7 | 181.0 | 3750.0 | Male |
| 1 | Adelie | Torgersen | 39.5 | 17.4 | 186.0 | 3800.0 | Female |
| 2 | Adelie | Torgersen | 40.3 | 18.0 | 195.0 | 3250.0 | Female |
| 3 | Adelie | Torgersen | NaN | NaN | NaN | NaN | NaN |
| 4 | Adelie | Torgersen | 36.7 | 19.3 | 193.0 | 3450.0 | Female |
df.island.value_counts()
island Biscoe 168 Dream 124 Torgersen 52 Name: count, dtype: int64
g1 = df[df.island=='Biscoe'].body_mass_g.dropna()
g2 = df[df.island=='Dream'].body_mass_g.dropna()
g3 = df[df.island=='Torgersen'].body_mass_g.dropna()
stats.f_oneway(g1, g2, g3)
F_onewayResult(statistic=110.00796506232122, pvalue=1.5151291424015603e-37)
allpairtestbenötigt alle Daten in derselben Tabelle
- Wir müssen also in der Ausgangstabelle alle Zeilen löschen, in denen das Gewicht fehlt
df_dropped = df[df.body_mass_g.notnull()]
df_dropped
| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex | |
|---|---|---|---|---|---|---|---|
| 0 | Adelie | Torgersen | 39.1 | 18.7 | 181.0 | 3750.0 | Male |
| 1 | Adelie | Torgersen | 39.5 | 17.4 | 186.0 | 3800.0 | Female |
| 2 | Adelie | Torgersen | 40.3 | 18.0 | 195.0 | 3250.0 | Female |
| 4 | Adelie | Torgersen | 36.7 | 19.3 | 193.0 | 3450.0 | Female |
| 5 | Adelie | Torgersen | 39.3 | 20.6 | 190.0 | 3650.0 | Male |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 338 | Gentoo | Biscoe | 47.2 | 13.7 | 214.0 | 4925.0 | Female |
| 340 | Gentoo | Biscoe | 46.8 | 14.3 | 215.0 | 4850.0 | Female |
| 341 | Gentoo | Biscoe | 50.4 | 15.7 | 222.0 | 5750.0 | Male |
| 342 | Gentoo | Biscoe | 45.2 | 14.8 | 212.0 | 5200.0 | Female |
| 343 | Gentoo | Biscoe | 49.9 | 16.1 | 213.0 | 5400.0 | Male |
342 rows × 7 columns
- man möchte testen
df.body_mass_g==np.nan - das geht aber nicht, weil
nanbesondere Rechenregeln hat - stattdessen prüft die Methode
notnull()darauf, ob ein Element ungleichNaNist - das Gegenteil von
notnullistisnull
Im Gegensatz dazu entfernt die folgende Operation alle Zeilen, in denen mindestens ein Wert fehlt
df2 = df.dropna()
df2
| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex | |
|---|---|---|---|---|---|---|---|
| 0 | Adelie | Torgersen | 39.1 | 18.7 | 181.0 | 3750.0 | Male |
| 1 | Adelie | Torgersen | 39.5 | 17.4 | 186.0 | 3800.0 | Female |
| 2 | Adelie | Torgersen | 40.3 | 18.0 | 195.0 | 3250.0 | Female |
| 4 | Adelie | Torgersen | 36.7 | 19.3 | 193.0 | 3450.0 | Female |
| 5 | Adelie | Torgersen | 39.3 | 20.6 | 190.0 | 3650.0 | Male |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 338 | Gentoo | Biscoe | 47.2 | 13.7 | 214.0 | 4925.0 | Female |
| 340 | Gentoo | Biscoe | 46.8 | 14.3 | 215.0 | 4850.0 | Female |
| 341 | Gentoo | Biscoe | 50.4 | 15.7 | 222.0 | 5750.0 | Male |
| 342 | Gentoo | Biscoe | 45.2 | 14.8 | 212.0 | 5200.0 | Female |
| 343 | Gentoo | Biscoe | 49.9 | 16.1 | 213.0 | 5400.0 | Male |
333 rows × 7 columns
- Das sind 9 Zeilen weniger
- Es gibt also 9 Pinguine, von denen das Gewicht bestimmt werden konnte, mindestens ein anderer Wert aber nicht
Die unbestimmte Größe ist übrigens das Geschlecht:
df_dropped[df_dropped.sex.isnull()]
| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex | |
|---|---|---|---|---|---|---|---|
| 8 | Adelie | Torgersen | 34.1 | 18.1 | 193.0 | 3475.0 | NaN |
| 9 | Adelie | Torgersen | 42.0 | 20.2 | 190.0 | 4250.0 | NaN |
| 10 | Adelie | Torgersen | 37.8 | 17.1 | 186.0 | 3300.0 | NaN |
| 11 | Adelie | Torgersen | 37.8 | 17.3 | 180.0 | 3700.0 | NaN |
| 47 | Adelie | Dream | 37.5 | 18.9 | 179.0 | 2975.0 | NaN |
| 246 | Gentoo | Biscoe | 44.5 | 14.3 | 216.0 | 4100.0 | NaN |
| 286 | Gentoo | Biscoe | 46.2 | 14.4 | 214.0 | 4650.0 | NaN |
| 324 | Gentoo | Biscoe | 47.3 | 13.8 | 216.0 | 4725.0 | NaN |
| 336 | Gentoo | Biscoe | 44.5 | 15.7 | 217.0 | 4875.0 | NaN |
muc = MultiComparison(df_dropped.body_mass_g, df_dropped.island)
res = muc.allpairtest(stats.ttest_ind, method='bonferroni')
res[0]
| group1 | group2 | stat | pval | pval_corr | reject |
|---|---|---|---|---|---|
| Biscoe | Dream | 12.9663 | 0.0 | 0.0 | True |
| Biscoe | Torgersen | 8.7781 | 0.0 | 0.0 | True |
| Dream | Torgersen | 0.0924 | 0.9265 | 1.0 | False |
- Wir haben einen Störfaktor (engl. confounding variable)
- Das ist eine unbeachtete Größe, die die Zielvariable beeinflusst
- Das Gewicht hängt von der Art ab
- Nicht auf allen Inseln sind alle Arten im selben Umfang vertreten
sns.displot(df, x="body_mass_g", col="island", hue="species", multiple='stack');
muc = MultiComparison(df_dropped.body_mass_g, df_dropped.species)
res = muc.allpairtest(stats.ttest_ind, method='bonferroni')
res[0]
| group1 | group2 | stat | pval | pval_corr | reject |
|---|---|---|---|---|---|
| Adelie | Chinstrap | -0.5081 | 0.6119 | 1.0 | False |
| Adelie | Gentoo | -23.6136 | 0.0 | 0.0 | True |
| Chinstrap | Gentoo | -19.1032 | 0.0 | 0.0 | True |
- Adelie- und Zügelpinguine wiegen gleich viel
- Eselspinguine unterscheiden sich im Gewicht
- Eselspinguine gibt es nur auf Biscoe