{ "cells": [ { "cell_type": "markdown", "id": "02000000", "metadata": {}, "source": [ "# Rachunek prawdopodobieństwa i zmienne losowe\n", "\n", "Ten notatnik jest w 100% w Pythonie: używamy `numpy`, `scipy.stats` i `matplotlib` (oraz odrobiny `pandas`).\n", "\n", "Prawdopodobieństwo jest językiem niepewności: pozwala formalnie opisywać losowość i zmienność.\n", "\n", "W tym kursie trzymamy się przede wszystkim **interpretacji częstotliwościowej**: prawdopodobieństwo zdarzenia rozumiemy jako graniczną częstość w bardzo długiej serii powtórzeń (przy stałych warunkach).\n", "\n", "Żeby przykłady były bliższe kognitywistyce, będziemy wracać do trzech prostych motywów:\n", "\n", "- zadanie dwuwyborowe (2AFC): odpowiedź poprawna/niepoprawna,\n", "- czas reakcji (ms) jako zmienna ciągła,\n", "- detekcja bodźca: bodziec jest/nie ma go, a badany odpowiada „tak/nie”.\n", "\n", "W kilku miejscach pojawią się też proste elementy interaktywne (widżety), które pomagają „zobaczyć” działanie wzorów.\n" ] }, { "cell_type": "markdown", "id": "02000001", "metadata": {}, "source": [ "## Cele\n", "\n", "Po tym notatniku:\n", "\n", "- Rozumiesz, jak w podejściu **częstotliwościowym** interpretować prawdopodobieństwo (jako częstość w długim okresie).\n", "- Umiesz pracować z pojęciami: przestrzeń wyników, zdarzenie, suma/przecięcie, dopełnienie, prawdopodobieństwo warunkowe.\n", "- Umiesz policzyć proste prawdopodobieństwa „dokładnie” (enumeracja) oraz przez symulację.\n", "- Rozumiesz różnicę między zmienną losową dyskretną i ciągłą.\n", "- Potrafisz obliczyć (na prostym przykładzie) wartość oczekiwaną i wariancję zmiennej losowej dyskretnej.\n", "- Umiesz zastosować twierdzenie Bayesa na prostym przykładzie oraz interpretować wynik w kategoriach naturalnych częstości.\n" ] }, { "cell_type": "markdown", "id": "02000002", "metadata": {}, "source": [ "## Wymagania wstępne\n", "\n", "- Podstawy Pythona.\n", "- Prosta algebra (dodawanie, mnożenie, ułamki).\n", "- Instalacja środowiska z tego repo (`uv sync`).\n", "- Dostęp do notatników Jupyter i obsługa widżetów (pakiet `ipywidgets`).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000003", "metadata": {}, "outputs": [], "source": [ "import math\n", "\n", "import numpy as np\n", "import pandas as pd\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", "\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\n" ] }, { "cell_type": "markdown", "id": "02000004", "metadata": {}, "source": [ "## Prawdopodobieństwo: intuicja częstotliwościowa\n", "\n", "Nie ma jednej, „jedynej” filozoficznej definicji prawdopodobieństwa, ale w statystyce klasycznej bardzo często używa się interpretacji częstotliwościowej.\n", "\n", "W tej interpretacji $P(A)$ oznacza **częstość względną zdarzenia $A$ w bardzo długiej serii powtórzeń** tego samego doświadczenia (w identycznych warunkach).\n", "\n", "W krótkich seriach częstości „pływają” (losowa zmienność). W miarę wzrostu liczby prób zwykle stabilizują się w pobliżu wartości $p$ — to intuicja stojąca za prawem wielkich liczb.\n", "\n", "Zobaczmy to na prostym przykładzie z kognitywistyki: zadaniu dwuwyborowym, w którym każda próba kończy się odpowiedzią poprawną albo błędną.\n" ] }, { "cell_type": "markdown", "id": "02000005", "metadata": {}, "source": [ "### Zadanie dwuwyborowe (2AFC) w jednym akapicie\n", "\n", "W zadaniu dwuwyborowym (2AFC) w każdej próbie badany musi wybrać **jedną z dwóch** odpowiedzi.\n", "\n", "Przykład: w dwóch krótkich przedziałach czasowych odtwarzany jest dźwięk; dźwięk pojawia się w jednym z nich, a zadaniem jest wskazać, czy był w pierwszym czy w drugim.\n", "\n", "Jeżeli zapisujemy wynik tylko jako „poprawnie / błędnie”, to pojedyncza próba ma dwa możliwe wyniki (0/1). Taki model nazywamy **próbą Bernoulliego**. Jej parametrem jest $p$ — prawdopodobieństwo odpowiedzi poprawnej (przy stałych warunkach zadania).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000006", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED)\n", "\n", "p_poprawnej_odpowiedzi = 0.7\n", "liczba_prob = 5000\n", "\n", "poprawna_odpowiedz = rng.random(liczba_prob) < p_poprawnej_odpowiedzi\n", "estymata_skumulowana = np.cumsum(poprawna_odpowiedz) / np.arange(1, liczba_prob + 1)\n", "\n", "estymata_skumulowana[:10]\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000007", "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(10, 4))\n", "plt.plot(estymata_skumulowana, linewidth=2)\n", "plt.axhline(p_poprawnej_odpowiedzi, linestyle='--', color='black', label='p (założone)')\n", "\n", "plt.title('Zadanie 2AFC: częstość skumulowana jako estymator p')\n", "plt.xlabel('Liczba prób')\n", "plt.ylabel('Częstość odpowiedzi poprawnych')\n", "plt.legend()\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "02000008", "metadata": {}, "source": [ "### Widżet: jak szybko stabilizuje się częstość?\n", "\n", "Ustaw $p$ oraz liczbę prób i zobacz, jak zachowuje się częstość skumulowana w zadaniu dwuwyborowym.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000009", "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", " p_slider = widgets.FloatSlider(\n", " value=0.7,\n", " min=0.05,\n", " max=0.95,\n", " step=0.01,\n", " description='p',\n", " readout_format='.2f',\n", " )\n", " liczba_prob_slider = widgets.IntSlider(\n", " value=2000,\n", " min=100,\n", " max=20000,\n", " step=100,\n", " description='Liczba prób',\n", " )\n", " seed_input = widgets.IntText(value=SEED, description='Ziarno')\n", "\n", " def pokaz_stabilizacje(p, liczba_prob, seed):\n", " rng = np.random.default_rng(seed)\n", " poprawna_odpowiedz = rng.random(liczba_prob) < p\n", " estymata_skumulowana = np.cumsum(poprawna_odpowiedz) / np.arange(1, liczba_prob + 1)\n", "\n", " plt.figure(figsize=(10, 4))\n", " plt.plot(estymata_skumulowana, linewidth=2)\n", " plt.axhline(p, linestyle='--', color='black', label='p')\n", "\n", " plt.title('Zadanie 2AFC: stabilizacja częstości skumulowanej')\n", " plt.xlabel('Liczba prób')\n", " plt.ylabel('Częstość odpowiedzi poprawnych')\n", " plt.ylim(0.0, 1.0)\n", " plt.legend()\n", " plt.show()\n", "\n", " out = widgets.interactive_output(\n", " pokaz_stabilizacje,\n", " {'p': p_slider, 'liczba_prob': liczba_prob_slider, 'seed': seed_input},\n", " )\n", "\n", " display(widgets.VBox([p_slider, liczba_prob_slider, seed_input]), out)\n" ] }, { "cell_type": "markdown", "id": "02000010", "metadata": {}, "source": [ "### Uwaga: „te same warunki” w eksperymencie to założenie\n", "\n", "Interpretacja częstotliwościowa mówi o powtarzaniu doświadczenia w identycznych warunkach. W prawdziwym eksperymencie bywa inaczej: badany może się uczyć, męczyć albo zmieniać strategię. Wtedy prawdopodobieństwo odpowiedzi poprawnej może się **zmieniać w czasie**.\n", "\n", "Poniżej prosta symulacja, w której $p$ rośnie liniowo (uczenie). Zobacz, jak to wpływa na częstość skumulowaną.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000011", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED + 10)\n", "\n", "liczba_prob = 2000\n", "p_poczatek = 0.55\n", "p_koniec = 0.80\n", "\n", "p_w_czasie = np.linspace(p_poczatek, p_koniec, liczba_prob)\n", "\n", "poprawna_odpowiedz = rng.random(liczba_prob) < p_w_czasie\n", "estymata_skumulowana = np.cumsum(poprawna_odpowiedz) / np.arange(1, liczba_prob + 1)\n", "\n", "plt.figure(figsize=(10, 4))\n", "plt.plot(estymata_skumulowana, linewidth=2, label='częstość skumulowana')\n", "plt.plot(p_w_czasie, linestyle='--', color='black', alpha=0.7, label='p w czasie (uczenie)')\n", "\n", "plt.title('Gdy warunki nie są stałe: p zmienia się w czasie')\n", "plt.xlabel('Liczba prób')\n", "plt.ylabel('Częstość odpowiedzi poprawnych')\n", "plt.legend()\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "02000012", "metadata": {}, "source": [ "## Zdarzenia i podstawowe reguły\n", "\n", "Zaczynamy od słownika, którym posługuje się rachunek prawdopodobieństwa.\n", "\n", "- **Przestrzeń wyników** $\\Omega$ to zbiór wszystkich możliwych wyników doświadczenia.\n", "- **Zdarzenie** $A$ to podzbiór $\\Omega$ (czyli: pewna grupa wyników, które nas interesują).\n", "\n", "Jeżeli wszystkie wyniki elementarne są jednakowo prawdopodobne (np. uczciwa kostka), to:\n", "\n", "$$\n", "P(A) = \\frac{|A|}{|\\Omega|}\n", "$$\n", "\n", "Najczęściej używane operacje na zdarzeniach:\n", "\n", "- **dopełnienie** $A^c$ („nie A”),\n", "- **suma** $A\\cup B$ („A lub B”),\n", "- **przecięcie** $A\\cap B$ („A i B”).\n", "\n", "Przydają nam się też dwa wzory:\n", "\n", "- reguła dodawania: $P(A\\cup B)=P(A)+P(B)-P(A\\cap B)$,\n", "- prawdopodobieństwo warunkowe: $P(A\\mid B)=\\frac{P(A\\cap B)}{P(B)}$ (dla $P(B)>0$).\n", "\n", "Zobaczmy to na przykładzie dwóch rzutów uczciwą kostką. Zrobimy to „dokładnie” przez enumerację (wypisanie całej $\\Omega$).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000013", "metadata": {}, "outputs": [], "source": [ "outcomes = []\n", "\n", "for d1 in range(1, 7):\n", " for d2 in range(1, 7):\n", " outcomes.append({'d1': d1, 'd2': d2})\n", "\n", "omega_df = pd.DataFrame(outcomes)\n", "omega_df['suma'] = omega_df['d1'] + omega_df['d2']\n", "\n", "def prob(mask) -> float:\n", " return float(mask.mean())\n", "\n", "print('Liczba wyników w Ω:', int(omega_df.shape[0]))\n", "omega_df.head(10)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000014", "metadata": {}, "outputs": [], "source": [ "# A: suma oczek = 7\n", "A = omega_df['suma'] == 7\n", "\n", "# B: przynajmniej jedna szóstka\n", "B = (omega_df['d1'] == 6) | (omega_df['d2'] == 6)\n", "\n", "p_A = prob(A)\n", "p_B = prob(B)\n", "\n", "print('P(A) (enumeracja) =', p_A)\n", "print('P(B) (enumeracja) =', p_B)\n", "\n", "# Symulacja jako kontrola intuicji\n", "rng = np.random.default_rng(SEED + 1)\n", "n_sim = 200_000\n", "\n", "d1_sim = rng.integers(1, 7, size=n_sim)\n", "d2_sim = rng.integers(1, 7, size=n_sim)\n", "\n", "p_A_mc = float(np.mean(d1_sim + d2_sim == 7))\n", "p_B_mc = float(np.mean((d1_sim == 6) | (d2_sim == 6)))\n", "\n", "print('P(A) (symulacja Monte Carlo) =', p_A_mc)\n", "print('P(B) (symulacja Monte Carlo) =', p_B_mc)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000015", "metadata": {}, "outputs": [], "source": [ "p_union = prob(A | B)\n", "p_intersection = prob(A & B)\n", "\n", "# Reguła dodawania:\n", "# P(A \\cup B) = P(A) + P(B) - P(A \\cap B)\n", "p_union_rhs = p_A + p_B - p_intersection\n", "\n", "print('P(A ∪ B) =', p_union)\n", "print('P(A)+P(B)-P(A ∩ B) =', p_union_rhs)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000016", "metadata": {}, "outputs": [], "source": [ "# Prawdopodobieństwo warunkowe: P(A | C) = P(A \\cap C) / P(C)\n", "# C: pierwszy rzut to 3\n", "C = omega_df['d1'] == 3\n", "\n", "p_C = prob(C)\n", "p_A_and_C = prob(A & C)\n", "p_A_given_C = p_A_and_C / p_C\n", "\n", "# To samo można policzyć jako \"odsetek A\" w podzbiorze spełniającym C\n", "p_A_given_C_alt = float((omega_df.loc[C, 'suma'] == 7).mean())\n", "\n", "print('P(C) =', p_C)\n", "print('P(A ∩ C) =', p_A_and_C)\n", "print('P(A | C) =', p_A_given_C)\n", "print('P(A | C) (w podzbiorze) =', p_A_given_C_alt)\n" ] }, { "cell_type": "markdown", "id": "02000017", "metadata": {}, "source": [ "### Zdarzenia w danych eksperymentalnych: poprawność i czas reakcji\n", "\n", "W praktyce zdarzenia często definiuje się na danych: np. „odpowiedź była poprawna” albo „czas reakcji był krótszy niż 450 ms”.\n", "\n", "Poniżej tworzymy mały, sztuczny zbiór danych z prób zadania dwuwyborowego i liczymy kilka prawdopodobieństw tak samo, jak przy kostkach (częstość = średnia wartości logicznych).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000018", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED + 2)\n", "\n", "liczba_prob = 200\n", "\n", "czas_reakcji_ms = rng.normal(loc=550, scale=90, size=liczba_prob)\n", "czas_reakcji_ms = np.clip(czas_reakcji_ms, 200, None)\n", "\n", "proby_df = pd.DataFrame({'czas_reakcji_ms': czas_reakcji_ms})\n", "\n", "szybka = proby_df['czas_reakcji_ms'] < 450\n", "p_poprawna = np.where(szybka, 0.80, 0.65)\n", "\n", "proby_df['poprawna'] = rng.random(liczba_prob) < p_poprawna\n", "\n", "p_poprawna_all = prob(proby_df['poprawna'])\n", "p_szybka = prob(szybka)\n", "p_poprawna_i_szybka = prob(proby_df['poprawna'] & szybka)\n", "\n", "print('P(poprawna) =', p_poprawna_all)\n", "print('P(szybka) =', p_szybka)\n", "print('P(poprawna ∩ szybka) =', p_poprawna_i_szybka)\n", "print('P(poprawna | szybka) =', p_poprawna_i_szybka / p_szybka)\n", "print('P(poprawna | wolniejsza) =', prob(proby_df['poprawna'] & ~szybka) / prob(~szybka))\n", "\n", "proby_df.head(10)\n" ] }, { "cell_type": "markdown", "id": "02000019", "metadata": {}, "source": [ "### Niezależność\n", "\n", "Intuicyjnie: zdarzenia $A$ i $B$ są niezależne, gdy informacja o jednym z nich **nie zmienia** prawdopodobieństwa drugiego.\n", "\n", "Formalnie (równoważne warunki):\n", "\n", "- $P(A \\cap B) = P(A)P(B)$,\n", "- albo $P(A\\mid B)=P(A)$ (dla $P(B)>0$).\n", "\n", "Sprawdźmy to na prostym przykładzie: parzystość pierwszego i drugiego rzutu kostką.\n", "\n", "Niech:\n", "\n", "- $E_1$ = „pierwszy rzut parzysty”,\n", "- $E_2$ = „drugi rzut parzysty”.\n", "\n", "Uwaga: w eksperymentach kolejne próby mogą nie być niezależne (np. uczenie, zmęczenie, efekt poprzedniego bodźca). Niezależność jest więc założeniem modelu, które warto świadomie oceniać.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "02000020", "metadata": {}, "outputs": [], "source": [ "E1 = omega_df['d1'] % 2 == 0\n", "E2 = omega_df['d2'] % 2 == 0\n", "\n", "p_E1 = prob(E1)\n", "p_E2 = prob(E2)\n", "p_E1_and_E2 = prob(E1 & E2)\n", "\n", "p_E1_given_E2 = p_E1_and_E2 / p_E2\n", "\n", "print('P(E1) =', p_E1)\n", "print('P(E2) =', p_E2)\n", "print('P(E1 ∩ E2) =', p_E1_and_E2)\n", "print('P(E1)P(E2) =', p_E1 * p_E2)\n", "print('P(E1 | E2) =', p_E1_given_E2)\n" ] }, { "cell_type": "markdown", "id": "02000021", "metadata": {}, "source": [ "## Zmienne losowe: dyskretne i ciągłe\n", "\n", "W rachunku prawdopodobieństwa często nie interesuje nas sam \"surowy\" wynik doświadczenia, tylko pewna liczba, którą z niego wyprowadzamy. Taką funkcję nazywamy **zmienną losową**.\n", "\n", "Przykłady:\n", "\n", "- $X$ = liczba odpowiedzi poprawnych w 10 próbach zadania dwuwyborowego (zmienna **dyskretna**),\n", "- $Y$ = czas reakcji w ms (zmienna **ciągła**),\n", "- $Z$ = średnica źrenicy w mm (zmienna **ciągła**).\n", "\n", "**Zmienna losowa dyskretna** przyjmuje wartości policzalne.\n", "\n", "**Zmienna losowa ciągła** przyjmuje wartości z pewnego przedziału liczb rzeczywistych.\n", "\n", "Kluczowa konsekwencja:\n", "\n", "- dla dyskretnej sensowne jest $P(X=x)$ i może być dodatnie,\n", "- dla ciągłej zawsze $P(X=x)=0$ — prawdopodobieństwa dotyczą przedziałów (np. $P(a