{ "cells": [ { "cell_type": "markdown", "id": "34e797dc", "metadata": {}, "source": [ "# Przedziały ufności: konstrukcja i interpretacja\n", "\n", "W tym notatniku skupiamy się na **przedziałach ufności (PU)** (*confidence intervals*) oraz na tym, skąd one się biorą (logika „powtarzalnych prób”).\n", "\n", "Ważne: przedział ufności nie jest zdaniem o tym, że „z prawdopodobieństwem 95% prawdziwe $\\mu$ leży w środku”. To jest zdanie o **procedurze**, która w długim szeregu powtórzeń (dla ustalonego poziomu ufności, np. 95%) w ok. 95% prób obejmie prawdziwy parametr.\n" ] }, { "cell_type": "markdown", "id": "6f2bb192", "metadata": {}, "source": [ "## Cele\n", "\n", "- Umieć **zbudować** 95% przedział ufności wokół średniej: krok po kroku.\n", "- Rozumieć **interpretację częstotliwościową**: *pokrycie* w powtarzalnych próbach.\n", "- Zobaczyć, dlaczego przy nieznanym $\\sigma$ pojawia się **rozkład t**.\n", "- Umieć powiązać PU z testem: w teście dwustronnym $\\alpha=0.05$ wartość $\\mu_0$ jest „zgodna z danymi”, jeśli leży w 95% PU.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "98c45656", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "from scipy import stats\n", "\n", "from IPython.display import display\n", "\n", "SEED = 20250116\n", "rng = np.random.default_rng(SEED)\n", "\n", "sns.set_theme(style=\"whitegrid\")\n", "\n", "try:\n", " import ipywidgets as widgets\n", "\n", " WIDGETY_DOSTEPNE = True\n", "except ImportError:\n", " widgets = None\n", " WIDGETY_DOSTEPNE = False" ] }, { "cell_type": "markdown", "id": "c27eb8f6", "metadata": {}, "source": [ "## Centralne Twierdzenie Graniczne - przypomnienie\n", "\n", "Proste symulacje pokażą nam to, co teoretycznie wiemy z Centralnego Twierdzenia Granicznego:\n", "\n", "- że średnie z prób losowych oscylują wokół średniej w populacji,\n", "- a w szczególności, że niezależnie od rozkładu zmiennej w populacji średnie z prób losowych rozkładają się normalnie wokół tej średniej,\n", "- że ich rozproszenie jest tym mniejsze, im mniejsze jest rozproszenie zmiennej w populacji,\n", "- i tym mniejsze, im większa jest liczebność prób,\n", "- a dokładniej, że odchylenie standardowe średnich z prób losowych równe jest odchyleniu standardowemu w populacji podzielonemu przez pierwiastek liczebności prób.\n", "\n", "Taki rozkład teoretyczny $N(\\mu, \\sigma^2/N)$, który nazywamy **rozkładem średniej z próby**, jest rozkładem prawdopodobieństwa, który pozwala nam szacować, jak prawdopodobne jest wylosowanie z danej populacji próby o takiej czy innej średniej.\n", "\n", "Odchylenie standardowe tego rozkładu nazywa się **błędem standardowym średniej**.\n" ] }, { "cell_type": "markdown", "id": "f4df6b53", "metadata": {}, "source": [ "### Przykład z kognitywistyki: czasy reakcji w zadaniu 2AFC\n", "\n", "W wielu badaniach kognitywistycznych mierzymy **czas reakcji** — czas od pojawienia się bodźca do odpowiedzi.\n", "\n", "Jednym z najprostszych paradygmatów jest zadanie **wymuszonego wyboru z dwóch opcji** (skrót: 2AFC): w każdej próbie osoba badana musi wybrać jedną z dwóch odpowiedzi, np. „bodziec był po lewej” albo „bodziec był po prawej”.\n", "\n", "Czasy reakcji w pojedynczych próbach często mają rozkład **prawoskośny** (rzadkie, ale bardzo długie reakcje tworzą „ogon” po prawej). To nie przeszkadza w tym, żeby **średnia z próby** miała rozkład zbliżony do normalnego dla umiarkowanych liczebności $n$ — i to jest praktyczna treść CTG.\n", "\n", "Poniżej tworzymy sztuczną „populację” czasów reakcji (w ms) i porównujemy rozkład pojedynczych czasów z rozkładem średnich z prób o różnej liczebności." ] }, { "cell_type": "code", "execution_count": null, "id": "328acbcd", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED + 10)\n", "\n", "mu_ms = 520\n", "sd_ms = 80\n", "\n", "# Zabawka dydaktyczna: prawoskośny rozkład \"podobny\" do czasów reakcji.\n", "raw = rng.lognormal(mean=0.0, sigma=0.35, size=200_000)\n", "rt_populacja = (raw - raw.mean()) / raw.std() * sd_ms + mu_ms\n", "\n", "repeats = 5_000\n", "sample_sizes = [5, 20, 100]\n", "\n", "sample_means = {}\n", "for n in sample_sizes:\n", " samples = rng.choice(rt_populacja, size=(repeats, n), replace=True)\n", " sample_means[n] = samples.mean(axis=1)\n", "\n", "fig, axes = plt.subplots(1, 1 + len(sample_sizes), figsize=(15, 4))\n", "\n", "sns.histplot(rt_populacja, bins=60, ax=axes[0], color='lightgrey')\n", "axes[0].set_title('Pojedyncze czasy reakcji (\"populacja\")')\n", "axes[0].set_xlabel('czas reakcji (ms)')\n", "axes[0].set_ylabel('liczność')\n", "\n", "for i, n in enumerate(sample_sizes, start=1):\n", " sns.histplot(sample_means[n], bins=60, ax=axes[i], color='steelblue')\n", " axes[i].set_title(f'Średnia z próby (n={n})')\n", " axes[i].set_xlabel('średnia (ms)')\n", " axes[i].set_ylabel('liczność')\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "0ea149dc", "metadata": {}, "source": [ "#### Interaktywnie (opcjonalnie)\n", "\n", "Widżet poniżej pozwala zmieniać liczebność próby `n` i porównać dwa przypadki:\n", "\n", "- rozkład prawoskośny (model „czasów reakcji”),\n", "- rozkład normalny (dla kontrastu).\n", "\n", "Wykres po lewej pokazuje pojedyncze obserwacje, a po prawej — rozkład średniej z próby." ] }, { "cell_type": "code", "execution_count": null, "id": "44159ef5", "metadata": {}, "outputs": [], "source": [ "if not WIDGETY_DOSTEPNE:\n", " print('Widżety nie są dostępne: zainstaluj pakiet ipywidgets, aby korzystać z części interaktywnych.')\n", "else:\n", " MU_MS = 520.0\n", " SD_MS = 80.0\n", "\n", " rng_pop = np.random.default_rng(SEED + 100)\n", " raw = rng_pop.lognormal(mean=0.0, sigma=0.35, size=200_000)\n", " POP_RT = (raw - raw.mean()) / raw.std() * SD_MS + MU_MS\n", " POP_NORMAL = rng_pop.normal(loc=MU_MS, scale=SD_MS, size=200_000)\n", "\n", " rozklad_dropdown = widgets.Dropdown(\n", " options=[\n", " ('Prawoskośny (model czasów reakcji)', 'rt'),\n", " ('Normalny (dla porównania)', 'normalny'),\n", " ],\n", " value='rt',\n", " description='Rozkład',\n", " )\n", "\n", " n_slider = widgets.IntSlider(\n", " value=20,\n", " min=2,\n", " max=200,\n", " step=1,\n", " description='n',\n", " continuous_update=False,\n", " )\n", "\n", " repeats_slider = widgets.IntSlider(\n", " value=3_000,\n", " min=500,\n", " max=10_000,\n", " step=500,\n", " description='powtórzenia',\n", " continuous_update=False,\n", " )\n", "\n", " def pokaz_ctg(rozklad: str, n: int, repeats: int) -> None:\n", " rng = np.random.default_rng(SEED + 101)\n", " n = int(n)\n", " repeats = int(repeats)\n", "\n", " populacja = POP_RT if rozklad == 'rt' else POP_NORMAL\n", "\n", " # Losujemy powtarzane próby i liczymy średnią w każdej z nich.\n", " samples = rng.choice(populacja, size=(repeats, n), replace=True)\n", " means = samples.mean(axis=1)\n", "\n", " fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", "\n", " podprobka = populacja[:50_000]\n", " sns.histplot(podprobka, bins=60, ax=axes[0], color='lightgrey')\n", " axes[0].set_title('Pojedyncze obserwacje')\n", " axes[0].set_xlabel('czas reakcji (ms)')\n", " axes[0].set_ylabel('liczność')\n", "\n", " sns.histplot(means, bins=60, ax=axes[1], color='steelblue')\n", " axes[1].set_title(f'Średnia z próby (n={n})')\n", " axes[1].set_xlabel('średnia (ms)')\n", " axes[1].set_ylabel('liczność')\n", "\n", " plt.tight_layout()\n", " plt.show()\n", "\n", " out = widgets.interactive_output(\n", " pokaz_ctg,\n", " {\n", " 'rozklad': rozklad_dropdown,\n", " 'n': n_slider,\n", " 'repeats': repeats_slider,\n", " },\n", " )\n", "\n", " display(widgets.VBox([rozklad_dropdown, n_slider, repeats_slider, out]))\n" ] }, { "cell_type": "code", "execution_count": null, "id": "9355d284", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(-4, 4, 400)\n", "y = stats.norm.pdf(x)\n", "\n", "plt.figure(figsize=(10, 6))\n", "plt.plot(x, y, color=\"black\")\n", "plt.axvline(0, color=\"grey\", linestyle=\":\", linewidth=2)\n", "\n", "plt.title(\"Rozkład średniej z próby (schemat)\")\n", "plt.xlabel(r\"$\\bar{X}$\")\n", "plt.ylabel(\"gęstość\")\n", "\n", "plt.text(0.15, 0.02, r\"$\\mu$\", fontsize=14)\n", "plt.text(1.9, 0.20, r\"$\\sigma_{\\bar{X}} = \\sigma/\\sqrt{N}$\", fontsize=14)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "95911d7b", "metadata": {}, "source": [ "#### Liczebność próby a błąd standardowy średniej\n", "\n", "- Oczywiście zależy nam na tym, żeby błąd standardowy był jak najmniejszy: dlatego dążymy do dużego $N$.\n", "- Z drugiej strony ten zysk na precyzji jest szczególnie duży przy małych wartościach $N$; im większe $N$, tym mniejsza redukcja błędu standardowego." ] }, { "cell_type": "code", "execution_count": null, "id": "6026e3ef", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(1, 50, 300)\n", "\n", "curves = [\n", " (1 / x**2, r\"Wykres 1: $y = 1/x^2$\", False),\n", " (np.sqrt(x), r\"Wykres 2: $y = \\sqrt{x}$\", False),\n", " (1 / np.sqrt(x), r\"Wykres 3: $y = 1/\\sqrt{x}$\", True),\n", " (x**2, r\"Wykres 4: $y = x^2$\", False),\n", "]\n", "\n", "fig, axes = plt.subplots(2, 2, figsize=(11, 8))\n", "axes = axes.ravel()\n", "\n", "for ax, (y_values, title, is_correct) in zip(axes, curves):\n", " if is_correct:\n", " ax.set_facecolor(\"#eef8ee\")\n", " ax.plot(x, y_values, color=\"seagreen\", linewidth=3)\n", " ax.fill_between(x, y_values, color=\"seagreen\", alpha=0.15)\n", " ax.set_title(title, color=\"seagreen\", fontweight=\"bold\")\n", " ax.text(\n", " 0.03,\n", " 0.92,\n", " r\"właściwy kształt: $SE \\propto 1/\\sqrt{N}$\",\n", " transform=ax.transAxes,\n", " fontsize=10,\n", " color=\"seagreen\",\n", " bbox={\"boxstyle\": \"round,pad=0.3\", \"facecolor\": \"white\", \"edgecolor\": \"seagreen\"},\n", " )\n", " for spine in ax.spines.values():\n", " spine.set_color(\"seagreen\")\n", " spine.set_linewidth(2)\n", " else:\n", " ax.set_facecolor(\"#f7f7f7\")\n", " ax.plot(x, y_values, color=\"0.55\", linewidth=2, linestyle=\"--\")\n", " ax.set_title(title, color=\"0.35\")\n", " for spine in ax.spines.values():\n", " spine.set_color(\"0.75\")\n", "\n", " ax.set_xlabel(\"N\")\n", " ax.set_ylabel(\"wartość\")\n", " ax.grid(alpha=0.15)\n", "\n", "fig.suptitle(\"Jak zmienia się błąd standardowy wraz z N? (intuicja)\")\n", "fig.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "98f5b0a4", "metadata": {}, "source": [ "Potrafiąc skonstruować rozkład średniej z próby, możemy testować prostą hipotezę zerową: **dana próba losowa pochodzi z populacji o takiej i takiej średniej**.\n", "\n", "Jeśli różnica między średnią naszej próby a założoną zgodnie z hipotezą zerową średnią w populacji jest zbyt duża (jeśli nasza średnia wpada w obszar krytyczny), odrzucamy hipotezę zerową.\n", "\n", "Poniżej schematycznie pokazujemy obszar krytyczny dla testu **dwustronnego** przy $\\alpha=0.05$ (po 2.5% w każdym ogonie)." ] }, { "cell_type": "code", "execution_count": null, "id": "7524da43", "metadata": {}, "outputs": [], "source": [ "alpha = 0.05\n", "z_low = stats.norm.ppf(alpha / 2)\n", "z_high = stats.norm.ppf(1 - alpha / 2)\n", "\n", "x = np.linspace(-4, 4, 600)\n", "y = stats.norm.pdf(x)\n", "\n", "fig, ax = plt.subplots(figsize=(10, 5.5))\n", "\n", "ax.plot(x, y, color=\"0.15\", linewidth=2.5)\n", "ax.fill_between(x, 0, y, where=x <= z_low, color=\"#d95f5f\", alpha=0.28)\n", "ax.fill_between(x, 0, y, where=(x >= z_low) & (x <= z_high), color=\"#4c956c\", alpha=0.14)\n", "ax.fill_between(x, 0, y, where=x >= z_high, color=\"#d95f5f\", alpha=0.28)\n", "\n", "ax.axvline(0, color=\"0.45\", linestyle=\":\", linewidth=1.8)\n", "ax.axvline(z_low, color=\"#8c2d2d\", linestyle=\"--\", linewidth=2)\n", "ax.axvline(z_high, color=\"#8c2d2d\", linestyle=\"--\", linewidth=2)\n", "\n", "ax.set_title(\"Standaryzowany rozkład normalny (Z) i obszar krytyczny dla α = 0.05\")\n", "ax.set_xlabel(\"wartość statystyki Z\")\n", "ax.set_ylabel(\"gęstość\")\n", "ax.set_xlim(-4, 4)\n", "ax.set_ylim(0, y.max() + 0.05)\n", "ax.set_xticks([z_low, 0, z_high])\n", "ax.set_xticklabels([f\"{z_low:.2f}\", \"0\", f\"{z_high:.2f}\"])\n", "ax.grid(axis=\"y\", alpha=0.2)\n", "ax.spines[\"top\"].set_visible(False)\n", "ax.spines[\"right\"].set_visible(False)\n", "\n", "ax.text(0, 0.21, \"centralne 95%\", ha=\"center\", fontsize=11, color=\"#2f6f44\")\n", "ax.text(z_low - 0.55, 0.035, \"2.5%\", ha=\"center\", color=\"#8c2d2d\", fontsize=10)\n", "ax.text(z_high + 0.55, 0.035, \"2.5%\", ha=\"center\", color=\"#8c2d2d\", fontsize=10)\n", "ax.text(0, y.max() + 0.02, r\"$Z \\sim N(0,1)$\", ha=\"center\", fontsize=11)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "3f06625a", "metadata": {}, "source": [ "Wygodniej może być wystandaryzować rozkład średniej z próby, czyli doprowadzić go do takiej postaci, w której średnia wynosi 0, a odchylenie standardowe --- 1.\n", "\n", "Taki rozkład „działa” zawsze tak samo, niezależnie od oryginalnej skali naszej zmiennej.\n", "\n", "Standaryzacja (gdy znamy $\\sigma$) prowadzi do statystyki $Z$:\n", "\n", "$$\n", "Z = \\frac{\\bar{X} - \\mu}{\\sigma/\\sqrt{N}}.\n", "$$\n" ] }, { "cell_type": "markdown", "id": "ca2ef753", "metadata": {}, "source": [ "### Jak zapisać warunek wejścia w obszar krytyczny?\n", "\n", "Dla testu dwustronnego przy $\\alpha = 0.05$ odrzucamy $H_0$, gdy średnia z próby wypada poza centralne 95% rozkładu średniej z próby.\n", "\n", "Ten sam warunek można zapisać na trzy równoważne sposoby:\n", "\n", "- bezpośrednio przez kwantyle rozkładu $N(\\mu_0, \\sigma/\\sqrt{N})$,\n", "- przez granice postaci $\\mu_0 \\pm z_{0.975}\\,\\sigma/\\sqrt{N}$,\n", "- po standaryzacji: $|Z| > z_{0.975}$, gdzie\n", "\n", "$$\n", "Z = \\frac{\\bar{X} - \\mu_0}{\\sigma/\\sqrt{N}}.\n", "$$\n", "\n", "W praktyce najwygodniejsza jest zwykle ostatnia postać, bo od razu prowadzi do statystyki testowej i do standardowego rozkładu normalnego." ] }, { "cell_type": "markdown", "id": "bb52936d", "metadata": {}, "source": [ "## Przedziały ufności\n", "\n", "Alternatywą dla testowania hipotez (albo ich dopełnieniem; zależy, jak na to patrzeć) jest konstruowanie przedziałów ufności wokół statystyki z próby.\n", "\n", "Podręczniki i kursy, a w konsekwencji badacze w praktyce, skupiają się na tym pierwszym, a niesłusznie, bowiem **przedziały ufności dają nam więcej informacji**.\n", "\n", "Testowanie hipotezy zerowej pomaga jedynie odpowiedzieć na pytanie o jakąś pojedynczą hipotetyczną wartość średniej w populacji.\n", "\n", "Skonstruowanie przedziału ufności pozwala określić cały zakres wartości średniej w populacji, który jest „zgodny z danymi” przy zadanym poziomie ufności.\n" ] }, { "cell_type": "markdown", "id": "48091066", "metadata": {}, "source": [ "Poniżej znajduje się schematyczny rysunek, na którym przedstawione mamy rozkłady średniej z próby dla różnych hipotez zerowych.\n", "\n", "Nie odrzucamy hipotezy zerowej dla wszystkich tych hipotez, dla których średnia populacji mieści się między dolną i górną granicą przedziału (oznaczmy je symbolicznie $\\mu_d$ i $\\mu_g$).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "3faf60d1", "metadata": {}, "outputs": [], "source": [ "alpha = 0.05\n", "z_low = stats.norm.ppf(alpha / 2)\n", "z_high = stats.norm.ppf(1 - alpha / 2)\n", "\n", "x = np.linspace(-4, 4, 400)\n", "\n", "plt.figure(figsize=(10, 6))\n", "\n", "means = [z_low, z_high, stats.norm.ppf(0.25), stats.norm.ppf(0.40), stats.norm.ppf(0.75), stats.norm.ppf(0.90)]\n", "colors = [\"black\", \"black\", \"grey\", \"grey\", \"grey\", \"grey\"]\n", "\n", "for mean, color in zip(means, colors, strict=True):\n", " y = stats.norm.pdf(x, loc=mean, scale=1.0)\n", " plt.plot(x, y, color=color, linewidth=2 if color == \"black\" else 1)\n", "\n", "plt.axvline(z_low, color=\"grey\", linestyle=\"--\", linewidth=2)\n", "plt.axvline(z_high, color=\"grey\", linestyle=\"--\", linewidth=2)\n", "\n", "plt.title(\"Przedział ufności jako zbiór hipotez H0, których nie odrzucamy (schemat)\")\n", "plt.xlabel(r\"$\\bar{X}$ (dla uproszczenia: skala standaryzowana)\")\n", "plt.ylabel(\"gęstość\")\n", "\n", "plt.text(z_low, 0.01, r\"$\\mu_d$\", ha=\"center\")\n", "plt.text(z_high, 0.01, r\"$\\mu_g$\", ha=\"center\")\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "45afb608", "metadata": {}, "source": [ "### Alternatywne warianty wykresu: PU jako zbiór hipotez\n", "\n", "Poniżej są dwie dodatkowe, wersje tego wykresu. " ] }, { "cell_type": "code", "execution_count": null, "id": "2b990a49", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(-4, 4, 600)\n", "x_obs = 0.0\n", "mu_bounds = np.array([stats.norm.ppf(0.025), stats.norm.ppf(0.975)])\n", "mu_inner = np.array([stats.norm.ppf(0.20), stats.norm.ppf(0.35), 0.0, stats.norm.ppf(0.65), stats.norm.ppf(0.80)])\n", "\n", "fig, ax = plt.subplots(figsize=(10, 6))\n", "ax.set_xlim(-4, 4)\n", "ax.set_ylim(0, 0.43)\n", "ax.spines[\"top\"].set_visible(False)\n", "ax.spines[\"right\"].set_visible(False)\n", "ax.tick_params(axis=\"y\", left=False, labelleft=False)\n", "\n", "ax.fill_between([mu_bounds[0], mu_bounds[1]], 0, 0.018, color=\"0.92\", zorder=0)\n", "\n", "for mu0 in mu_inner:\n", " color = \"#1f78b4\" if np.isclose(mu0, 0.0) else \"0.75\"\n", " linewidth = 2.5 if np.isclose(mu0, 0.0) else 1.4\n", " ax.plot(x, stats.norm.pdf(x, loc=mu0, scale=1.0), color=color, linewidth=linewidth)\n", "\n", "for mu0 in mu_bounds:\n", " ax.plot(x, stats.norm.pdf(x, loc=mu0, scale=1.0), color=\"black\", linewidth=2.2)\n", "\n", "ax.vlines(mu_bounds, ymin=0.018, ymax=0.39, colors=\"0.35\", linewidth=1.8, linestyles=\"--\")\n", "ax.vlines(x_obs, ymin=0.05, ymax=0.37, colors=\"#c46a4a\", linewidth=1.6, linestyles=(0, (4, 3)))\n", "\n", "ax.hlines(0.026, mu_bounds[0], mu_bounds[1], color=\"0.2\", linewidth=2.5)\n", "ax.vlines(mu_bounds, ymin=0.02, ymax=0.032, color=\"0.2\", linewidth=2.5)\n", "\n", "ax.plot([], [], color=\"black\", linewidth=2.2, label=\"hipotezy graniczne\")\n", "ax.plot([], [], color=\"#1f78b4\", linewidth=2.5, label=\"hipoteza H0 zachowana\")\n", "ax.plot([], [], color=\"#c46a4a\", linewidth=1.6, linestyle=(0, (4, 3)), label=r\"obserwowane $\\bar{x}_{obs}$\")\n", "\n", "ax.text(mu_bounds[0], 0.403, r\"graniczne $\\mu_L$\", ha=\"center\", va=\"bottom\", fontsize=10)\n", "ax.text(0.0, 0.403, r\"zachowane $\\mu_0$\", ha=\"center\", va=\"bottom\", fontsize=10, color=\"#1f78b4\")\n", "ax.text(mu_bounds[1], 0.403, r\"graniczne $\\mu_U$\", ha=\"center\", va=\"bottom\", fontsize=10)\n", "ax.text(x_obs + 0.10, 0.29, r\"$\\bar{x}_{obs}$\", ha=\"left\", fontsize=10, color=\"#c46a4a\")\n", "ax.text(mu_bounds.mean(), 0.04, \"zakres hipotez H0, których nie odrzucamy\", ha=\"center\", fontsize=9, color=\"0.2\")\n", "\n", "ax.set_title(\"Przedział ufności jako zbiór hipotez H0: wariant 1\")\n", "ax.set_xlabel(r\"Możliwe wartości $\\bar{X}$\")\n", "ax.set_ylabel(\"gęstość\")\n", "ax.legend(frameon=False, loc=\"upper right\", fontsize=9)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "id": "502c4599", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(-4, 4, 600)\n", "x_obs = 0.0\n", "mu_bounds = np.array([stats.norm.ppf(0.025), stats.norm.ppf(0.975)])\n", "mu_inner = np.array([stats.norm.ppf(0.25), 0.0, stats.norm.ppf(0.75)])\n", "\n", "fig, ax = plt.subplots(figsize=(10, 6))\n", "ax.set_xlim(-4, 4)\n", "ax.set_ylim(0, 0.43)\n", "ax.spines[\"top\"].set_visible(False)\n", "ax.spines[\"right\"].set_visible(False)\n", "ax.tick_params(axis=\"y\", left=False, labelleft=False)\n", "\n", "ax.fill_between([mu_bounds[0], mu_bounds[1]], 0, 0.028, color=\"0.92\", zorder=0)\n", "\n", "for mu0 in mu_inner:\n", " color = \"#2c7c69\" if np.isclose(mu0, 0.0) else \"0.75\"\n", " linewidth = 2.4 if np.isclose(mu0, 0.0) else 1.3\n", " ax.plot(x, stats.norm.pdf(x, loc=mu0, scale=1.0), color=color, linewidth=linewidth)\n", "\n", "for mu0 in mu_bounds:\n", " ax.plot(x, stats.norm.pdf(x, loc=mu0, scale=1.0), color=\"black\", linewidth=2.2)\n", "\n", "ax.vlines(mu_bounds, ymin=0.012, ymax=0.392, colors=\"0.35\", linewidth=1.8, linestyles=\"--\")\n", "ax.vlines(x_obs, ymin=0.055, ymax=0.375, colors=\"#b54708\", linewidth=1.5, linestyles=(0, (4, 3)))\n", "\n", "ax.hlines(0.044, mu_bounds[0], mu_bounds[1], color=\"0.2\", linewidth=3)\n", "ax.vlines(mu_bounds, ymin=0.037, ymax=0.051, color=\"0.2\", linewidth=3)\n", "\n", "ax.text(mu_bounds[0], 0.013, r\"$\\mu_L$\", ha=\"center\", va=\"bottom\", fontsize=10, color=\"0.15\")\n", "ax.text(x_obs, 0.013, r\"$\\bar{x}_{obs}$\", ha=\"center\", va=\"bottom\", fontsize=10, color=\"#b54708\")\n", "ax.text(mu_bounds[1], 0.013, r\"$\\mu_U$\", ha=\"center\", va=\"bottom\", fontsize=10, color=\"0.15\")\n", "\n", "ax.text(mu_bounds[0], 0.405, r\"graniczne $\\mu_L$\", ha=\"center\", va=\"bottom\", fontsize=10)\n", "ax.text(0.0, 0.405, r\"zachowane $\\mu_0$\", ha=\"center\", va=\"bottom\", fontsize=10, color=\"#2c7c69\")\n", "ax.text(mu_bounds[1], 0.405, r\"graniczne $\\mu_U$\", ha=\"center\", va=\"bottom\", fontsize=10)\n", "ax.text(x_obs + 0.10, 0.315, r\"$\\bar{x}_{obs}$\", ha=\"left\", fontsize=10, color=\"#b54708\")\n", "ax.text(mu_bounds.mean(), 0.07, \"95% PU = hipotezy H0, których nie odrzucamy\", ha=\"center\", fontsize=9, color=\"0.2\")\n", "\n", "ax.set_title(\"Przedział ufności jako zbiór hipotez H0: wariant 2\")\n", "ax.set_xlabel(r\"Możliwe wartości $\\bar{X}$\")\n", "ax.set_ylabel(\"gęstość\")\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "92487568", "metadata": {}, "source": [ "### Widżet: przedział ufności a test $Z$ (dwustronny)\n", "\n", "Dla testu dwustronnego `Z` zachodzi prosta zależność: wartość $\\mu_0$ jest „zgodna z danymi” przy poziomie istotności $\\alpha$ wtedy i tylko wtedy, gdy leży w odpowiadającym mu dwustronnym PU (przy tych samych założeniach).\n", "\n", "Na tym etapie notatnika trzymamy się uproszczenia, że znamy odchylenie standardowe w populacji $\\sigma$.\n", "\n", "Poniżej przyjmujemy dane podsumowane z fikcyjnego badania czasów reakcji w zadaniu 2AFC i zmieniamy $\\mu_0$.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "a3b71b3b", "metadata": {}, "outputs": [], "source": [ "if not WIDGETY_DOSTEPNE:\n", " print('Widżety nie są dostępne: zainstaluj pakiet ipywidgets, aby korzystać z części interaktywnych.')\n", "else:\n", " n = 30\n", " sample_mean = 540.0\n", " sigma = 80.0\n", "\n", " x_min = 460.0\n", " x_max = 620.0\n", "\n", " mu0_slider = widgets.FloatSlider(\n", " value=520.0,\n", " min=x_min,\n", " max=x_max,\n", " step=1.0,\n", " description='μ0 (ms)',\n", " readout_format='.0f',\n", " continuous_update=False,\n", " )\n", "\n", " conf_slider = widgets.FloatSlider(\n", " value=0.95,\n", " min=0.80,\n", " max=0.99,\n", " step=0.01,\n", " description='poziom ufności',\n", " readout_format='.2f',\n", " continuous_update=False,\n", " )\n", "\n", " def pokaz_powiazanie(mu0: float, confidence_level: float) -> None:\n", " mu0 = float(mu0)\n", " confidence_level = float(confidence_level)\n", "\n", " se = sigma / np.sqrt(n)\n", "\n", " alpha = 1 - confidence_level\n", " z_crit = stats.norm.ppf(1 - alpha / 2)\n", "\n", " ci_low = sample_mean - z_crit * se\n", " ci_high = sample_mean + z_crit * se\n", "\n", " z_stat = (sample_mean - mu0) / se\n", " p_two_sided = 2 * stats.norm.sf(abs(z_stat))\n", "\n", " in_ci = ci_low <= mu0 <= ci_high\n", " zgodnosc = 'TAK' if in_ci else 'NIE'\n", "\n", " print(f'n = {n}')\n", " print(f'Średnia w próbie = {sample_mean:.1f} ms, znane σ = {sigma:.1f} ms')\n", " print(f'Poziom ufności = {confidence_level:.2f} (α = {alpha:.2f})')\n", " print(f'{confidence_level:.0%} PU dla średniej: [{ci_low:.1f}, {ci_high:.1f}] ms')\n", " print()\n", " print(f'H0: μ = {mu0:.0f} ms')\n", " print(f'z = {z_stat:.3f}, p (dwustronnie) = {float(p_two_sided):.4f}')\n", " print(f'Czy μ0 leży w PU? {zgodnosc}')\n", "\n", " plt.figure(figsize=(9.5, 2.8))\n", " plt.hlines(0, x_min, x_max, color='0.85', linewidth=2, zorder=0)\n", " plt.axvspan(ci_low, ci_high, color='steelblue', alpha=0.15, zorder=1)\n", " plt.plot(\n", " [ci_low, ci_high],\n", " [0, 0],\n", " color='steelblue',\n", " linewidth=8,\n", " solid_capstyle='round',\n", " zorder=2,\n", " label=f'{confidence_level:.0%} PU',\n", " )\n", " plt.scatter([ci_low, ci_high], [0, 0], color='steelblue', s=45, zorder=3)\n", "\n", " kolor = 'seagreen' if in_ci else 'crimson'\n", " plt.scatter([mu0], [0], color=kolor, s=130, zorder=4, label='μ0')\n", " plt.axvline(sample_mean, color='black', linestyle=':', linewidth=2, label='średnia z próby')\n", "\n", " plt.xlim(x_min, x_max)\n", " plt.ylim(-0.3, 0.3)\n", " plt.yticks([])\n", " plt.xlabel('czas reakcji (ms)')\n", " plt.title('PU a test Z (dwustronny)')\n", " plt.grid(axis='x', alpha=0.25)\n", " plt.legend(loc='upper right')\n", " plt.tight_layout()\n", " plt.show()\n", "\n", " out = widgets.interactive_output(\n", " pokaz_powiazanie,\n", " {\n", " 'mu0': mu0_slider,\n", " 'confidence_level': conf_slider,\n", " },\n", " )\n", "\n", " display(widgets.VBox([mu0_slider, conf_slider, out]))\n" ] }, { "cell_type": "markdown", "id": "ff3d1d83", "metadata": {}, "source": [ "W pewnym sensie konstruowanie przedziału ufności to odwrotność testowania hipotezy zerowej: określamy przedział takich możliwych wartości średniej w populacji, dla których nie odrzucilibyśmy hipotezy zerowej.\n", "\n", "95-procentowy przedział ufności to przedział, który w 95% przypadków obejmie prawdziwą średnią w populacji — w sensie **pokrycia** procedury w powtarzalnych próbach.\n", "\n", "**Przykład (kognitywistyka)**: wyobraźmy sobie bardzo proste zadanie 2AFC (wymuszony wybór z dwóch opcji), np. „lewo/prawo”. Mierzymy **czas reakcji** (w milisekundach). Poniższy kod losuje `k` prób z zadanej „populacji” czasów reakcji i dla każdej z nich rysuje odcinek odpowiadający 95-procentowemu przedziałowi ufności wokół średniej.\n", "\n", "Na razie zakładamy (dla uproszczenia), że znamy odchylenie standardowe w populacji $\\sigma$ — do tego uproszczenia wrócimy za chwilę.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "12993819", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED + 1)\n", "\n", "mu = 520\n", "sigma = 80\n", "sample_size = 20\n", "k = 50\n", "\n", "confidence_level = 0.95\n", "z_crit = stats.norm.ppf((1 + confidence_level) / 2)\n", "half_width = z_crit * sigma / np.sqrt(sample_size)\n", "\n", "plt.figure(figsize=(10, 7))\n", "plt.title(\"Przedziały ufności wokół średniej z próby (σ znane — uproszczenie)\")\n", "plt.xlabel(\"czas reakcji (ms)\")\n", "plt.ylabel(\"numer próby\")\n", "\n", "plt.axvline(mu, color=\"black\", linestyle=\":\", linewidth=2)\n", "\n", "for i in range(k):\n", " sample = rng.normal(loc=mu, scale=sigma, size=sample_size)\n", " sample_mean = float(sample.mean())\n", "\n", " lower = sample_mean - half_width\n", " upper = sample_mean + half_width\n", "\n", " plt.plot([lower, upper], [i + 1, i + 1], color=\"steelblue\", linewidth=4)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "2f0ad924", "metadata": {}, "source": [ "### Widżet: pokrycie w praktyce\n", "\n", "Poniżej symulujemy `k` powtórzeń tego samego eksperymentu (np. `k` niezależnych replikacji badania). Każda pozioma kreska to PU wyznaczony z jednej próby.\n", "\n", "Możesz porównać trzy procedury:\n", "\n", "- **Z i znane $\\sigma$** (uproszczenie, punkt odniesienia),\n", "- **Z i $s$ z próby** (często zbyt wąski PU dla małych prób),\n", "- **$t$ i $s$ z próby** (standardowy PU dla średniej).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "fdded867", "metadata": {}, "outputs": [], "source": [ "if not WIDGETY_DOSTEPNE:\n", " print('Widżety nie są dostępne: zainstaluj pakiet ipywidgets, aby korzystać z części interaktywnych.')\n", "else:\n", " mu = 520.0\n", "\n", " metoda_dropdown = widgets.Dropdown(\n", " options=[\n", " ('Z i znane σ (uproszczenie)', 'z_sigma'),\n", " ('Z i s z próby (zwykle zbyt wąski)', 'z_s'),\n", " ('t i s z próby (standard)', 't_s'),\n", " ],\n", " value='z_sigma',\n", " description='Metoda',\n", " )\n", "\n", " n_slider = widgets.IntSlider(\n", " value=20,\n", " min=5,\n", " max=200,\n", " step=1,\n", " description='n',\n", " continuous_update=False,\n", " )\n", "\n", " sigma_slider = widgets.FloatSlider(\n", " value=80.0,\n", " min=20.0,\n", " max=200.0,\n", " step=5.0,\n", " description='σ (ms)',\n", " readout_format='.0f',\n", " continuous_update=False,\n", " )\n", "\n", " conf_slider = widgets.FloatSlider(\n", " value=0.95,\n", " min=0.80,\n", " max=0.99,\n", " step=0.01,\n", " description='poziom ufności',\n", " readout_format='.2f',\n", " continuous_update=False,\n", " )\n", "\n", " k_slider = widgets.IntSlider(\n", " value=60,\n", " min=10,\n", " max=120,\n", " step=1,\n", " description='k',\n", " continuous_update=False,\n", " )\n", "\n", " max_alpha = 1 - conf_slider.max\n", " max_t_crit = stats.t.ppf(1 - max_alpha / 2, df=n_slider.min - 1)\n", " max_half_width = max_t_crit * sigma_slider.max / np.sqrt(n_slider.min)\n", " x_margin = float(np.ceil(1.05 * max_half_width / 10) * 10)\n", " x_min = mu - x_margin\n", " x_max = mu + x_margin\n", "\n", " def pokaz_pokrycie(n: int, sigma: float, confidence_level: float, k: int, metoda: str) -> None:\n", " n = int(n)\n", " sigma = float(sigma)\n", " confidence_level = float(confidence_level)\n", " k = int(k)\n", "\n", " rng = np.random.default_rng(SEED + 20)\n", " samples = rng.normal(loc=mu, scale=sigma, size=(k, n))\n", "\n", " sample_means = samples.mean(axis=1)\n", " sample_sds = samples.std(axis=1, ddof=1)\n", "\n", " alpha = 1 - confidence_level\n", "\n", " if metoda == 'z_sigma':\n", " crit = stats.norm.ppf(1 - alpha / 2)\n", " half_width = crit * sigma / np.sqrt(n)\n", " lower = sample_means - half_width\n", " upper = sample_means + half_width\n", " elif metoda == 'z_s':\n", " crit = stats.norm.ppf(1 - alpha / 2)\n", " half_widths = crit * sample_sds / np.sqrt(n)\n", " lower = sample_means - half_widths\n", " upper = sample_means + half_widths\n", " else:\n", " crit = stats.t.ppf(1 - alpha / 2, df=n - 1)\n", " half_widths = crit * sample_sds / np.sqrt(n)\n", " lower = sample_means - half_widths\n", " upper = sample_means + half_widths\n", "\n", " covered = (lower <= mu) & (mu <= upper)\n", " pokrycie = float(covered.mean())\n", " srednia_szerokosc = float((upper - lower).mean())\n", "\n", " plt.figure(figsize=(10, 7))\n", " plt.axvline(mu, color='black', linestyle=':', linewidth=2)\n", "\n", " for i in range(k):\n", " color = 'seagreen' if covered[i] else 'crimson'\n", " plt.plot([lower[i], upper[i]], [i + 1, i + 1], color=color, linewidth=3, alpha=0.85)\n", "\n", " plt.xlim(x_min, x_max)\n", " plt.ylim(0.5, k + 0.5)\n", " plt.grid(axis='x', alpha=0.25)\n", " plt.title(\n", " f'Pokrycie ≈ {pokrycie:.3f} (poziom ufności {confidence_level:.2f}); '\n", " f'średnia szerokość ≈ {srednia_szerokosc:.1f} ms'\n", " )\n", " plt.xlabel('czas reakcji (ms)')\n", " plt.ylabel('numer próby')\n", " plt.tight_layout()\n", " plt.show()\n", "\n", " out = widgets.interactive_output(\n", " pokaz_pokrycie,\n", " {\n", " 'n': n_slider,\n", " 'sigma': sigma_slider,\n", " 'confidence_level': conf_slider,\n", " 'k': k_slider,\n", " 'metoda': metoda_dropdown,\n", " },\n", " )\n", "\n", " display(widgets.VBox([metoda_dropdown, n_slider, sigma_slider, conf_slider, k_slider, out]))" ] }, { "cell_type": "markdown", "id": "b1123bf4", "metadata": {}, "source": [ "Oto jeszcze prostsza wersja tej samej idei. Zamiast rysować wszystkie odcinki, kod zwraca jedną liczbę: odsetek przedziałów, które objęły prawdziwą średnią w populacji.\n", "\n", "Dla dobrze skalibrowanej procedury wartość ta powinna przy dużej liczbie powtórzeń oscylować wokół poziomu ufności, czyli tutaj wokół $0.95$." ] }, { "cell_type": "code", "execution_count": null, "id": "c09f17a3", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED + 2)\n", "\n", "mu = 520\n", "sigma = 80\n", "sample_size = 20\n", "\n", "confidence_level = 0.95\n", "z_crit = stats.norm.ppf((1 + confidence_level) / 2)\n", "half_width = z_crit * sigma / np.sqrt(sample_size)\n", "\n", "repeats = 100_000\n", "samples = rng.normal(loc=mu, scale=sigma, size=(repeats, sample_size))\n", "\n", "sample_means = samples.mean(axis=1)\n", "lower_bounds = sample_means - half_width\n", "upper_bounds = sample_means + half_width\n", "\n", "coverage = np.mean((lower_bounds <= mu) & (mu <= upper_bounds))\n", "print(\"Pokrycie (symulacja):\", float(coverage))" ] }, { "cell_type": "markdown", "id": "98e08290", "metadata": {}, "source": [ "### Jak czytać wpływ $N$ i $\\sigma$?\n", "\n", "Z symulacji i wzoru na błąd standardowy wynikają cztery najważniejsze wnioski:\n", "\n", "- im większe $N$, tym mniejszy błąd standardowy i tym **węższy** przedział ufności,\n", "- im większe $\\sigma$, tym większy błąd standardowy i tym **szerszy** przedział ufności,\n", "- poziom ufności ustalamy osobno: to nasza decyzja metodologiczna, a nie własność danych,\n", "- przy poprawnej procedurze pokrycie powinno pozostawać bliskie poziomowi ufności; zmienia się głównie **precyzja**, nie nominalne 95%.\n", "\n", "Poniższy kod pokazuje to na prostych przykładach liczbowych." ] }, { "cell_type": "code", "execution_count": null, "id": "8c79c925", "metadata": {}, "outputs": [], "source": [ "def ci_mean_known_sigma(sample_mean: float, *, sigma: float, n: int, confidence_level: float = 0.95) -> tuple[float, float]:\n", " '''Przedział ufności dla średniej przy znanym σ.\n", "\n", " Parameters\n", " ----------\n", " sample_mean:\n", " Średnia z próby.\n", " sigma:\n", " Odchylenie standardowe w populacji (znane).\n", " n:\n", " Liczebność próby.\n", " confidence_level:\n", " Poziom ufności, np. 0.95.\n", "\n", " Returns\n", " -------\n", " (lower, upper)\n", " Dolna i górna granica PU.\n", " '''\n", " alpha = 1 - confidence_level\n", " z_crit = stats.norm.ppf(1 - alpha / 2)\n", " half_width = z_crit * sigma / np.sqrt(n)\n", " return sample_mean - half_width, sample_mean + half_width\n", "\n", "\n", "examples = [\n", " {\"sigma\": 5.0, \"n\": 10},\n", " {\"sigma\": 5.0, \"n\": 100},\n", " {\"sigma\": 10.0, \"n\": 10},\n", " {\"sigma\": 10.0, \"n\": 100},\n", "]\n", "\n", "sample_mean = 0.0\n", "for ex in examples:\n", " lower, upper = ci_mean_known_sigma(sample_mean, sigma=ex[\"sigma\"], n=ex[\"n\"], confidence_level=0.95)\n", " width = upper - lower\n", " print(f\"σ={ex['sigma']:.0f}, n={ex['n']:<3d} -> szerokość PU = {width:.3f}\")\n" ] }, { "cell_type": "markdown", "id": "e50ab1eb", "metadata": {}, "source": [ "Do tej pory milcząco zakładaliśmy, że konstruując przedział ufności wokół średniej (albo testując dotyczącą jej hipotezę zerową) **znamy odchylenie standardowe w populacji**.\n", "\n", "W praktyce znacznie częściej tego parametru nie znamy. Wtedy nadal możemy szacować średnią, ale zastępujemy nieznane $\\sigma$ odchyleniem standardowym z próby, czyli $s$.\n", "\n", "Ta podmiana ma dwie ważne konsekwencje:\n", "\n", "- długość przedziału ufności przestaje być stała i zaczyna się zmieniać z próby na próbę,\n", "- jeśli mimo to zostawimy krytyczne wartości z rozkładu normalnego $Z$, to przedział bywa **systematycznie zbyt wąski**, zwłaszcza dla małych prób.\n", "\n", "Za chwilę zobaczymy obie te rzeczy wprost w symulacjach." ] }, { "cell_type": "code", "execution_count": null, "id": "96fd2571", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED + 3)\n", "\n", "mu = 520\n", "sigma = 80\n", "sample_size = 20\n", "\n", "confidence_level = 0.95\n", "alpha = 1 - confidence_level\n", "z_crit = stats.norm.ppf(1 - alpha / 2)\n", "\n", "half_widths = []\n", "for _ in range(5):\n", " sample = rng.normal(loc=mu, scale=sigma, size=sample_size)\n", " s = float(sample.std(ddof=1))\n", " half_widths.append(z_crit * s / np.sqrt(sample_size))\n", "\n", "print(\"Kilka wartości 'ramienia' PU (z sd(sample)):\")\n", "for hw in half_widths:\n", " print(f\" {hw:.3f}\")" ] }, { "cell_type": "markdown", "id": "f7b26a72", "metadata": {}, "source": [ "Okazuje się, że odchylenie standardowe w próbie jest całkiem dobrym estymatorem odchylenia standardowego w populacji: przedziały ufności nadal „działają”.\n", "\n", "Ponieważ jednak to odchylenie za każdym razem liczymy z próby i zmienia się ono z próby na próbę, zmienna jest również długość przedziału.\n", "\n", "Mogłoby się wydawać, że raz będziemy przeszacowywać, a raz niedoszacowywać.\n", "\n", "Tymczasem okazuje się, że używając odchylenia standardowego z próby i krytycznych wartości z rozkładu normalnego (Z) **systematycznie zawężamy przedział**: faktyczne pokrycie spada poniżej 95%.\n", "\n", "Poniżej pokazujemy symulację dla małej próby (N=10) i większej (N=100).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "6b648011", "metadata": {}, "outputs": [], "source": [ "def coverage_with_z_and_sample_sd(*, mu: float, sigma: float, n: int, repeats: int = 100_000, seed: int = 0) -> float:\n", " rng = np.random.default_rng(seed)\n", "\n", " confidence_level = 0.95\n", " alpha = 1 - confidence_level\n", " z_crit = stats.norm.ppf(1 - alpha / 2)\n", "\n", " samples = rng.normal(loc=mu, scale=sigma, size=(repeats, n))\n", "\n", " sample_means = samples.mean(axis=1)\n", " sample_sds = samples.std(axis=1, ddof=1)\n", "\n", " half_widths = z_crit * sample_sds / np.sqrt(n)\n", "\n", " lower = sample_means - half_widths\n", " upper = sample_means + half_widths\n", "\n", " return float(np.mean((lower <= mu) & (mu <= upper)))\n", "\n", "\n", "mu = 520\n", "sigma = 80\n", "\n", "coverage_n10 = coverage_with_z_and_sample_sd(mu=mu, sigma=sigma, n=10, seed=SEED + 4)\n", "coverage_n100 = coverage_with_z_and_sample_sd(mu=mu, sigma=sigma, n=100, seed=SEED + 5)\n", "\n", "print(\"Pokrycie przy użyciu Z oraz sd(sample):\")\n", "print(\"N=10 :\", coverage_n10)\n", "print(\"N=100:\", coverage_n100)" ] }, { "cell_type": "markdown", "id": "e5a88dae", "metadata": {}, "source": [ "### Dlaczego PU oparte na $Z$ i $s$ bywa zbyt wąskie?\n", "\n", "Klucz tkwi w rozkładzie wariancji i odchylenia standardowego z próby.\n", "\n", "- Dla małych prób rozkład $s^2$ jest prawoskośny.\n", "- Jego średnia pozostaje poprawna, ale mediana często leży poniżej $\\sigma^2$.\n", "- To oznacza, że typowa pojedyncza próba częściej daje $s$ trochę mniejsze od prawdziwego $\\sigma$ niż trochę większe.\n", "- Jeśli dodatkowo użyjemy krytycznej wartości z rozkładu normalnego zamiast z rozkładu $t$, to przedział robi się przeciętnie za krótki.\n", "\n", "Poniżej możesz zmieniać `n` i porównywać położenie mediany z prawdziwymi wartościami $\\sigma^2$ i $\\sigma$." ] }, { "cell_type": "code", "execution_count": null, "id": "d92ff2a7", "metadata": {}, "outputs": [], "source": [ "if not WIDGETY_DOSTEPNE:\n", " print('Widżety nie są dostępne: zainstaluj pakiet ipywidgets, aby korzystać z części interaktywnych.')\n", "else:\n", " mu = 0.0\n", " sigma = 80.0\n", " repeats = 80_000\n", "\n", " n_slider = widgets.IntSlider(\n", " value=10,\n", " min=5,\n", " max=100,\n", " step=1,\n", " description='n',\n", " continuous_update=False,\n", " )\n", "\n", " min_df = n_slider.min - 1\n", " var_upper = sigma**2 * stats.chi2.ppf(0.995, df=min_df) / min_df\n", " var_xmax = float(np.ceil(var_upper / 1000) * 1000)\n", " sd_xmax = float(np.ceil(np.sqrt(var_upper) / 10) * 10)\n", "\n", " def pokaz_rozklad_s(n: int) -> None:\n", " n = int(n)\n", " rng = np.random.default_rng(SEED + 6 + n)\n", "\n", " samples = rng.normal(loc=mu, scale=sigma, size=(repeats, n))\n", " sample_vars = samples.var(axis=1, ddof=1)\n", " sample_sds = np.sqrt(sample_vars)\n", "\n", " med_var = float(np.median(sample_vars))\n", " med_sd = float(np.median(sample_sds))\n", "\n", " fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))\n", "\n", " axes[0].hist(\n", " sample_vars,\n", " bins=70,\n", " range=(0, var_xmax),\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " axes[0].axvline(sigma**2, color='crimson', linestyle=':', linewidth=2.2, label=r'$\\sigma^2$')\n", " axes[0].axvline(med_var, color='darkorange', linestyle='--', linewidth=2.2, label=r'mediana $s^2$')\n", " axes[0].set_xlim(0, var_xmax)\n", " axes[0].set_title(f'Rozkład wariancji z próby $s^2$ (n={n})')\n", " axes[0].set_xlabel('wariancja z próby (ms²)')\n", " axes[0].set_ylabel('gęstość')\n", " axes[0].grid(axis='x', alpha=0.25)\n", " axes[0].legend(loc='upper right')\n", "\n", " axes[1].hist(\n", " sample_sds,\n", " bins=70,\n", " range=(0, sd_xmax),\n", " density=True,\n", " color='steelblue',\n", " edgecolor='white',\n", " )\n", " axes[1].axvline(sigma, color='crimson', linestyle=':', linewidth=2.2, label=r'$\\sigma$')\n", " axes[1].axvline(med_sd, color='darkorange', linestyle='--', linewidth=2.2, label='mediana s')\n", " axes[1].set_xlim(0, sd_xmax)\n", " axes[1].set_title(f'Rozkład odchylenia standardowego z próby $s$ (n={n})')\n", " axes[1].set_xlabel('odchylenie standardowe z próby (ms)')\n", " axes[1].set_ylabel('gęstość')\n", " axes[1].grid(axis='x', alpha=0.25)\n", " axes[1].legend(loc='upper right')\n", "\n", " plt.tight_layout()\n", " plt.show()\n", "\n", " print(f'Średnia s^2: {float(sample_vars.mean()):.2f}')\n", " print(f'Mediana s^2: {med_var:.2f}')\n", " print(f'P( s^2 < σ^2 ): {float(np.mean(sample_vars < sigma**2)):.3f}')\n", " print()\n", " print(f'Średnia s: {float(sample_sds.mean()):.2f}')\n", " print(f'Mediana s: {med_sd:.2f}')\n", " print(f'P( s < σ ): {float(np.mean(sample_sds < sigma)):.3f}')\n", "\n", " out = widgets.interactive_output(pokaz_rozklad_s, {'n': n_slider})\n", " display(widgets.VBox([n_slider, out]))\n" ] }, { "cell_type": "markdown", "id": "1025b30d", "metadata": {}, "source": [ "Niemniej, kiedy nie znamy $\\sigma$ i opieramy się na $s$, od rozkładu normalnego lepszym przybliżeniem jest rozkład $t$.\n", "\n", "Rozkład ten wygląda podobnie do standardowego rozkładu normalnego, ale ma **grubsze ogony** (szczególnie dla małych prób).\n", "\n", "Dalej zaczyna się obszar krytyczny i dłuższe są przedziały ufności oparte na tym rozkładzie.\n", "\n", "Jest jeden parametr, który dokładnie określa kształt rozkładu $t$: liczba stopni swobody.\n", "\n", "Im mniejsza próba (mniej stopni swobody), tym trudniej odrzucić hipotezę zerową i tym mniej precyzyjny (dłuższy) przedział ufności wokół średniej.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "6d7e6afa", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(-4, 4, 400)\n", "\n", "plt.figure(figsize=(10, 6))\n", "plt.plot(x, stats.norm.pdf(x), color='black', linestyle='--', linewidth=2.2, label=r'$N(0,1)$')\n", "plt.plot(x, stats.t.pdf(x, df=5), color='crimson', linewidth=2.2, label='t(5)')\n", "plt.plot(x, stats.t.pdf(x, df=35), color='steelblue', linewidth=2.2, label='t(35)')\n", "\n", "plt.title('Rozkład t a standardowy normalny')\n", "plt.xlabel('wartość')\n", "plt.ylabel('gęstość')\n", "plt.legend()\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "51626936", "metadata": {}, "source": [ "### Widżet: rozkład $t$ a liczba stopni swobody\n", "\n", "Suwak poniżej pokazuje, jak zmienia się rozkład $t$ wraz ze wzrostem liczby stopni swobody (czyli w praktyce: wraz ze wzrostem liczebności próby)." ] }, { "cell_type": "code", "execution_count": null, "id": "25676e9d", "metadata": {}, "outputs": [], "source": [ "if not WIDGETY_DOSTEPNE:\n", " print('Widżety nie są dostępne: zainstaluj pakiet ipywidgets, aby korzystać z części interaktywnych.')\n", "else:\n", " df_slider = widgets.IntSlider(\n", " value=10,\n", " min=1,\n", " max=200,\n", " step=1,\n", " description='df',\n", " continuous_update=False,\n", " )\n", "\n", " def pokaz_t(df: int) -> None:\n", " df = int(df)\n", " x = np.linspace(-4, 4, 400)\n", "\n", " plt.figure(figsize=(10, 4))\n", " plt.plot(x, stats.norm.pdf(x), color='black', linestyle='--', linewidth=2, label=r'$N(0,1)$')\n", " plt.plot(x, stats.t.pdf(x, df=df), color='steelblue', linewidth=2, label=f't({df})')\n", "\n", " plt.title('Rozkład t a standardowy normalny')\n", " plt.xlabel('wartość')\n", " plt.ylabel('gęstość')\n", " plt.legend()\n", " plt.tight_layout()\n", " plt.show()\n", "\n", " out = widgets.interactive_output(pokaz_t, {'df': df_slider})\n", " display(widgets.VBox([df_slider, out]))" ] }, { "cell_type": "markdown", "id": "689f5f1a", "metadata": {}, "source": [ "Jak pamiętamy, wzór na statystykę testową $z$ wyglądał tak:\n", "\n", "$$\n", "z = \\frac{\\bar{X} - \\mu}{\\sigma/\\sqrt{N}}\n", "$$\n", "\n", "Zmodyfikowany tak, że uwzględnia fakt, że estymujemy odchylenie standardowe, wygląda tak:\n", "\n", "$$\n", "t = \\frac{\\bar{X} - \\mu}{s/\\sqrt{N}}\n", "$$\n", "\n", "Jeśli użyjemy krytycznych wartości z rozkładu $t$, symulacja pokrycia powinna wrócić w okolice 0.95.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "87bc664a", "metadata": {}, "outputs": [], "source": [ "def coverage_with_t(*, mu: float, sigma: float, n: int, repeats: int = 100_000, seed: int = 0) -> float:\n", " rng = np.random.default_rng(seed)\n", "\n", " confidence_level = 0.95\n", " alpha = 1 - confidence_level\n", " t_crit = stats.t.ppf(1 - alpha / 2, df=n - 1)\n", "\n", " samples = rng.normal(loc=mu, scale=sigma, size=(repeats, n))\n", " sample_means = samples.mean(axis=1)\n", " sample_sds = samples.std(axis=1, ddof=1)\n", "\n", " half_widths = t_crit * sample_sds / np.sqrt(n)\n", "\n", " lower = sample_means - half_widths\n", " upper = sample_means + half_widths\n", "\n", " return float(np.mean((lower <= mu) & (mu <= upper)))\n", "\n", "\n", "mu = 520\n", "sigma = 80\n", "sample_size = 20\n", "\n", "coverage_t = coverage_with_t(mu=mu, sigma=sigma, n=sample_size, seed=SEED + 7)\n", "print(\"Pokrycie (przedział t):\", coverage_t)" ] }, { "cell_type": "markdown", "id": "bf61e01c", "metadata": {}, "source": [ "## Przykład końcowy\n", "\n", "Compas i współpracownicy (1994) zauważyli, że dzieci będące pod stresem deklarują mniej symptomów lęku i depresji, niż można by oczekiwać. Jednocześnie badaczy zainteresowało, że te same dzieci uzyskują podwyższone wyniki w **skali kłamstwa**, czyli miarze skłonności do udzielania społecznie pożądanych odpowiedzi.\n", "\n", "W populacji dzieci średni wynik w tej skali w Children’s Manifest Anxiety Scale (Reynolds i Richmond, 1978) wynosi $\\mu_0 = 3.87$. Dla próby `n = 36` dzieci pod stresem uzyskano średnią $\\bar{x} = 4.39$ i odchylenie standardowe `s = 2.61`.\n", "\n", "Pytanie badawcze brzmi: czy te dane dają podstawy, by uznać, że w tej grupie średni wynik jest **wyższy** niż wartość populacyjna?\n" ] }, { "cell_type": "markdown", "id": "e5fa7339", "metadata": {}, "source": [ "### Przykład — test i PU krok po kroku\n", "\n", "**Dane podsumowane**: mamy próbę $n = 36$ dzieci, średnia $\\bar{x} = 4.39$, odchylenie standardowe $s = 2.61$.\n", "\n", "**Cel analizy**: sprawdzić, czy średni wynik w populacji tych dzieci jest wyższy niż średnia referencyjna $\\mu_0 = 3.87$.\n", "\n", "**Hipotezy** (zależą od kierunkowości):\n", "\n", "- dwustronnie: $H_0: \\mu = 3.87$ wobec $H_A: \\mu \\neq 3.87$,\n", "- prawostronnie: $H_0: \\mu = 3.87$ wobec $H_A: \\mu > 3.87$.\n", "\n", "**Poziom istotności**: ustalamy $\\alpha = 0.05$.\n", "\n", "**Statystyka i rozkład pod $H_0$**:\n", "\n", "$$\n", " t = \\frac{\\bar{x} - \\mu_0}{s/\\sqrt{n}},\\quad T \\sim t(df=n-1).\n", "$$\n", "\n", "**Region krytyczny**:\n", "\n", "- dwustronnie: $|t| \\ge t_{1-\\alpha/2,\\,df}$,\n", "- prawostronnie: $t \\ge t_{1-\\alpha,\\,df}$.\n", "\n", "**p-wartość „na piechotę”**:\n", "\n", "- dwustronnie: $p = 2\\,P(T \\ge |t_{obs}|\\mid H_0)$,\n", "- prawostronnie: $p = P(T \\ge t_{obs}\\mid H_0)$.\n", "\n", "**Weryfikacja funkcją**: mając tylko dane podsumowane nie uruchomimy `ttest_1samp` (brak surowych obserwacji), ale p-wartość liczymy z dystrybuanty rozkładu $t$ (`scipy.stats.t.cdf/sf`).\n", "\n", "**Interpretacja**: raportujemy estymatę efektu (np. $\\bar{x} - \\mu_0$) i 95% PU; p-wartość opisuje rzadkość danych warunkowo na $H_0$.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "eaf6c41d", "metadata": {}, "outputs": [], "source": [ "n = 36\n", "mu0 = 3.87\n", "sample_mean = 4.39\n", "sample_sd = 2.61\n", "\n", "alpha = 0.05\n", "\n", "df = n - 1\n", "standard_error = sample_sd / np.sqrt(n)\n", "\n", "t_stat = (sample_mean - mu0) / standard_error\n", "\n", "# Krytyczna wartość dla testu dwustronnego\n", "critical_two_sided = stats.t.ppf(1 - alpha / 2, df=df)\n", "\n", "# p-wartość \"na piechotę\" (dwustronnie)\n", "p_two_sided = 2 * stats.t.sf(abs(t_stat), df=df)\n", "\n", "# Jeśli ktoś chciałby test jednostronny: H_A: mu > mu0\n", "critical_one_sided = stats.t.ppf(1 - alpha, df=df)\n", "p_one_sided = stats.t.sf(t_stat, df=df)\n", "\n", "# 95% PU dla średniej\n", "ci_low = sample_mean - critical_two_sided * standard_error\n", "ci_high = sample_mean + critical_two_sided * standard_error\n", "\n", "# Estymata efektu w naturalnej skali (różnica średnich)\n", "effect = sample_mean - mu0\n", "\n", "print(f\"n = {n}, df = {df}\")\n", "print(f\"Średnia w próbie = {sample_mean:.2f}, sd w próbie = {sample_sd:.2f}\")\n", "print(f\"Efekt (różnica względem μ0) = {effect:.2f}\")\n", "print()\n", "print(f\"t = {t_stat:.4f}\")\n", "print(f\"t_krytyczne (dwustronnie, α=0.05) = {critical_two_sided:.5f}\")\n", "print(f\"p (dwustronnie) = {float(p_two_sided):.4f}\")\n", "print()\n", "print(f\"t_krytyczne (prawostronnie, α=0.05) = {critical_one_sided:.5f}\")\n", "print(f\"p (prawostronnie) = {float(p_one_sided):.4f}\")\n", "print()\n", "print(f\"95% PU dla średniej: ({ci_low:.4f}, {ci_high:.4f})\")\n" ] }, { "cell_type": "markdown", "id": "7b7d7b55", "metadata": {}, "source": [ "### Wniosek z przykładu\n", "\n", "W tym przykładzie statystyka $t$ nie przekracza odpowiedniej wartości krytycznej, a 95% przedział ufności dla średniej obejmuje wartość $\\mu_0 = 3.87$.\n", "\n", "Wniosek brzmi więc: **nie mamy podstaw do odrzucenia $H_0$** przy przyjętych założeniach i poziomie istotności $\\alpha = 0.05$.\n", "\n", "Dyscyplina językowa jest tu ważna. Zamiast mówić „hipoteza zerowa jest prawdziwa”, mówimy raczej:\n", "\n", "- przy założeniu $H_0$ oraz założeń testu takie dane nie są wystarczająco rzadkie, żeby odrzucić $H_0$,\n", "- estymata efektu wynosi tu $0.52$ punktu,\n", "- a niepewność tego oszacowania opisuje 95% przedział ufności, który obejmuje wartość referencyjną $3.87$." ] }, { "cell_type": "markdown", "id": "96efb311", "metadata": {}, "source": [ "## Podsumowanie\n", "\n", "- Błąd standardowy średniej maleje jak $1/\\sqrt{N}$.\n", "- 95% PU to własność procedury w powtarzalnych próbach (pokrycie ≈ 0.95).\n", "- Gdy $\\sigma$ nieznane, używamy rozkładu $t$ (grubsze ogony) i krytycznej wartości $t$.\n", "- W teście dwustronnym z $\\alpha=0.05$ wartość $\\mu_0$ jest zgodna z danymi, jeśli leży w 95% PU (przy tych samych założeniach).\n" ] }, { "cell_type": "markdown", "id": "ee358cec", "metadata": {}, "source": [ "## Ćwiczenia\n", "\n", "1) W badaniu czasów reakcji (ms) w prostym zadaniu 2AFC uzyskano z danych podsumowanych: $\\bar{x}=512$, $s=74$, $n=25$. Oblicz 95% PU dla średniej (użyj rozkładu $t$).\n", "2) Sprawdź, jak zmieni się szerokość PU, gdy $n$ wzrośnie z 25 do 100 (przy tym samym $s$).\n", "3) Własnymi słowami: czym różni się interpretacja 95% PU od interpretacji p-wartości?\n", "4) (Opcjonalnie) Efekt Stroopa jako problem jednopróbkowy: mamy różnice $d_i = RT_{\\text{niespójne}} - RT_{\\text{spójne}}$ dla $n=30$ osób. Z danych podsumowanych: $\\bar{d}=35$ ms, $s_d=60$ ms. Oblicz 95% PU dla średniej różnicy i zinterpretuj wynik.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.2" } }, "nbformat": 4, "nbformat_minor": 5 }