{ "cells": [ { "cell_type": "markdown", "id": "01000000", "metadata": {}, "source": [ "# Statystyka opisowa i wizualizacja rozkładów\n", "\n", "Ten notatnik jest w 100% w Pythonie: używamy `numpy`, `pandas`, `scipy.stats`, `matplotlib` i `seaborn`.\n", "\n", "Statystyka opisowa ma prosty cel: **uporządkować dane i opisać je tak, aby można było je sensownie interpretować**.\n", "\n", "W praktyce zwykle zaczynamy od wykresu, a dopiero potem liczymy \"jedną liczbę\" (np. średnią). Wykres szybko ujawnia kształt rozkładu i potencjalne problemy (np. obserwacje odstające).\n", "\n", "W tym notatniku skupimy się na opisie rozkładu zmiennej ilościowej: **centrum**, **rozproszenie**, **kształt** oraz **obserwacje odstające**. To fundament, zanim przejdziemy do wnioskowania statystycznego.\n", "\n", "Na wykresach konsekwentnie opisujemy osie i jednostki — to drobny nawyk, który bardzo ułatwia interpretację.\n" ] }, { "cell_type": "markdown", "id": "01000001", "metadata": {}, "source": [ "## Cele\n", "\n", "Po tym notatniku:\n", "\n", "- Umiesz wykonać podstawowe wykresy rozkładu (tabela częstości, histogram, wygładzenie, wykres pudełkowy).\n", "- Umiesz opisać rozkład: centrum, rozproszenie, kształt, obserwacje odstające.\n", "- Umiesz policzyć miary tendencji centralnej (np. średnia, mediana, średnia obcięta).\n", "- Umiesz policzyć miary zmienności (np. wariancja, odchylenie standardowe, IQR, MAD).\n", "- Umiesz liczyć i interpretować percentyle / kwartyle / decyle.\n", "- Rozumiesz, jak proste transformacje liniowe wpływają na średnią i odchylenie standardowe.\n", "- Rozróżniasz podstawowe skale pomiarowe (nominalną, porządkową, przedziałową, ilorazową) i dobierasz do nich sensowne podsumowania oraz wykresy.\n", "- Rozumiesz różnicę między populacją i próbą oraz między statystyką opisową a wnioskowaniem statystycznym.\n" ] }, { "cell_type": "markdown", "id": "01000002", "metadata": {}, "source": [ "## Wymagania wstępne\n", "\n", "- Podstawy Pythona (zmienne, pętle `for`, funkcje).\n", "- Podstawy `numpy` i `pandas`.\n", "- Instalacja środowiska z tego repo (`uv sync`).\n" ] }, { "cell_type": "markdown", "id": "01000045", "metadata": {}, "source": [ "## Podstawowe pojęcia: populacja, próba i losowość\n", "\n", "W statystyce prawie zawsze interesuje nas pewna **populacja** (np. wszyscy studenci pierwszego roku), ale w praktyce obserwujemy tylko **próbę** (np. 60 studentów, którzy wypełnili ankietę).\n", "\n", "- Populacja to zbiór wszystkich wyników/obiektów, o których chcemy coś powiedzieć.\n", "- Próba to część populacji, którą rzeczywiście mierzymy.\n", "\n", "Z próby liczymy **statystyki** (np. średnią z próby $\\bar{x}$), a populację opisują **parametry** (np. średnia $\\mu$). Parametry zwykle są nieznane.\n", "\n", "Dwa słowa, które łatwo pomylić:\n", "\n", "- *Losowy dobór próby* dotyczy tego, **kogo** badamy (reprezentatywność).\n", "- *Losowy przydział do warunków* dotyczy tego, **co** komu robimy w eksperymencie (wnioskowanie przyczynowe).\n", "\n", "W tym notatniku będziemy głównie porządkować i opisywać dane, ale te pojęcia wrócą, gdy przejdziemy do wnioskowania.\n" ] }, { "cell_type": "markdown", "id": "01000046", "metadata": {}, "source": [ "## Statystyka opisowa a wnioskowanie statystyczne\n", "\n", "- **Statystyka opisowa** porządkuje dane: wykresy, tabele, miary centrum i rozproszenia.\n", "- **Wnioskowanie statystyczne** odpowiada na pytanie: co wyniki z próby mówią o populacji i jak duża jest niepewność tej odpowiedzi? Tutaj pojawiają się m.in. przedziały ufności i testy.\n", "\n", "Ten notatnik jest przede wszystkim opisowy: uczymy się języka i narzędzi, bez których interpretacja wyników byłaby przypadkowa.\n" ] }, { "cell_type": "markdown", "id": "01000047", "metadata": {}, "source": [ "## Skale pomiarowe i typy danych\n", "\n", "To, co wolno zrobić z danymi, zależy od tego, co one znaczą. Szczególnie ważna jest **skala pomiarowa**.\n", "\n", "Praktyczny podział:\n", "\n", "- **Dane kategoryczne (jakościowe)**: wartości to etykiety, np. typ bodźca (twarz / dom), poprawność odpowiedzi (poprawna / niepoprawna), grupa eksperymentalna.\n", "- **Dane pomiarowe (ilościowe)**: wartości to liczby z jednostką, np. czas reakcji [ms], czas fiksacji [ms], wynik testu [pkt].\n", "\n", "Klasyczny podział skal pomiarowych:\n", "\n", "1) **Skala nominalna**\n", "\n", "- Kategorie bez naturalnego porządku (np. typ bodźca).\n", "- Sensowne są zliczenia, odsetki i moda; typowy wykres to słupki (częstości lub odsetki).\n", "- Średnia z kategorii nie ma interpretacji.\n", "\n", "2) **Skala porządkowa**\n", "\n", "- Kategorie mają porządek, ale odstępy między nimi nie muszą być równe (np. ocena pewności 1–7).\n", "- Sensowne są mediany, kwartyle/percentyle i rankingi.\n", "- Używanie średniej jest dodatkowym założeniem (traktujemy kategorie jak równomiernie rozstawione punkty na osi liczbowej).\n", "\n", "3) **Skala przedziałowa**\n", "\n", "- Równe odstępy, brak absolutnego zera (np. temperatura w °C; także niektóre skale standaryzowane w testach).\n", "- Różnice mają sens (o 10 jednostek więcej), ilorazy już nie (20 nie jest „dwa razy” 10).\n", "- Średnia i odchylenie standardowe są interpretowalne; typowe wykresy to histogram i krzywa gęstości.\n", "\n", "4) **Skala ilorazowa**\n", "\n", "- Jak przedziałowa, ale z naturalnym zerem (np. czas reakcji, czas trwania, liczba poprawnych odpowiedzi).\n", "- Różnice i ilorazy mają sens; możemy stosować standardowe miary i wykresy dla danych ilościowych.\n", "\n", "W dalszej części notatnika będziemy pracować głównie na zmiennych ilościowych (czas reakcji), dlatego pojawią się histogramy, miary centrum i miary rozproszenia.\n" ] }, { "cell_type": "markdown", "id": "01000048", "metadata": {}, "source": [ "## Komputery i odtwarzalność\n", "\n", "W praktyce większość obliczeń wykonuje się w programie statystycznym. To zmniejsza ryzyko błędów rachunkowych, ale nie zwalnia z myślenia: trzeba umieć dobrać miarę do skali pomiarowej i sprawdzić, czy wynik ma sens.\n", "\n", "W tym kursie używamy notatników Jupyter:\n", "\n", "- notatnik powinien uruchamiać się od góry do dołu,\n", "- tam gdzie pojawia się losowość, ustawiamy ziarno generatora, aby wynik był odtwarzalny.\n" ] }, { "cell_type": "markdown", "id": "01000003", "metadata": {}, "source": [ "## Dane przykładowe: czasy reakcji\n", "\n", "Zaczniemy od klasycznego przykładu z psychologii poznawczej: **zadania Sternberga** (przeszukiwanie pamięci krótkotrwałej).\n", "\n", "W każdej próbie badany:\n", "\n", "1. zapamiętuje krótki zestaw elementów (np. cyfr),\n", "2. widzi bodziec testowy,\n", "3. odpowiada, czy bodziec testowy był w zestawie (**odpowiedź pozytywna**) czy nie (**odpowiedź negatywna**).\n", "\n", "Jedna z prostych, opisowych hipotez (na razie bez testów) brzmi: **im większy zestaw do przeszukania, tym dłuższy czas reakcji**.\n", "\n", "Dla celów dydaktycznych stworzymy mały, **syntetyczny** zbiór danych o podobnej strukturze. To nie są dane empiryczne, ale zachowują typowy „kształt problemu”.\n", "\n", "Ustawiamy też stałe ziarno generatora liczb losowych (`SEED`), aby wyniki były w pełni odtwarzalne.\n", "\n", "Zmienne w zbiorze danych:\n", "\n", "- `set_size` — liczba elementów w zestawie (1, 3, 5),\n", "- `response` — odpowiedź „pozytywna” lub „negatywna”,\n", "- `reaction_time_ms` — czas reakcji w milisekundach.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000004", "metadata": {}, "outputs": [], "source": [ "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", "SEED = 20250116\n", "\n", "sns.set_theme(style='whitegrid')\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000005", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(SEED)\n", "\n", "set_sizes = [1, 3, 5]\n", "response_types = ['pozytywna', 'negatywna']\n", "\n", "trials_per_condition = 25\n", "\n", "rows = []\n", "\n", "for set_size in set_sizes:\n", " for response in response_types:\n", " if response == 'pozytywna':\n", " mean_ms = 420 + 35 * set_size\n", " else:\n", " mean_ms = 450 + 35 * set_size\n", "\n", " sd_ms = 45\n", "\n", " reaction_times = rng.normal(\n", " loc=mean_ms,\n", " scale=sd_ms,\n", " size=trials_per_condition,\n", " )\n", " reaction_times = np.clip(reaction_times, a_min=200, a_max=None)\n", "\n", " for rt in reaction_times:\n", " rows.append(\n", " {\n", " 'set_size': int(set_size),\n", " 'response': response,\n", " 'reaction_time_ms': float(rt),\n", " }\n", " )\n", "\n", "df = pd.DataFrame(rows)\n", "df.head(10)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000006", "metadata": {}, "outputs": [], "source": [ "print('Wymiary (wiersze, kolumny):', df.shape)\n", "print('\\nBraki danych:')\n", "print(df.isna().sum())\n", "\n", "df[['reaction_time_ms']].describe()\n" ] }, { "cell_type": "markdown", "id": "01000007", "metadata": {}, "source": [ "## Organizacja danych: tabela częstości i proste wykresy\n", "\n", "Surowe dane (lista liczb) zwykle trudno interpretować. Pierwszy krok to ich uporządkowanie: zliczamy, jak często pojawiają się poszczególne wartości (tabela częstości) albo przedziały wartości.\n", "\n", "Ponieważ czasy reakcji są mierzone w milisekundach i mają część ułamkową, na potrzeby tabeli częstości zaokrąglimy je do **10 ms**, czyli do **setnych części sekundy (0.01 s)**. Dzięki temu wiele obserwacji trafi do tych samych kategorii.\n", "\n", "Uwaga: takie zaokrąglenie jest tylko narzędziem opisu i wizualizacji — nie zmienia sensu danych, o ile nie jest zbyt agresywne.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000008", "metadata": {}, "outputs": [], "source": [ "df['reaction_time_cs'] = (df['reaction_time_ms'] / 10).round().astype(int) # 1 cs = 10 ms\n", "\n", "freq = df['reaction_time_cs'].value_counts().sort_index()\n", "freq_df = freq.rename_axis('reaction_time_cs').reset_index(name='frequency')\n", "\n", "freq_df.head(15)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000009", "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(10, 4))\n", "plt.plot(\n", " freq_df['reaction_time_cs'],\n", " freq_df['frequency'],\n", " marker='o',\n", " linewidth=2,\n", ")\n", "\n", "plt.title('Wielokąt częstości (z tabeli częstości)')\n", "plt.xlabel('Czas reakcji [0.01 s]')\n", "plt.ylabel('Częstość')\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "01000010", "metadata": {}, "source": [ "### Zależność między zmiennymi: wykres punktowy i średnie\n", "\n", "Opis rozkładu jednej zmiennej to początek. Często chcemy też zobaczyć zależność między zmiennymi: czy średni czas reakcji rośnie, gdy rośnie `set_size`?\n", "\n", "Najpierw pokażemy wszystkie obserwacje (wykres punktowy), a potem podsumujemy dane średnią i zmiennością w każdej grupie.\n", "\n", "Uwaga: na wykresie ze średnimi pokażemy **błąd standardowy średniej** (SEM). To nie jest \"rozrzut danych\" (tym jest odchylenie standardowe), tylko niepewność oszacowania średniej — maleje, gdy rośnie liczebność próby.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000011", "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(8, 4))\n", "sns.scatterplot(\n", " data=df,\n", " x='set_size',\n", " y='reaction_time_ms',\n", " hue='response',\n", " alpha=0.6,\n", ")\n", "\n", "plt.title('Czas reakcji a wielkość zestawu')\n", "plt.xlabel('Wielkość zestawu')\n", "plt.ylabel('Czas reakcji [ms]')\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000012", "metadata": {}, "outputs": [], "source": [ "grouped = df.groupby(['set_size', 'response'])['reaction_time_ms']\n", "summary = grouped.agg(mean='mean', std='std', n='count').reset_index()\n", "summary['sem'] = summary['std'] / np.sqrt(summary['n'])\n", "summary\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000013", "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(8, 4))\n", "\n", "for response in response_types:\n", " subset = summary[summary['response'] == response]\n", " plt.errorbar(\n", " subset['set_size'],\n", " subset['mean'],\n", " yerr=subset['sem'],\n", " marker='o',\n", " capsize=4,\n", " linewidth=2,\n", " label=response,\n", " )\n", "\n", "plt.title('Średni czas reakcji (z błędem standardowym średniej)')\n", "plt.xlabel('Wielkość zestawu')\n", "plt.ylabel('Średni czas reakcji [ms]')\n", "plt.legend(title='Odpowiedź')\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "681b977e", "metadata": {}, "source": [ "### Co na razie widzimy (opis, bez wnioskowania)\n", "\n", "Na wykresach możemy opisać kilka rzeczy:\n", "\n", "- W każdej grupie czasy reakcji są rozproszone: pojedyncze obserwacje różnią się od siebie nawet przy tej samej wielkości zestawu.\n", "- Średni czas reakcji rośnie wraz z wielkością zestawu (`set_size`), co jest zgodne z intuicją „więcej do przeszukania → wolniejsza odpowiedź”.\n", "- Różnice między odpowiedzią pozytywną i negatywną mogą się pojawiać, ale na tym etapie **nie rozstrzygamy**, czy są „prawdziwe w populacji” — do tego potrzebujemy wnioskowania statystycznego.\n", "\n", "Ważne: słupki błędu na wykresie średnich pokazują **błąd standardowy średniej** (SEM), czyli miarę niepewności oszacowania średniej, a nie miarę rozproszenia danych." ] }, { "cell_type": "markdown", "id": "01000014", "metadata": {}, "source": [ "## Histogramy\n", "\n", "Histogram pokazuje, jak dane rozkładają się wzdłuż osi liczbowej. Oś X dzielimy na **przedziały** o stałej szerokości, a następnie zliczamy, ile obserwacji wpada do każdego przedziału.\n", "\n", "Histogram pomaga odpowiedzieć na pytania: czy rozkład jest symetryczny, czy ma długi ogon, czy jest jedno- lub wielomodalny (ma jeden lub kilka „szczytów”).\n", "\n", "Praktyczna uwaga: wygląd histogramu zależy od doboru przedziałów (ich szerokości i położenia granic). Dlatego warto sprawdzić kilka sensownych ustawień.\n", "\n", "Na osi Y możemy pokazać liczbę obserwacji (częstość) albo gęstość (tak, aby pole pod słupkami było równe 1). Poniżej użyjemy gęstości.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000015", "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(10, 4))\n", "sns.histplot(\n", " data=df,\n", " x='reaction_time_ms',\n", " bins=14,\n", " stat='density',\n", " color='lightgrey',\n", " edgecolor='white',\n", ")\n", "\n", "plt.title('Histogram czasów reakcji (14 koszyków)')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000016", "metadata": {}, "outputs": [], "source": [ "reaction_times = df['reaction_time_ms'].to_numpy()\n", "\n", "bin_width = 30\n", "start_a = 300\n", "start_b = 315\n", "\n", "bins_a = np.arange(start_a, reaction_times.max() + bin_width, bin_width)\n", "bins_b = np.arange(start_b, reaction_times.max() + bin_width, bin_width)\n", "\n", "plt.figure(figsize=(12, 4))\n", "\n", "plt.subplot(1, 2, 1)\n", "plt.hist(\n", " reaction_times,\n", " bins=bins_a,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", ")\n", "plt.title('Histogram: początek=300, szerokość=30')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "\n", "plt.subplot(1, 2, 2)\n", "plt.hist(\n", " reaction_times,\n", " bins=bins_b,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", ")\n", "plt.title('Histogram: początek=315, szerokość=30')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "\n", "plt.tight_layout()\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "a724e035", "metadata": {}, "source": [ "## Interaktywny histogram: szerokość i początek przedziałów\n", "\n", "Poniższy widget pozwala zmieniać **szerokość przedziałów** oraz ich **punkt startowy** i od razu obserwować, jak wpływa to na wygląd histogramu.\n", "\n", "Cel ćwiczenia jest prosty: zobaczyć, że histogram jest użytecznym opisem rozkładu, ale jego szczegóły zależą od sensownego doboru parametrów.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "0173729a", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " bin_width_ms_slider = widgets.IntSlider(\n", " value=30,\n", " min=10,\n", " max=80,\n", " step=5,\n", " description='Szerokość [ms]:',\n", " continuous_update=False,\n", " )\n", "\n", " start_ms_slider = widgets.IntSlider(\n", " value=300,\n", " min=200,\n", " max=420,\n", " step=5,\n", " description='Początek [ms]:',\n", " continuous_update=False,\n", " )\n", "\n", " y_axis_scale_toggle = widgets.ToggleButtons(\n", " options=['Gęstość', 'Częstość'],\n", " value='Gęstość',\n", " description='Oś Y:',\n", " )\n", "\n", " histogram_out = widgets.Output()\n", "\n", " def draw_histogram(_=None):\n", " bin_width_ms = int(bin_width_ms_slider.value)\n", " start_ms = int(start_ms_slider.value)\n", " y_scale = str(y_axis_scale_toggle.value)\n", "\n", " bins = np.arange(start_ms, reaction_times.max() + bin_width_ms, bin_width_ms)\n", "\n", " with histogram_out:\n", " histogram_out.clear_output(wait=True)\n", "\n", " fig, ax = plt.subplots(figsize=(10, 4))\n", "\n", " if y_scale == 'Gęstość':\n", " ax.hist(\n", " reaction_times,\n", " bins=bins,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " ax.set_ylabel('Gęstość')\n", " else:\n", " ax.hist(\n", " reaction_times,\n", " bins=bins,\n", " density=False,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " ax.set_ylabel('Częstość')\n", "\n", " ax.set_title('Histogram czasów reakcji: wpływ wyboru przedziałów')\n", " ax.set_xlabel('Czas reakcji [ms]')\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " for control in [bin_width_ms_slider, start_ms_slider, y_axis_scale_toggle]:\n", " control.observe(draw_histogram, names='value')\n", "\n", " controls = widgets.HBox([bin_width_ms_slider, start_ms_slider, y_axis_scale_toggle])\n", " display(controls, histogram_out)\n", "\n", " draw_histogram()\n" ] }, { "cell_type": "markdown", "id": "01000017", "metadata": {}, "source": [ "## Wygładzanie rozkładu: krzywa normalna i estymacja jądrowa gęstości (KDE)\n", "\n", "Histogram jest wykresem „schodkowym” i zależy od doboru przedziałów. Innym sposobem jest narysowanie krzywej, która **wygładza** rozkład.\n", "\n", "Dwie popularne strategie:\n", "\n", "1. Dopasować krzywą rozkładu normalnego o parametrach oszacowanych z danych (podejście parametryczne).\n", "2. Zastosować **estymację jądrową gęstości** (KDE) — wygładzanie bez przyjmowania konkretnej rodziny rozkładów.\n", "\n", "To są narzędzia **opisowe**: nie „dowodzą normalności” i nie zastępują interpretacji merytorycznej. Estymacja jądrowa również ma parametr wygładzania (analogicznie do tego, że histogram ma szerokość przedziałów).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000018", "metadata": {}, "outputs": [], "source": [ "mu_hat = float(reaction_times.mean())\n", "sd_hat = float(reaction_times.std(ddof=1))\n", "\n", "x_grid = np.linspace(reaction_times.min(), reaction_times.max(), 400)\n", "normal_pdf = stats.norm.pdf(x_grid, loc=mu_hat, scale=sd_hat)\n", "\n", "plt.figure(figsize=(10, 4))\n", "plt.hist(\n", " reaction_times,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", ")\n", "plt.plot(x_grid, normal_pdf, color='black', linewidth=2, label='Dopasowana normalna')\n", "\n", "plt.title('Histogram + dopasowana krzywa normalna')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "plt.legend()\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000019", "metadata": {}, "outputs": [], "source": [ "kde = stats.gaussian_kde(reaction_times)\n", "kde_pdf = kde(x_grid)\n", "\n", "plt.figure(figsize=(10, 4))\n", "plt.hist(\n", " reaction_times,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", ")\n", "plt.plot(x_grid, kde_pdf, color='C0', linewidth=2, label='Estymacja jądrowa gęstości')\n", "plt.plot(\n", " x_grid,\n", " normal_pdf,\n", " color='black',\n", " linestyle='--',\n", " linewidth=2,\n", " label='Dopasowana normalna',\n", ")\n", "\n", "plt.title('Histogram + estymacja jądrowa gęstości')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "plt.legend()\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "5b38305c", "metadata": {}, "source": [ "## Interaktywne wygładzanie: estymacja jądrowa gęstości\n", "\n", "W estymacji jądrowej gęstości kluczowy jest **parametr wygładzania**.\n", "\n", "- Mniejsze wygładzanie daje bardziej „poszarpaną” krzywą (łatwiej zobaczyć drobne fluktuacje, ale też łatwiej pomylić szum ze strukturą).\n", "- Większe wygładzanie daje gładszą krzywą (łatwiej zobaczyć ogólny kształt, ale można ukryć ważne szczegóły, np. dwumodalność).\n", "\n", "Poniższy suwak pozwala zmieniać wygładzanie i od razu obserwować efekt na wykresie." ] }, { "cell_type": "code", "execution_count": null, "id": "c145de62", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " kde_default = stats.gaussian_kde(reaction_times)\n", " default_smoothing = float(kde_default.factor)\n", "\n", " smoothing_slider = widgets.FloatSlider(\n", " value=default_smoothing,\n", " min=0.15,\n", " max=1.50,\n", " step=0.01,\n", " description='Wygładzanie:',\n", " readout_format='.2f',\n", " continuous_update=False,\n", " )\n", "\n", " kde_out = widgets.Output()\n", "\n", " def draw_kde(_=None):\n", " smoothing = float(smoothing_slider.value)\n", " x_grid = np.linspace(reaction_times.min(), reaction_times.max(), 400)\n", " kde = stats.gaussian_kde(reaction_times, bw_method=smoothing)\n", " kde_pdf = kde(x_grid)\n", "\n", " with kde_out:\n", " kde_out.clear_output(wait=True)\n", "\n", " fig, ax = plt.subplots(figsize=(10, 4))\n", " ax.hist(\n", " reaction_times,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " ax.plot(x_grid, kde_pdf, color='C0', linewidth=2, label='Estymacja jądrowa gęstości')\n", "\n", " ax.set_title(f'Estymacja jądrowa gęstości (wygładzanie = {smoothing:.2f})')\n", " ax.set_xlabel('Czas reakcji [ms]')\n", " ax.set_ylabel('Gęstość')\n", " ax.legend()\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " smoothing_slider.observe(draw_kde, names='value')\n", "\n", " display(smoothing_slider, kde_out)\n", " draw_kde()\n" ] }, { "cell_type": "markdown", "id": "43fcf3c8", "metadata": {}, "source": [ "## Przykład kognitywistyczny: dwie strategie i rozkład dwumodalny\n", "\n", "Czasem w danych widać więcej niż jeden „szczyt” rozkładu. W psychologii poznawczej może to wynikać z tego, że część osób (albo część prób) przebiega według innej strategii.\n", "\n", "Prosty przykład: w zadaniu decyzyjnym badany może czasem odpowiadać **automatycznie** (szybko), a czasem **w sposób kontrolowany** (wolniej), np. gdy bodziec jest niejednoznaczny. Jeśli te dwa tryby mieszają się w danych, rozkład czasów reakcji może wyglądać na dwumodalny.\n", "\n", "Poniżej tworzymy syntetyczne dane z dwóch strategii i sprawdzamy, jak wyglądają na histogramie oraz po wygładzeniu." ] }, { "cell_type": "code", "execution_count": null, "id": "4ec23600", "metadata": {}, "outputs": [], "source": [ "rng_strategie = np.random.default_rng(SEED + 2)\n", "\n", "n_trials = 200\n", "p_szybka = 0.65\n", "\n", "strategie = rng_strategie.choice(\n", " ['szybka', 'wolna'],\n", " size=n_trials,\n", " p=[p_szybka, 1 - p_szybka],\n", ")\n", "\n", "rt_szybka = rng_strategie.normal(loc=430, scale=35, size=n_trials)\n", "rt_wolna = rng_strategie.normal(loc=650, scale=45, size=n_trials)\n", "\n", "rt = np.where(strategie == 'szybka', rt_szybka, rt_wolna)\n", "rt = np.clip(rt, a_min=200, a_max=None)\n", "\n", "strategie_df = pd.DataFrame({'strategia': strategie, 'reaction_time_ms': rt})\n", "strategie_df.head()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "fd43560d", "metadata": {}, "outputs": [], "source": [ "x_grid = np.linspace(strategie_df['reaction_time_ms'].min(), strategie_df['reaction_time_ms'].max(), 400)\n", "kde = stats.gaussian_kde(strategie_df['reaction_time_ms'])\n", "kde_pdf = kde(x_grid)\n", "\n", "plt.figure(figsize=(12, 4))\n", "\n", "plt.subplot(1, 2, 1)\n", "sns.histplot(\n", " data=strategie_df,\n", " x='reaction_time_ms',\n", " bins=30,\n", " stat='density',\n", " color='lightgrey',\n", " edgecolor='white',\n", ")\n", "plt.plot(x_grid, kde_pdf, color='black', linewidth=2, label='Estymacja jądrowa gęstości')\n", "plt.title('Dwie strategie: histogram + wygładzenie')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "plt.legend()\n", "\n", "plt.subplot(1, 2, 2)\n", "sns.histplot(\n", " data=strategie_df,\n", " x='reaction_time_ms',\n", " hue='strategia',\n", " bins=30,\n", " stat='density',\n", " element='step',\n", " common_norm=False,\n", ")\n", "plt.title('To samo, z rozbiciem na strategie')\n", "plt.xlabel('Czas reakcji [ms]')\n", "plt.ylabel('Gęstość')\n", "plt.legend(title='Strategia')\n", "\n", "plt.tight_layout()\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "01000020", "metadata": {}, "source": [ "## Mała próba: wykres punktowy i wykres łodygowo-liściowy\n", "\n", "Dla niewielkich zbiorów danych warto używać wizualizacji, które pokazują **każdą obserwację**.\n", "\n", "Poniżej rozważmy mały zestaw wyników punktowych (np. liczba punktów w krótkim teście poznawczym). Takie dane są dyskretne i łatwo je uporządkować.\n", "\n", "- Wykres łodygowo-liściowy to zapis tekstowy, który porządkuje liczby i jednocześnie zachowuje ich konkretne wartości.\n", "- Wykres punktowy w 1D pokazuje każdą obserwację jako punkt na osi liczbowej.\n", "\n", "Zobaczymy oba podejścia na tej samej liście wyników.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000021", "metadata": {}, "outputs": [], "source": [ "scores = [\n", " 42,\n", " 47,\n", " 51,\n", " 56,\n", " 56,\n", " 57,\n", " 60,\n", " 61,\n", " 63,\n", " 67,\n", " 68,\n", " 71,\n", " 73,\n", " 74,\n", " 74,\n", " 77,\n", " 82,\n", " 88,\n", "]\n", "\n", "scores\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000022", "metadata": {}, "outputs": [], "source": [ "scores_df = pd.DataFrame({'score': scores})\n", "\n", "plt.figure(figsize=(10, 2))\n", "sns.stripplot(data=scores_df, x='score', jitter=0, size=7, color='C0')\n", "plt.title('Wykres punktowy dla małej próby')\n", "plt.xlabel('Wynik [pkt]')\n", "plt.yticks([])\n", "plt.show()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000023", "metadata": {}, "outputs": [], "source": [ "def stem_and_leaf(values, stem_unit=10):\n", " \"\"\"Zwróć tekstowy wykres łodygowo-liściowy.\n", "\n", " Ideą jest prosta: dzielimy liczbę na \"łodygę\" (np. dziesiątki) i \"liść\" (np. jedności),\n", " a potem zapisujemy liście w kolejności rosnącej.\n", " \"\"\"\n", " sorted_values = sorted([int(v) for v in values])\n", "\n", " stems = {}\n", "\n", " for value in sorted_values:\n", " stem = value // stem_unit\n", " leaf = value % stem_unit\n", "\n", " if stem not in stems:\n", " stems[stem] = []\n", " stems[stem].append(leaf)\n", "\n", " lines = []\n", "\n", " for stem in sorted(stems):\n", " leaves_as_text = []\n", " for leaf in stems[stem]:\n", " leaves_as_text.append(str(leaf))\n", "\n", " leaves = ' '.join(leaves_as_text)\n", " lines.append(f'{stem:>2} | {leaves}')\n", "\n", " return '\\n'.join(lines)\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000024", "metadata": {}, "outputs": [], "source": [ "print(stem_and_leaf(scores))\n" ] }, { "cell_type": "markdown", "id": "01000025", "metadata": {}, "source": [ "## Jak opisać rozkład danych: cztery pytania\n", "\n", "Kiedy opisujemy jedną zmienną ilościową, warto konsekwentnie odpowiedzieć na cztery pytania:\n", "\n", "1. **Centrum**: jaka jest typowa wartość? (np. średnia, mediana)\n", "2. **Rozproszenie**: jak bardzo wyniki różnią się między sobą? (np. odchylenie standardowe, IQR)\n", "3. **Kształt**: czy rozkład jest symetryczny, czy ma ogon? (pomagają histogram i krzywa gęstości oraz porównanie średniej i mediany)\n", "4. **Obserwacje odstające**: czy są wartości wyraźnie oddzielone od reszty?\n", "\n", "Dobrym nawykiem jest łączenie wykresu z jedną–dwiema miarami liczbowymi, zamiast polegać wyłącznie na tabelce.\n", "\n", "Jedną z prostych miar kształtu jest **skośność**: dodatnia skośność oznacza zwykle \"ogon\" po prawej stronie (kilka bardzo dużych wartości), a ujemna — ogon po lewej.\n", "\n", "Uwaga kognitywistyczna: czasy reakcji często mają **dodatnią skośność** (długi prawy ogon). Jednym z powodów mogą być sporadyczne **przerwy uwagi** (chwilowe rozproszenie), które produkują bardzo długie czasy reakcji." ] }, { "cell_type": "code", "execution_count": null, "id": "01000026", "metadata": {}, "outputs": [], "source": [ "mean_rt = float(reaction_times.mean())\n", "median_rt = float(np.median(reaction_times))\n", "sd_rt = float(reaction_times.std(ddof=1))\n", "\n", "skew_rt = float(stats.skew(reaction_times, bias=False))\n", "\n", "summary_shape = pd.DataFrame(\n", " {\n", " 'miara': ['średnia', 'mediana', 'odchylenie standardowe (s, ddof=1)', 'skośność'],\n", " 'wartość': [mean_rt, median_rt, sd_rt, skew_rt],\n", " }\n", ")\n", "summary_shape\n" ] }, { "cell_type": "markdown", "id": "0af0aa6a", "metadata": {}, "source": [ "### Interaktywny przykład: przerwy uwagi jako obserwacje odstające\n", "\n", "W danych z czasami reakcji często obserwujemy sporadycznie bardzo długie odpowiedzi. Jednym z możliwych wyjaśnień są **chwilowe przerwy uwagi** (krótkie rozproszenie), które wydłużają czas reakcji w pojedynczych próbach.\n", "\n", "Poniższy widget pozwala dodać do danych kilka takich prób i zobaczyć, jak zmieniają się miary opisowe. Zwróć uwagę, że średnia zwykle reaguje silniej niż mediana i średnia obcięta.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "2ab8965d", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " lapse_value_slider = widgets.IntSlider(\n", " value=1200,\n", " min=800,\n", " max=3000,\n", " step=50,\n", " description='Wartość [ms]:',\n", " continuous_update=False,\n", " )\n", "\n", " n_lapses_slider = widgets.IntSlider(\n", " value=1,\n", " min=0,\n", " max=10,\n", " step=1,\n", " description='Liczba prób:',\n", " continuous_update=False,\n", " )\n", "\n", " outlier_out = widgets.Output()\n", "\n", " def summarize(values_ms):\n", " values_ms = np.asarray(values_ms, dtype=float)\n", " q1 = float(np.percentile(values_ms, 25))\n", " q3 = float(np.percentile(values_ms, 75))\n", "\n", " return {\n", " 'n': int(values_ms.size),\n", " 'średnia [ms]': float(values_ms.mean()),\n", " 'mediana [ms]': float(np.median(values_ms)),\n", " 'średnia obcięta 10% [ms]': float(stats.trim_mean(values_ms, proportiontocut=0.1)),\n", " 'odchylenie standardowe (ddof=1) [ms]': float(values_ms.std(ddof=1)),\n", " 'IQR [ms]': q3 - q1,\n", " }\n", "\n", " def draw_outlier_demo(_=None):\n", " lapse_value_ms = int(lapse_value_slider.value)\n", " n_lapses = int(n_lapses_slider.value)\n", "\n", " base = reaction_times\n", " if n_lapses > 0:\n", " lapses = np.full(shape=n_lapses, fill_value=lapse_value_ms, dtype=float)\n", " with_lapses = np.concatenate([base, lapses])\n", " else:\n", " with_lapses = base.copy()\n", "\n", " summary_df = pd.DataFrame(\n", " [summarize(base), summarize(with_lapses)],\n", " index=['bez przerw uwagi', 'z przerwami uwagi'],\n", " ).reset_index(names='wariant')\n", "\n", " with outlier_out:\n", " outlier_out.clear_output(wait=True)\n", "\n", " fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharex=True, sharey=True)\n", "\n", " for ax, values_ms, title in [\n", " (axes[0], base, 'Bez przerw uwagi'),\n", " (axes[1], with_lapses, f'Z przerwami uwagi (dodano: {n_lapses} × {lapse_value_ms} ms)'),\n", " ]:\n", " ax.hist(\n", " values_ms,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", "\n", " mean_ms = float(np.mean(values_ms))\n", " median_ms = float(np.median(values_ms))\n", "\n", " ax.axvline(mean_ms, color='C3', linewidth=2, label='średnia')\n", " ax.axvline(median_ms, color='C0', linestyle='--', linewidth=2, label='mediana')\n", "\n", " ax.set_title(title)\n", " ax.set_xlabel('Czas reakcji [ms]')\n", "\n", " axes[0].set_ylabel('Gęstość')\n", " axes[0].legend()\n", " axes[1].legend()\n", "\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " display(summary_df)\n", "\n", " for control in [lapse_value_slider, n_lapses_slider]:\n", " control.observe(draw_outlier_demo, names='value')\n", "\n", " display(widgets.HBox([n_lapses_slider, lapse_value_slider]), outlier_out)\n", " draw_outlier_demo()\n" ] }, { "cell_type": "markdown", "id": "01000028", "metadata": {}, "source": [ "## Notacja: populacja a próba\n", "\n", "W statystyce rozróżniamy to, co dotyczy populacji (zwykle nieznanej), i to, co liczymy z próby.\n", "\n", "- **Parametry populacji**: np. średnia $\\mu$ i odchylenie standardowe $\\sigma$.\n", "- **Statystyki z próby**: np. średnia z próby $\\bar{x}$ i odchylenie standardowe z próby $s$.\n", "\n", "W praktyce prawie zawsze mamy do dyspozycji próbę, więc liczymy $\\bar{x}$ i $s$. Różnica między $\\sigma$ i $s$ pojawia się m.in. we wzorze na wariancję: w próbie dzielimy przez $(n-1)$, a nie przez $n$.\n", "\n", "W Pythonie kontrolujemy to parametrem `ddof` (delta degrees of freedom):\n", "\n", "- `ddof=0` — dzielenie przez $n$,\n", "- `ddof=1` — dzielenie przez $(n-1)$.\n" ] }, { "cell_type": "markdown", "id": "01000029", "metadata": {}, "source": [ "## Miary tendencji centralnej\n", "\n", "Miary tendencji centralnej opisują \"typową\" wartość w zbiorze.\n", "\n", "**Średnia arytmetyczna**\n", "\n", "$$\n", "\\bar{x} = \\frac{1}{n}\\sum_{i=1}^n x_i\n", "$$\n", "\n", "Jest wygodna i ma dobre własności matematyczne, ale jest wrażliwa na obserwacje odstające.\n", "\n", "**Mediana** to wartość środkowa w uporządkowanym zbiorze (a przy parzystej liczbie obserwacji — średnia z dwóch środkowych). Jest bardziej odporna na odstające.\n", "\n", "**Moda** to najczęstsza wartość. Dla danych ciągłych zwykle nie ma jednej \"mody\" bez dodatkowego grupowania/zaokrąglania.\n", "\n", "**Średnia obcięta** usuwa określony procent najmniejszych i największych obserwacji, a potem liczy średnią. To kompromis między średnią a medianą.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000030", "metadata": {}, "outputs": [], "source": [ "# 1) Mały przykład „na piechotę”: policzmy miary centrum dla listy wyników.\n", "scores_sorted = sorted(scores)\n", "n_scores = len(scores_sorted)\n", "\n", "mean_scores = sum(scores_sorted) / n_scores\n", "\n", "# Mediana: przy parzystej liczbie obserwacji bierzemy średnią z dwóch środkowych.\n", "middle_left = scores_sorted[n_scores // 2 - 1]\n", "middle_right = scores_sorted[n_scores // 2]\n", "median_scores = (middle_left + middle_right) / 2\n", "\n", "# Średnia obcięta: obcinamy po 10% z obu stron (tu: floor(0.1*n) obserwacji).\n", "g = int(0.1 * n_scores)\n", "trimmed_scores = scores_sorted[g : n_scores - g]\n", "trimmed_mean_scores = sum(trimmed_scores) / len(trimmed_scores)\n", "\n", "# Moda (dla danych dyskretnych): wartość o największej częstości.\n", "counts = {}\n", "for value in scores_sorted:\n", " counts[value] = counts.get(value, 0) + 1\n", "\n", "mode_scores = max(counts, key=counts.get)\n", "mode_count = counts[mode_scores]\n", "\n", "print('Przykład (scores): n =', n_scores)\n", "print('Średnia:', mean_scores)\n", "print('Mediana:', median_scores)\n", "print('Średnia obcięta (10%):', trimmed_mean_scores)\n", "print('Moda:', mode_scores, '(liczność:', mode_count, ')')\n", "\n", "# Wpływ odstającej wartości (zwykle mocniej przesuwa średnią niż medianę).\n", "scores_with_outlier = scores_sorted + [200]\n", "mean_scores_outlier = sum(scores_with_outlier) / len(scores_with_outlier)\n", "median_scores_outlier = float(np.median(scores_with_outlier))\n", "\n", "print('\\nWpływ odstającej wartości (200):')\n", "print('Średnia:', mean_scores_outlier)\n", "print('Mediana:', median_scores_outlier)\n", "\n", "# 2) To samo dla naszej zmiennej: czas reakcji (ms).\n", "reaction_time_series = df['reaction_time_ms']\n", "\n", "mean_rt_ms = float(reaction_time_series.mean())\n", "median_rt_ms = float(reaction_time_series.median())\n", "trimmed_mean_rt_ms = float(stats.trim_mean(reaction_time_series, proportiontocut=0.1))\n", "\n", "# Moda dla danych ciągłych: przybliżenie po zaokrągleniu do 0.01 s.\n", "mode_cs = int(df['reaction_time_cs'].value_counts().idxmax())\n", "mode_ms_approx = mode_cs * 10\n", "\n", "central = pd.DataFrame(\n", " {\n", " 'miara': [\n", " 'średnia',\n", " 'mediana',\n", " 'średnia obcięta (10%)',\n", " 'moda (po zaokr. do 0.01 s)',\n", " ],\n", " 'wartość': [mean_rt_ms, median_rt_ms, trimmed_mean_rt_ms, mode_ms_approx],\n", " 'jednostka': ['ms', 'ms', 'ms', 'ms'],\n", " }\n", ")\n", "central\n" ] }, { "cell_type": "markdown", "id": "a39a6693", "metadata": {}, "source": [ "### Interaktywna średnia obcięta\n", "\n", "Średnia obcięta jest kompromisem między średnią a medianą.\n", "\n", "- Najpierw usuwamy określony odsetek najmniejszych i największych obserwacji,\n", "- a następnie liczymy średnią z pozostałych danych.\n", "\n", "Suwak poniżej ustala, jaki odsetek obcinamy **z każdej strony** (np. 0,10 oznacza 10% najmniejszych i 10% największych wartości).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "d9d104a2", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " obciecie_slider = widgets.FloatSlider(\n", " value=0.10,\n", " min=0.00,\n", " max=0.40,\n", " step=0.05,\n", " description='Obcięcie:',\n", " readout_format='.2f',\n", " continuous_update=False,\n", " )\n", "\n", " trimmed_out = widgets.Output()\n", "\n", " def draw_trimmed_mean(_=None):\n", " proportion = float(obciecie_slider.value)\n", " x = np.asarray(reaction_times, dtype=float)\n", " n = int(x.size)\n", " g = int(proportion * n)\n", "\n", " x_sorted = np.sort(x)\n", " if g > 0:\n", " x_trimmed = x_sorted[g : n - g]\n", " lower = float(x_sorted[g])\n", " upper = float(x_sorted[n - g - 1])\n", " else:\n", " x_trimmed = x_sorted\n", " lower = float(x_sorted[0])\n", " upper = float(x_sorted[-1])\n", "\n", " mean_x = float(x.mean())\n", " median_x = float(np.median(x))\n", " trimmed_mean_x = float(x_trimmed.mean())\n", "\n", " summary = pd.DataFrame(\n", " {\n", " 'miara': ['średnia', 'mediana', 'średnia obcięta'],\n", " 'wartość [ms]': [mean_x, median_x, trimmed_mean_x],\n", " }\n", " )\n", "\n", " with trimmed_out:\n", " trimmed_out.clear_output(wait=True)\n", "\n", " fig, ax = plt.subplots(figsize=(10, 4))\n", " ax.hist(\n", " x,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", "\n", " ax.axvline(mean_x, color='C3', linewidth=2, label='średnia')\n", " ax.axvline(median_x, color='C0', linestyle='--', linewidth=2, label='mediana')\n", " ax.axvline(trimmed_mean_x, color='C2', linestyle='-.', linewidth=2, label='średnia obcięta')\n", "\n", " if g > 0:\n", " ax.axvline(lower, color='black', linestyle=':', linewidth=2, label='granice obcięcia')\n", " ax.axvline(upper, color='black', linestyle=':', linewidth=2)\n", "\n", " ax.set_title(\n", " f'Średnia obcięta: obcięcie = {proportion:.2f} z każdej strony '\n", " f'(usunięto {g} + {g} obserwacji)'\n", " )\n", " ax.set_xlabel('Czas reakcji [ms]')\n", " ax.set_ylabel('Gęstość')\n", " ax.legend()\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " print(f'n = {n}, n po obcięciu = {int(x_trimmed.size)}')\n", " display(summary)\n", "\n", " obciecie_slider.observe(draw_trimmed_mean, names='value')\n", "\n", " display(obciecie_slider, trimmed_out)\n", " draw_trimmed_mean()\n" ] }, { "cell_type": "markdown", "id": "01000031", "metadata": {}, "source": [ "## Miary zmienności (rozproszenia)\n", "\n", "Miary zmienności mówią, **jak bardzo obserwacje różnią się między sobą**.\n", "\n", "**Wariancja** (dla próby):\n", "\n", "$$\n", "s^2 = \\frac{1}{n-1}\\sum_{i=1}^n (x_i-\\bar{x})^2\n", "$$\n", "\n", "To średni kwadrat odchyleń od średniej (z poprawką $n-1$). Wariancja ma jednostkę \"do kwadratu\" (np. ms²), co utrudnia interpretację.\n", "\n", "**Odchylenie standardowe** to pierwiastek z wariancji:\n", "\n", "$$\n", "s = \\sqrt{s^2}\n", "$$\n", "\n", "i ma tę samą jednostkę co dane (np. ms), dlatego jest interpretacyjnie wygodniejsze.\n", "\n", "Dodatkowo często stosuje się miary odporne na odstające:\n", "\n", "- **rozstęp** (max − min),\n", "- **IQR** (rozstęp międzykwartylowy, Q3 − Q1),\n", "- **MAD** — odchylenie medianowe (miara odporna na odstające).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000032", "metadata": {}, "outputs": [], "source": [ "# 1) Mały przykład „na piechotę”: wariancja i odchylenie standardowe dla `scores`.\n", "scores_array = np.array(scores, dtype=float)\n", "n_scores = int(scores_array.size)\n", "\n", "mean_scores = float(scores_array.mean())\n", "deviations = scores_array - mean_scores\n", "squared_deviations = deviations**2\n", "sum_squared = float(squared_deviations.sum())\n", "\n", "var_pop_manual = sum_squared / n_scores\n", "var_sample_manual = sum_squared / (n_scores - 1)\n", "\n", "sd_pop_manual = float(np.sqrt(var_pop_manual))\n", "sd_sample_manual = float(np.sqrt(var_sample_manual))\n", "\n", "print('scores: n =', n_scores)\n", "print('Wariancja (dzielenie przez n):', var_pop_manual)\n", "print('Wariancja (dzielenie przez n-1):', var_sample_manual)\n", "print('Odch. std (sqrt wariancji, n-1):', sd_sample_manual)\n", "\n", "print('\\nWeryfikacja (numpy):')\n", "print('var ddof=0:', float(scores_array.var(ddof=0)))\n", "print('var ddof=1:', float(scores_array.var(ddof=1)))\n", "print('std ddof=1:', float(scores_array.std(ddof=1)))\n", "\n", "# 2) Miary zmienności dla czasów reakcji (ms).\n", "rt = df['reaction_time_ms'].to_numpy()\n", "\n", "range_rt = float(rt.max() - rt.min())\n", "var_rt_pop = float(rt.var(ddof=0))\n", "var_rt_sample = float(rt.var(ddof=1))\n", "std_rt_pop = float(rt.std(ddof=0))\n", "std_rt_sample = float(rt.std(ddof=1))\n", "\n", "q1 = float(np.percentile(rt, 25))\n", "q3 = float(np.percentile(rt, 75))\n", "iqr = q3 - q1\n", "\n", "mad = float(stats.median_abs_deviation(rt, scale=1))\n", "\n", "dispersion = pd.DataFrame(\n", " {\n", " 'miara': [\n", " 'rozstęp',\n", " 'wariancja (ddof=0)',\n", " 'wariancja (ddof=1)',\n", " 'odchylenie standardowe (ddof=0)',\n", " 'odchylenie standardowe (ddof=1)',\n", " 'IQR',\n", " 'MAD (skala=1)',\n", " ],\n", " 'wartość': [range_rt, var_rt_pop, var_rt_sample, std_rt_pop, std_rt_sample, iqr, mad],\n", " 'jednostka': ['ms', 'ms^2', 'ms^2', 'ms', 'ms', 'ms', 'ms'],\n", " }\n", ")\n", "dispersion\n" ] }, { "cell_type": "markdown", "id": "fca94a14", "metadata": {}, "source": [ "## Dane przykładowe: efekt Stroopa (dwa warunki)\n", "\n", "W wielu badaniach z psychologii poznawczej porównujemy dwie proste sytuacje (dwa warunki). Klasyczny przykład to **efekt Stroopa**: nazwanie koloru tuszu jest zwykle wolniejsze, gdy słowo oznacza inny kolor (warunek **niezgodny**), niż gdy słowo i kolor są zgodne (warunek **zgodny**).\n", "\n", "Poniżej tworzymy niewielki, syntetyczny zbiór danych z dwoma warunkami. To przykład do ćwiczenia opisowych wykresów porównawczych (zwłaszcza wykresu pudełkowego)." ] }, { "cell_type": "code", "execution_count": null, "id": "bcf19b4a", "metadata": {}, "outputs": [], "source": [ "rng_stroop = np.random.default_rng(SEED + 1)\n", "\n", "conditions = ['zgodny', 'niezgodny']\n", "trials_per_condition_stroop = 40\n", "\n", "stroop_rows = []\n", "\n", "for condition in conditions:\n", " if condition == 'zgodny':\n", " mean_ms = 520\n", " else:\n", " mean_ms = 600\n", "\n", " sd_ms = 60\n", "\n", " stroop_reaction_times = rng_stroop.normal(\n", " loc=mean_ms,\n", " scale=sd_ms,\n", " size=trials_per_condition_stroop,\n", " )\n", " stroop_reaction_times = np.clip(stroop_reaction_times, a_min=200, a_max=None)\n", "\n", " for rt_value in stroop_reaction_times:\n", " stroop_rows.append({'condition': condition, 'reaction_time_ms': float(rt_value)})\n", "\n", "stroop_df = pd.DataFrame(stroop_rows)\n", "stroop_df.head(10)\n" ] }, { "cell_type": "markdown", "id": "01000033", "metadata": {}, "source": [ "## Wykres pudełkowy\n", "\n", "Wykres pudełkowy jest skróconym, ale bardzo informacyjnym opisem rozkładu.\n", "\n", "- Linia w środku pudełka to mediana.\n", "- Pudełko zwykle obejmuje przedział od Q1 do Q3 (czyli IQR).\n", "- \"Wąsy\" często sięgają do wartości w granicach 1.5×IQR, a punkty poza nimi są oznaczane jako potencjalne obserwacje odstające.\n", "\n", "Uwaga: obserwacja oznaczona jako odstająca nie musi być błędem. To sygnał, że warto sprawdzić kontekst pomiaru i interpretację.\n", "\n", "W dydaktyce często warto nałożyć na wykres pudełkowy punkty, aby zobaczyć rzeczywisty rozkład próby.\n", "\n", "W przykładzie poniżej porównamy dwa warunki w zadaniu Stroopa (zgodny i niezgodny)." ] }, { "cell_type": "code", "execution_count": null, "id": "01000034", "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(9, 4))\n", "order = ['zgodny', 'niezgodny']\n", "\n", "sns.boxplot(data=stroop_df, x='condition', y='reaction_time_ms', order=order, color='lightgrey')\n", "sns.stripplot(data=stroop_df, x='condition', y='reaction_time_ms', order=order, alpha=0.6, size=4)\n", "\n", "plt.title('Czas reakcji w zadaniu Stroopa: warunek zgodny i niezgodny')\n", "plt.xlabel('Warunek')\n", "plt.ylabel('Czas reakcji [ms]')\n", "plt.show()\n" ] }, { "cell_type": "markdown", "id": "01000035", "metadata": {}, "source": [ "## Szybkie statystyki opisowe w Pythonie\n", "\n", "W praktyce często chcemy szybko zobaczyć podstawowe miary dla zmiennej ilościowej.\n", "\n", "W `pandas` służy do tego `describe()`, które standardowo pokazuje m.in.:\n", "\n", "- `count` (liczebność),\n", "- `mean` (średnia),\n", "- `std` (odchylenie standardowe z próby),\n", "- `min`, `max`,\n", "- kwartyle: `25%`, `50%` (mediana), `75%`.\n", "\n", "To dobry punkt startowy, ale pamiętaj: zawsze warto zestawić te liczby z wykresem.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000036", "metadata": {}, "outputs": [], "source": [ "df['reaction_time_ms'].describe()\n" ] }, { "cell_type": "markdown", "id": "01000037", "metadata": {}, "source": [ "## Percentyle, kwartyle, decyle\n", "\n", "Percentyl $p$ to taka wartość, poniżej której leży $p\\%$ obserwacji.\n", "\n", "- 25. percentyl to pierwszy kwartyl (Q1),\n", "- 50. percentyl to mediana,\n", "- 75. percentyl to trzeci kwartyl (Q3).\n", "\n", "Percentyle są szczególnie przydatne, gdy rozkład jest asymetryczny albo gdy chcemy opisać \"typowy\" zakres bez polegania na średniej i odchyleniu standardowym.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000038", "metadata": {}, "outputs": [], "source": [ "percentiles = [10, 25, 50, 75, 90]\n", "values = np.percentile(rt, percentiles)\n", "\n", "percentile_table = pd.DataFrame(\n", " {\n", " 'percentyl': percentiles,\n", " 'wartość [ms]': values,\n", " }\n", ")\n", "percentile_table\n" ] }, { "cell_type": "markdown", "id": "6126b0bd", "metadata": {}, "source": [ "### Interaktywny percentyl: linia na histogramie\n", "\n", "Wybierz percentyl $p$ suwakiem i zobacz, gdzie leży jego wartość na histogramie czasów reakcji.\n", "\n", "Przypomnienie: $p$-ty percentyl to taka wartość, że około $p\\%$ obserwacji jest **nie większych** od tej wartości." ] }, { "cell_type": "code", "execution_count": null, "id": "a5b1673f", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " percentyl_slider = widgets.IntSlider(\n", " value=50,\n", " min=1,\n", " max=99,\n", " step=1,\n", " description='Percentyl:',\n", " continuous_update=False,\n", " )\n", "\n", " percentyl_out = widgets.Output()\n", "\n", " def draw_percentyl(_=None):\n", " p = int(percentyl_slider.value)\n", " reaction_times_ms = np.asarray(rt, dtype=float)\n", " value_ms = float(np.percentile(reaction_times_ms, p))\n", "\n", " with percentyl_out:\n", " percentyl_out.clear_output(wait=True)\n", "\n", " fig, ax = plt.subplots(figsize=(10, 4))\n", " ax.hist(\n", " reaction_times_ms,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " ax.axvline(\n", " value_ms,\n", " color='C3',\n", " linewidth=2,\n", " label=f'{p}. percentyl = {value_ms:.1f} ms',\n", " )\n", "\n", " ax.set_title('Percentyl na histogramie czasów reakcji')\n", " ax.set_xlabel('Czas reakcji [ms]')\n", " ax.set_ylabel('Gęstość')\n", " ax.legend()\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " print(f'{p}. percentyl: {value_ms:.1f} ms (około {p}% obserwacji jest ≤ tej wartości).')\n", "\n", " percentyl_slider.observe(draw_percentyl, names='value')\n", "\n", " display(percentyl_slider, percentyl_out)\n", " draw_percentyl()\n" ] }, { "cell_type": "markdown", "id": "f1af84d5", "metadata": {}, "source": [ "### Interaktywny percentyl rangi: ile danych jest poniżej $x_0$?\n", "\n", "Czasem interesuje nas pytanie odwrotne niż „jaki jest 90. percentyl?”.\n", "\n", "Wybieramy konkretną wartość $x_0$ (np. 500 ms) i pytamy: **jaki odsetek obserwacji jest nie większy niż $x_0$?** To właśnie percentyl rangi dla $x_0$.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "b6b1ff64", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " reaction_times_ms = np.asarray(rt, dtype=float)\n", " min_ms = int(np.floor(reaction_times_ms.min()))\n", " max_ms = int(np.ceil(reaction_times_ms.max()))\n", "\n", " default_x0 = 500\n", " if default_x0 < min_ms:\n", " default_x0 = min_ms\n", " if default_x0 > max_ms:\n", " default_x0 = max_ms\n", "\n", " x0_slider = widgets.IntSlider(\n", " value=default_x0,\n", " min=min_ms,\n", " max=max_ms,\n", " step=10,\n", " description='x₀ [ms]:',\n", " continuous_update=False,\n", " )\n", "\n", " rank_out = widgets.Output()\n", "\n", " def draw_rank_percentyl(_=None):\n", " x0 = int(x0_slider.value)\n", " percentyl_rangi = float(stats.percentileofscore(reaction_times_ms, x0, kind='weak'))\n", "\n", " with rank_out:\n", " rank_out.clear_output(wait=True)\n", "\n", " fig, ax = plt.subplots(figsize=(10, 4))\n", " ax.hist(\n", " reaction_times_ms,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " ax.axvline(x0, color='C1', linewidth=2, label=f'x₀ = {x0} ms')\n", "\n", " ax.set_title('Percentyl rangi dla wybranej wartości x₀')\n", " ax.set_xlabel('Czas reakcji [ms]')\n", " ax.set_ylabel('Gęstość')\n", " ax.legend()\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " print(\n", " f'Percentyl rangi dla {x0} ms: {percentyl_rangi:.1f}% '\n", " f'(odsetek obserwacji ≤ {x0} ms).'\n", " )\n", "\n", " x0_slider.observe(draw_rank_percentyl, names='value')\n", "\n", " display(x0_slider, rank_out)\n", " draw_rank_percentyl()\n" ] }, { "cell_type": "markdown", "id": "01000040", "metadata": {}, "source": [ "## Transformacje liniowe i standaryzacja\n", "\n", "Często zmieniamy skalę pomiaru (np. ms → s) albo dodajemy/odejmujemy stałą. To są przykłady **transformacji liniowych**:\n", "\n", "$$\n", "Y = aX + b\n", "$$\n", "\n", "Wtedy zachodzą proste zależności:\n", "\n", "- $\\mathrm{E}[Y] = a\\,\\mathrm{E}[X] + b$ (czyli średnia przesuwa się i/lub skaluje),\n", "- $\\mathrm{SD}(Y) = |a|\\,\\mathrm{SD}(X)$ (dodanie stałej nie zmienia SD).\n", "\n", "Szczególnym przypadkiem jest **standaryzacja**, gdzie odejmujemy średnią i dzielimy przez odchylenie standardowe. Otrzymany wynik standaryzowany $z$ jest bezjednostkowy i pozwala porównywać wyniki na różnych skalach.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000041", "metadata": {}, "outputs": [], "source": [ "rt_ms = rt\n", "rt_s = rt_ms / 1000\n", "rt_ms_plus_20 = rt_ms + 20\n", "\n", "table = pd.DataFrame(\n", " {\n", " 'wariant': ['ms', 's (ms/1000)', 'ms + 20'],\n", " 'średnia': [float(rt_ms.mean()), float(rt_s.mean()), float(rt_ms_plus_20.mean())],\n", " 'odchylenie standardowe (ddof=1)': [\n", " float(rt_ms.std(ddof=1)),\n", " float(rt_s.std(ddof=1)),\n", " float(rt_ms_plus_20.std(ddof=1)),\n", " ],\n", " }\n", ")\n", "table\n" ] }, { "cell_type": "markdown", "id": "37bd4f48", "metadata": {}, "source": [ "### Interaktywna transformacja liniowa: $Y = aX + b$\n", "\n", "Poniższy widget pozwala zmieniać współczynniki $a$ (skala) i $b$ (przesunięcie) w transformacji liniowej:\n", "\n", "$$\n", "Y = aX + b\n", "$$\n", "\n", "Zwróć uwagę:\n", "\n", "- dodanie stałej $b$ przesuwa rozkład i średnią, ale nie zmienia odchylenia standardowego,\n", "- dla dodatniego $a$ odchylenie standardowe mnoży się przez $a$ (ogólnie: $\\mathrm{SD}(Y)=|a|\\,\\mathrm{SD}(X)$).\n" ] }, { "cell_type": "code", "execution_count": null, "id": "d18e2787", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "if os.environ.get('JPY_SESSION_NAME') is None:\n", " print('Ta sekcja korzysta z widgetów Jupyter i wymaga uruchomienia w notatniku. W trybie wsadowym (np. nbconvert) jest pomijana.')\n", "else:\n", " import ipywidgets as widgets\n", " from IPython.display import display\n", "\n", " a_slider = widgets.FloatSlider(\n", " value=1.0,\n", " min=0.1,\n", " max=3.0,\n", " step=0.1,\n", " description='a (skala):',\n", " readout_format='.1f',\n", " continuous_update=False,\n", " )\n", "\n", " b_slider = widgets.IntSlider(\n", " value=0,\n", " min=-300,\n", " max=300,\n", " step=10,\n", " description='b [ms]:',\n", " continuous_update=False,\n", " )\n", "\n", " transform_out = widgets.Output()\n", "\n", " def draw_transform(_=None):\n", " a = float(a_slider.value)\n", " b = float(b_slider.value)\n", "\n", " x_ms = np.asarray(rt, dtype=float)\n", " y_ms = a * x_ms + b\n", "\n", " mean_x = float(x_ms.mean())\n", " sd_x = float(x_ms.std(ddof=1))\n", "\n", " mean_y_emp = float(y_ms.mean())\n", " sd_y_emp = float(y_ms.std(ddof=1))\n", "\n", " mean_y_formula = a * mean_x + b\n", " sd_y_formula = abs(a) * sd_x\n", "\n", " summary = pd.DataFrame(\n", " {\n", " 'miara': ['średnia', 'odchylenie standardowe (ddof=1)'],\n", " 'X (oryginalne)': [mean_x, sd_x],\n", " 'Y (z danych)': [mean_y_emp, sd_y_emp],\n", " 'Y (z wzoru)': [mean_y_formula, sd_y_formula],\n", " }\n", " )\n", "\n", " with transform_out:\n", " transform_out.clear_output(wait=True)\n", "\n", " fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", "\n", " axes[0].hist(\n", " x_ms,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " axes[0].axvline(mean_x, color='C3', linewidth=2, label='średnia')\n", " axes[0].set_title('X: dane wyjściowe')\n", " axes[0].set_xlabel('Czas reakcji [ms]')\n", " axes[0].set_ylabel('Gęstość')\n", " axes[0].legend()\n", "\n", " axes[1].hist(\n", " y_ms,\n", " bins=20,\n", " density=True,\n", " color='lightgrey',\n", " edgecolor='white',\n", " )\n", " axes[1].axvline(mean_y_emp, color='C3', linewidth=2, label='średnia')\n", " axes[1].set_title(f'Y = {a:.1f}·X + {b:.0f}')\n", " axes[1].set_xlabel('Wartość po transformacji [ms]')\n", " axes[1].set_ylabel('Gęstość')\n", " axes[1].legend()\n", "\n", " fig.tight_layout()\n", " plt.show()\n", "\n", " display(summary)\n", "\n", " for control in [a_slider, b_slider]:\n", " control.observe(draw_transform, names='value')\n", "\n", " display(widgets.HBox([a_slider, b_slider]), transform_out)\n", " draw_transform()\n" ] }, { "cell_type": "code", "execution_count": null, "id": "01000042", "metadata": {}, "outputs": [], "source": [ "z_ms = (rt_ms - rt_ms.mean()) / rt_ms.std(ddof=0)\n", "z_plus_20 = (rt_ms_plus_20 - rt_ms_plus_20.mean()) / rt_ms_plus_20.std(ddof=0)\n", "z_s = (rt_s - rt_s.mean()) / rt_s.std(ddof=0)\n", "\n", "max_diff_plus_20 = float(np.max(np.abs(z_ms - z_plus_20)))\n", "max_diff_s = float(np.max(np.abs(z_ms - z_s)))\n", "\n", "print('Maks. różnica wyniku z (ms i ms+20):', max_diff_plus_20)\n", "print('Maks. różnica wyniku z (ms i s):', max_diff_s)\n" ] }, { "cell_type": "markdown", "id": "01000043", "metadata": {}, "source": [ "## Ćwiczenia\n", "\n", "1) Opis rozkładu (czasy reakcji w zadaniu Sternberga):\n", "\n", "- Na podstawie histogramu i wykres pudełkowyu opisz rozkład `reaction_time_ms` w 4 krokach: centrum, rozproszenie, kształt, odstające.\n", "\n", "2) Histogram (wpływ wyboru przedziałów):\n", "\n", "- Zmień `bin_width` (np. 20 i 50) i porównaj, jak zmienia się wrażenie „kształtu” rozkładu.\n", "\n", "3) Centrum:\n", "\n", "- Policz średnią, medianę i średnią obciętą (np. 20%) dla `reaction_time_ms`.\n", "- Która miara jest najbardziej odporna na obserwacje odstające? Uzasadnij krótko.\n", "\n", "4) Zmienność:\n", "\n", "- Policz odchylenie standardowe i IQR.\n", "- Zinterpretuj: co oznacza większe odchylenie standardowe w kontekście czasu reakcji?\n", "\n", "5) Percentyle:\n", "\n", "- Znajdź 90. percentyl czasu reakcji.\n", "- Sprawdź percentyl rangi dla 450 ms.\n", "\n", "6) Transformacje:\n", "\n", "- Zweryfikuj na danych, że po dodaniu stałej (np. `+ 20`) odchylenie standardowe się nie zmienia.\n", "\n", "7) Porównanie dwóch warunków (efekt Stroopa, opisowo):\n", "\n", "- Na podstawie wykresu pudełkowego opisz różnice między warunkiem `zgodny` i `niezgodny` w `stroop_df`.\n", "- Uzupełnij opis o dwie liczby: medianę i IQR w każdym warunku.\n", "\n", "8) Dwie strategie (kształt rozkładu):\n", "\n", "- Obejrzyj histogram `strategie_df` dla różnych liczb przedziałów (np. 15, 30, 45).\n", "- Czy rozkład wygląda na jedno- czy dwumodalny? Jak zmienia się wrażenie, gdy zmieniasz ustawienia histogramu?\n" ] }, { "cell_type": "markdown", "id": "01000044", "metadata": {}, "source": [ "## Podsumowanie\n", "\n", "- Statystyka opisowa porządkuje dane i pomaga je interpretować (np. na danych z zadania Sternberga i efektu Stroopa).\n", "- Zaczynamy od wykresu (tabela częstości, histogram, wykres punktowy), a miary liczbowe traktujemy jako uzupełnienie.\n", "- Rozkład opisujemy przez: centrum, rozproszenie, kształt i obserwacje odstające.\n", "- Średnia jest wrażliwa na odstające; mediana i miary oparte na kwartylach są bardziej odporne.\n", "- Wariancja ma jednostkę \"do kwadratu\", a odchylenie standardowe tę samą co dane — dlatego SD jest łatwiejsze do interpretacji.\n", "- Transformacje liniowe zmieniają średnią i SD w przewidywalny sposób; wynik standaryzowany $z$ pozwala porównywać wyniki na różnych skalach.\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 }