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

  1. 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.
  2. 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)