Standardisierte Effektgrößen
Zeit: 20 min | Schwierigkeit: Einsteiger
Theorie: Effekte auf gemeinsamer Skala
Der unstandardisierte Effekt ist direkt interpretierbar und praktisch relevant. Er hat jedoch zwei Grenzen.
Erstens sagt er nichts darüber aus, wie gut der Effekt im Verhältnis zur natürlichen Schwankung der Messung erkennbar ist. Zwei Studien finden denselben mittleren Zeitgewinn von 5 Minuten. Bei der einen beträgt die typische Streuung 2 Minuten, bei der anderen 20 Minuten. Im ersten Fall ist die Verbesserung für fast jeden Fahrgast spürbar, im zweiten geht sie im Rauschen unter. Der rohe Mittelwertsunterschied allein zeigt das nicht.
Zweitens lassen sich Effekte nicht direkt vergleichen, wenn dasselbe Konstrukt mit unterschiedlichen Messinstrumenten erfasst wurde. Zwei Forschungsteams messen die Kundenzufriedenheit: Team Nord mit einer 5-Punkte-Skala, Team Süd mit einer 100-Punkte-Skala. Team Nord findet eine Verbesserung von 0.5 Punkten, Team Süd von 10 Punkten. Handelt es sich um denselben Effekt? Anhand der Rohwerte ist das nicht zu beurteilen.
Cohens d adressiert beide Probleme, indem es den Effekt in Einheiten der Standardabweichung ausdrückt:
\[d = \frac{|\bar{M}_1 - \bar{M}_2|}{SD}\]
Ein d = 0.5 bedeutet: Die Gruppen-Mittelwerte liegen eine halbe Standardabweichung auseinander. Weil der Effekt nun relativ zur natürlichen Variabilität der Messung ausgedrückt ist, lassen sich Studien mit unterschiedlichen Skalen direkt vergleichen.
Wichtiger Vorbehalt: Die Richtwerte von Cohen (0.2, 0.5, 0.8) sind grobe Orientierungen, keine inhaltlichen Kriterien. Ein d = 0.3 kann in einem Kontext wichtig und in einem anderen trivial sein. Die Interpretation erfordert immer den sachlichen Kontext.
Beispiel SmartRail: Zwei Aspekte, eine Formel
Aspekt 1: Entdeckbarkeit. In Kapitel 2.3 haben wir gesehen, dass derselbe Mittelwertsunterschied bei hoher Streuung kaum spürbar ist. Cohens d quantifiziert genau dieses Signal-Rausch-Verhältnis. Ein kleines d bedeutet, der Effekt geht im Rauschen unter; ein großes d bedeutet, er ist klar erkennbar.
Aspekt 2: Vergleichbarkeit. Zwei Forschungsteams messen die Zufriedenheit der Fahrgäste mit SmartRail, nutzen aber unterschiedliche Fragebögen. Team Nord verwendet eine 5-Punkte-Skala, Team Süd eine 100-Punkte-Skala. Die Rohwerte sind nicht direkt vergleichbar. Cohens d schafft eine gemeinsame Skala: Beide Teams können dasselbe d berichten, auch wenn ihre Punktwerte weit auseinanderliegen.
Deine Aufgabe
- Entdeckbarkeit (Aspekt 1): Belasse den Effekt bei 5 Minuten und variiere die Streuung. Beobachte, wie d sinkt und die Verteilungen immer stärker überlappen. Bei kleinem d geht der Effekt für die einzelne Fahrt praktisch im Rauschen unter.
- Vergleichbarkeit (Aspekt 2): Wechsle zu Aspekt 2 und verschiebe den d-Regler. Die Rohwerte der beiden Teams laufen auseinander, aber d bleibt identisch. Das ist der Mehrwert der Standardisierung: Vergleichbarkeit trotz unterschiedlicher Skalen.
⏳ Die Anwendung wird geladen, dies kann bis zu 30 Sekunden dauern.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 780
from shiny import App, render, ui
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
EFFECT_MIN = 5.0 # fixer Zeitgewinn (Aspekt 1)
MEAN_OLD = 15.0 # Baseline-Verspätung (Aspekt 1)
SD_NORD = 1.0 # Aspekt 2: Instrument Team Nord (5-Punkte-Skala)
SD_SUED = 20.0 # Aspekt 2: Instrument Team Süd (100-Punkte-Skala)
BASE_NORD = 3.2 # Baseline-Zufriedenheit Team Nord
BASE_SUED = 64.0 # Baseline-Zufriedenheit Team Süd
app_ui = ui.page_fluid(
ui.card(
ui.card_header("SmartRail: Cohens d in zwei Kontexten"),
ui.layout_columns(
ui.div(
ui.h5("Aspekt"),
ui.input_radio_buttons(
"aspekt", None,
choices={
"detect": "1. Entdeckbarkeit",
"compare": "2. Vergleichbarkeit",
},
selected="detect"
),
ui.hr(),
ui.panel_conditional(
"input.aspekt === 'detect'",
ui.h5("Streuung einstellen"),
ui.input_slider("sd_val", "Streuung der Verspätung (Min)",
min=1.0, max=20.0, value=5.0, step=0.5),
),
ui.panel_conditional(
"input.aspekt === 'compare'",
ui.h5("Effektgröße einstellen"),
ui.input_slider("d_val", "Cohens d",
min=0.1, max=1.5, value=0.5, step=0.05),
),
ui.hr(),
ui.output_ui("info_panel"),
),
ui.output_plot("main_plot"),
col_widths=(4, 8)
)
)
)
def server(input, output, session):
@render.plot
def main_plot():
aspekt = input.aspekt()
if aspekt == "detect":
sd = input.sd_val()
d = EFFECT_MIN / sd
mean_new = MEAN_OLD - EFFECT_MIN
span = max(4.0 * sd, EFFECT_MIN + 2.0 * sd)
x = np.linspace(mean_new - span, MEAN_OLD + span, 500)
y_old = norm.pdf(x, MEAN_OLD, sd)
y_new = norm.pdf(x, mean_new, sd)
fig, ax = plt.subplots(figsize=(10, 6))
ax.fill_between(x, y_old, alpha=0.45, color='#95a5a6', label='Ohne SmartRail')
ax.fill_between(x, y_new, alpha=0.55, color='#3498db', label='Mit SmartRail')
ax.axvline(MEAN_OLD, color='#7f8c8d', linestyle='dotted', lw=1.5)
ax.axvline(mean_new, color='#2980b9', linestyle='dotted', lw=1.5)
y_top = max(y_old.max(), y_new.max()) * 0.88
ax.annotate("", xy=(mean_new, y_top), xytext=(MEAN_OLD, y_top),
arrowprops=dict(arrowstyle="<->", color='#2c3e50', lw=2))
ax.text((MEAN_OLD + mean_new) / 2, y_top * 1.05,
f'{EFFECT_MIN:.0f} Min', ha='center',
fontweight='bold', fontsize=11, color='#2c3e50')
ax.set_title(f"d = {EFFECT_MIN:.0f} Min / {sd:.1f} Min = {d:.2f}",
fontweight='bold', fontsize=13)
ax.set_xlabel("Verspätung in Minuten")
ax.set_ylabel("Dichte")
ax.legend(loc='upper right')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
fig.subplots_adjust(bottom=0.15, left=0.12, right=0.95, top=0.88)
return fig
else:
d = input.d_val()
eff_nord = d * SD_NORD
eff_sued = d * SD_SUED
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 6))
# Team Nord
span_n = 3.5 * SD_NORD
x_n = np.linspace(BASE_NORD - span_n, BASE_NORD + eff_nord + span_n, 400)
yn_base = norm.pdf(x_n, BASE_NORD, SD_NORD)
yn_new = norm.pdf(x_n, BASE_NORD + eff_nord, SD_NORD)
ax1.fill_between(x_n, yn_base, alpha=0.45, color='#95a5a6', label='Ohne SmartRail')
ax1.fill_between(x_n, yn_new, alpha=0.55, color='#3498db', label='Mit SmartRail')
ax1.axvline(BASE_NORD, color='#7f8c8d', linestyle='dotted', lw=1.5)
ax1.axvline(BASE_NORD + eff_nord, color='#2980b9', linestyle='dotted', lw=1.5)
y_top_n = max(yn_base.max(), yn_new.max()) * 0.88
ax1.annotate("", xy=(BASE_NORD + eff_nord, y_top_n), xytext=(BASE_NORD, y_top_n),
arrowprops=dict(arrowstyle="<->", color='#2c3e50', lw=2))
ax1.text(BASE_NORD + eff_nord / 2, y_top_n * 1.05,
f'+{eff_nord:.2f} Pkt.', ha='center',
fontweight='bold', fontsize=10, color='#2c3e50')
ax1.set_title(f"Team Nord (1-5 Skala)\nd = {eff_nord:.2f} / {SD_NORD:.0f} = {d:.2f}",
fontweight='bold', fontsize=11)
ax1.set_xlabel("Zufriedenheit (1-5 Punkte)")
ax1.set_ylabel("Dichte")
ax1.legend(fontsize=8)
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
# Team Süd
span_s = 3.5 * SD_SUED
x_s = np.linspace(BASE_SUED - span_s, BASE_SUED + eff_sued + span_s, 400)
ys_base = norm.pdf(x_s, BASE_SUED, SD_SUED)
ys_new = norm.pdf(x_s, BASE_SUED + eff_sued, SD_SUED)
ax2.fill_between(x_s, ys_base, alpha=0.45, color='#95a5a6', label='Ohne SmartRail')
ax2.fill_between(x_s, ys_new, alpha=0.55, color='#27ae60', label='Mit SmartRail')
ax2.axvline(BASE_SUED, color='#7f8c8d', linestyle='dotted', lw=1.5)
ax2.axvline(BASE_SUED + eff_sued, color='#229954', linestyle='dotted', lw=1.5)
y_top_s = max(ys_base.max(), ys_new.max()) * 0.88
ax2.annotate("", xy=(BASE_SUED + eff_sued, y_top_s), xytext=(BASE_SUED, y_top_s),
arrowprops=dict(arrowstyle="<->", color='#2c3e50', lw=2))
ax2.text(BASE_SUED + eff_sued / 2, y_top_s * 1.05,
f'+{eff_sued:.1f} Pkt.', ha='center',
fontweight='bold', fontsize=10, color='#2c3e50')
ax2.set_title(f"Team Süd (0-100 Skala)\nd = {eff_sued:.1f} / {SD_SUED:.0f} = {d:.2f}",
fontweight='bold', fontsize=11)
ax2.set_xlabel("Zufriedenheit (0-100 Punkte)")
ax2.set_ylabel("Dichte")
ax2.legend(fontsize=8)
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
fig.subplots_adjust(wspace=0.35, bottom=0.15, top=0.82, left=0.08, right=0.97)
return fig
@render.ui
def info_panel():
aspekt = input.aspekt()
if aspekt == "detect":
sd = input.sd_val()
d = EFFECT_MIN / sd
prob = norm.cdf(d / np.sqrt(2)) * 100
if d >= 0.8:
farbe = "#27ae60"
fazit = "Effekt klar erkennbar."
elif d >= 0.4:
farbe = "#e67e22"
fazit = "Effekt maessig erkennbar."
else:
farbe = "#c0392b"
fazit = "Effekt geht im Rauschen unter."
return ui.div(
ui.tags.p(
"d = ",
ui.tags.span(f"{d:.2f}",
style=f"color:{farbe}; font-weight:bold; font-size:18px;"),
),
ui.tags.p(f"{prob:.0f}% der Fahrten sind besser.",
style="margin:2px 0;"),
ui.tags.p(fazit,
style=f"color:{farbe}; font-style:italic; font-size:12px;"),
style="background:#f8f9fa; padding:12px; border-radius:8px; margin-top:8px;"
)
else:
d = input.d_val()
eff_nord = d * SD_NORD
eff_sued = d * SD_SUED
return ui.div(
ui.tags.p("Team Nord (1-5 Skala):", style="font-weight:bold;"),
ui.tags.p(f"Roheffekt: +{eff_nord:.2f} Pkt. | d = {d:.2f}",
style="color:#3498db; margin:2px 0;"),
ui.tags.hr(style="margin:6px 0;"),
ui.tags.p("Team Süd (0-100 Skala):", style="font-weight:bold;"),
ui.tags.p(f"Roheffekt: +{eff_sued:.1f} Pkt. | d = {d:.2f}",
style="color:#27ae60; margin:2px 0;"),
ui.tags.hr(style="margin:6px 0;"),
ui.tags.p(
"Beide messen denselben Effekt.",
style="font-weight:bold; font-size:12px; color:#2c3e50;"
),
style="background:#f8f9fa; padding:12px; border-radius:8px; "
"margin-top:8px; font-size:13px;"
)
app = App(app_ui, server)