Sama wartość p to za mało, żeby podjąć decyzję
P value jako sygnał, nie wyrok
P value jest tylko jednym z sygnałów w analizie statystycznej. W Pythonie wygląda niewinnie: funkcja z scipy.stats zwraca obiekt z polami statistic i pvalue, a większość osób od razu zerka na tę drugą liczbę. Jeżeli jest mniejsza niż 0,05, pojawia się klasyczne hasło „różnica jest istotna statystycznie”. To minimum, ale nie pełna interpretacja.
P value mówi, jak „dziwny” byłby zaobserwowany wynik, jeśli hipoteza zerowa byłaby prawdziwa i jeśli wszystkie założenia testu są spełnione. Nie mówi:
- jak duża jest różnica między grupami,
- czy ta różnica ma znaczenie praktyczne lub biznesowe,
- czy model lub test były dobrze dobrane,
- czy dane nie są skażone błędami pomiaru lub selekcji.
Jeśli odczyt ogranicza się do „p < 0,05, więc coś tam działa”, to nie jest analiza, ale automatyczna reakcja. Dla osoby patrzącej na raport taki komunikat powinien być sygnałem ostrzegawczym, że zabrakło szerszego kontekstu.
Istotność statystyczna vs istotność biznesowa
W dużych próbach nawet mikroskopijna różnica potrafi dać bardzo małe p value. Przykładowo, przy teście A/B dla aplikacji SaaS można znaleźć różnicę w konwersji rzędu ułamków procenta. Tester uruchamia w Pythonie:
from scipy import stats
stat, p = stats.ttest_ind(group_a, group_b, equal_var=False)
print(stat, p)
Pojawia się p = 0.01 i natychmiastowa interpretacja: „wariant B wygrywa”. Tymczasem:
- różnica w konwersji może być marginalna (np. 0,2 pp),
- koszt wdrożenia nowego wariantu może przewyższać oczekiwany zysk,
- grupa badana może nie reprezentować kluczowych klientów.
Na poziomie interpretacji p value trzeba zawsze przykleić do liczby opis: „jakiego rzędu jest różnica?”, „jak przekłada się na KPI?”, „jaki jest koszt błędnej decyzji?”. Jeśli raport kończy się jedną frazą „różnica jest istotna statystycznie”, a milczy o wielkości efektu, to sygnał ostrzegawczy, że analityk myli wynik testu z decyzją biznesową.
Gdzie p value prowadzi na manowce – krótki przykład
Wyobraźmy sobie analizę satysfakcji klientów dla dwóch wersji obsługi posprzedażowej. Zespół badawczy używa skali 1–10, zbiera tysiące odpowiedzi i uruchamia w Pythonie ttest_ind. Wynik:
- średnia dla wariantu A: 8,1,
- średnia dla wariantu B: 8,2,
- p value: 0,03.
Formalnie różnica jest „istotna statystycznie”. Praktycznie:
- 0,1 punktu na 10-stopniowej skali to różnica ledwo zauważalna dla człowieka,
- nie wiadomo, czy zmiana jest stabilna w czasie,
- brakuje analizy segmentów (np. nowi vs lojalni klienci).
Decyzja o zmianie procesu obsługi na bazie samego p value to ryzyko przepalenia zasobów. Punkt kontrolny: przed akceptacją wyniku trzeba zadać pytanie „gdyby różnica wynosiła dokładnie tyle, ile obserwujemy, czy decyzja byłaby taka sama, nawet gdyby p value było trochę wyższe?”. Jeśli odpowiedź brzmi „nie”, p value pełni rolę dekoracji, a nie argumentu.
Kontekst badawczy i koszt błędu jako filtr
Interpretacja p value powinna być filtrowana przez:
- pytanie badawcze – czego tak naprawdę dotyczy test,
- koszt błędu – co się stanie, jeśli wynik będzie fałszywie pozytywny lub fałszywie negatywny,
- dostępne dane – jak duża jest próba, jak zbierane są dane,
- konsekwencje decyzji – czy to eksperyment niskiego ryzyka, czy decyzja strategiczna.
Inaczej traktuje się p value w testach klinicznych, inaczej w szybkim eksperymencie marketingowym. W kontekście biznesowym często bardziej opłaca się dopuścić wyższe p value, ale skupić się na spodziewanej wartości zysku i niepewności (przedziały ufności), niż kurczowo trzymać się granicy 0,05.
Jeżeli prezentacja wyników sprowadza się do zera-jedynkowego „p < 0,05 – działamy, p ≥ 0,05 – odrzucamy”, to punkt kontrolny mówi jasno: interpretacja jest niekompletna i wymaga audytu.
Minimalny zestaw pojęć, bez których interpretacja się rozsypuje
Hipoteza zerowa i alternatywna w praktyce
Hipoteza zerowa (H0) to formalne stwierdzenie „brak efektu” lub „brak różnicy”, które test próbuję obalić. Hipoteza alternatywna (H1) opisuje to, co chcemy uprawdopodobnić. Bez jawnego zdefiniowania H0 i H1 test statystyczny w Pythonie jest czarną skrzynką.
Przykład: test t dwóch grup (scipy.stats.ttest_ind):
- H0: średnia w grupie A = średnia w grupie B,
- H1: średnia w grupie A ≠ średnia w grupie B (lub jednostronnie: > lub <).
Przykład: test chi-kwadrat niezależności (scipy.stats.chi2_contingency):
- H0: zmienne są niezależne (brak związku),
- H1: zmienne są zależne (jest związek).
Punkt kontrolny audytora: zanim pojawi się p value, powinien istnieć zapis w kodzie lub notatkach, co jest H0 i H1 słownie. Jeśli osoba analizująca dane nie potrafi w dwóch zdaniach odpowiedzieć „co jest hipotezą zerową?”, każda interpretacja p value staje się zgadywaniem.
Błąd pierwszego i drugiego rodzaju – gdzie tu p value
Błąd pierwszego rodzaju (typ I) to odrzucenie prawdziwej H0 – fałszywy alarm. Błąd drugiego rodzaju (typ II) to nieodrzucenie fałszywej H0 – przeoczenie istniejącego efektu. Poziom istotności alpha (np. 0,05) to maksymalne akceptowane ryzyko błędu I rodzaju.
P value to obserwowany poziom zgodności danych z H0. Jeśli p < alpha, decydujemy się na odrzucenie H0 i akceptujemy ryzyko, że popełniamy błąd I rodzaju z prawdopodobieństwem do 5% w długiej serii powtórzeń. Gdy p ≥ alpha, nie odrzucamy H0, ale:
- to nie oznacza, że H0 jest prawdziwa,
- to znaczy jedynie, że dane nie dają wystarczającego powodu, by ją obalić przy zadanym alpha.
Jeżeli w raporcie sformułowanie „brak istotnej różnicy” jest traktowane jako „udowodniony brak efektu”, to sygnał ostrzegawczy – autor myli błąd II rodzaju z potwierdzeniem hipotezy zerowej.
Alpha i p value – dwa różne pojęcia
W kodzie Pythona często poziom istotności pojawia się tylko w opisie, nie w samej funkcji. Typowy fragment dla testu t:
from scipy import stats
alpha = 0.05
stat, p = stats.ttest_ind(group_a, group_b, equal_var=False)
if p < alpha:
print("Odrzucamy H0")
else:
print("Brak podstaw do odrzucenia H0")
Alpha jest ustalone z góry, przed spojrzeniem na dane, a p value jest obliczone na podstawie próby. To dwa różne byty:
- alpha – założony z góry próg tolerancji na błąd I rodzaju,
- p value – wynik konkretnego testu w konkretnej próbie.
Kiedy próg alpha dostosowywany jest „po fakcie” tak, żeby dopasować się do p value („0,07 to może ustawmy alpha na 0,1”), mamy klasyczny p-hacking. Punkt kontrolny: alfa musi być ustalona przed uruchomieniem testu i nie wolno jej przesuwać na podstawie uzyskanego p.
Moc testu, wielkość efektu i przedziały ufności jako filtry bezpieczeństwa
Sama relacja p vs alpha to zbyt mało, aby oprzeć na niej decyzje. Trzy dodatkowe składniki to:
- moc testu (power) – prawdopodobieństwo wykrycia efektu, gdy naprawdę istnieje,
- wielkość efektu (effect size) – miara siły różnicy lub związku (np. Cohen’s d, r, Cramér’s V),
- przedziały ufności – zakres wartości zgodnych z danymi przy zadanym poziomie ufności (najczęściej 95%).
W Pythonie można korzystać z statsmodels, aby uzyskać przedziały ufności i wielkości efektu. Przykładowo przy modelach liniowych:
import statsmodels.api as sm
X = sm.add_constant(df[["feature"]])
y = df["target"]
model = sm.OLS(y, X).fit()
print(model.summary())
Podsumowanie modelu zawiera:
- współczynniki,
- wartości p,
- przedziały ufności dla parametrów.
Jeśli p value jest minimalnie poniżej 0,05, ale przedział ufności dla parametru zawiera wiele bardzo małych wartości bliskich zera, efekt może być statystycznie istotny, lecz praktycznie nieistotny. Punkt kontrolny: przy każdej interpretacji testu równolegle sprawdź co najmniej jedną miarę wielkości efektu oraz przedziały ufności.
Jeżeli w całej analizie ani razu nie pada słowo „moc testu” czy „przedział ufności”, interpretacja p value jest ryzykowna – brakuje kluczowych zabezpieczeń przed błędnymi decyzjami.

Środowisko pracy w Pythonie – narzędzia do czytania testów
Podstawowy zestaw bibliotek do analizy statystycznej
Do sensownego czytania wyników testów w Pythonie przydaje się stabilny zestaw narzędzi:
- pandas – struktury danych (DataFrame), filtrowanie, grupowanie, agregacje,
- numpy – operacje numeryczne, wektory, losowanie,
- scipy.stats – testy statystyczne (t-test, chi-kwadrat, testy normalności, korelacje),
- statsmodels – modele statystyczne, rozszerzone podsumowania, przedziały ufności,
- seaborn – wygodna wizualizacja, rozkłady, boxploty, wykresy zależności,
- matplotlib – niskopoziomowe wykresy, pełna kontrola nad figurami.
Minimalne środowisko pracy pozwala nie tylko uruchomić test, ale też:
- sprawdzić założenia testu wizualnie (rozkład, odstające obserwacje),
- wygodnie raportować wyniki w notatniku Jupyter,
- osadzić p value w kontekście innych miar i wykresów.
Punkt kontrolny: jeżeli workflow ogranicza się do „pandas do wczytania, scipy.stats do testu i koniec”, połowa potencjału Pythona zostaje niewykorzystana. Brakuje wtedy warstwy diagnostycznej i raportowej.
Typowy przepływ pracy: od danych do wyniku testu
Realistyczny przepływ (workflow) dla testów statystycznych wygląda w Pythonie mniej więcej tak:
- Wczytanie danych – pandas.read_csv, read_sql lub inne źródło.
- Czyszczenie i przygotowanie – usuwanie braków, filtrowanie, dobór próby.
- Eksploracja – podstawowe statystyki opisowe, histogramy, boxploty.
- Definicja pytania i hipotez – opis H0 i H1, wybór zmiennych.
- Wybór testu – na podstawie skali pomiaru, rozkładu danych, liczebności.
- Sprawdzenie założeń – testy normalności, równość wariancji, niezależność.
- Uruchomienie testu – odpowiednia funkcja z scipy.stats lub statsmodels.
- Interpretacja wyników – statistic, p value, effect size, CI, kontekst.
- Dokumentacja – zapis w notatniku Jupyter: kod + komentarze tekstowe.
Taki przepływ jest audytowalny: każdy krok można obejrzeć, powtórzyć i zweryfikować. Jeżeli w projekcie dane wskakują od razu do funkcji testującej, a sekcje eksploracji i założeń są pominięte, interpretacja p value stoi na bardzo kruchych podstawach.
scipy.stats – co dokładnie zwraca funkcja testująca
Jak odczytywać obiekty wynikowe z scipy.stats
Funkcje testujące w scipy.stats najczęściej zwracają obiekt zawierający co najmniej dwie informacje: statystykę testową i p value. W nowszych wersjach biblioteki zamiast krotki coraz częściej zwracany jest obiekt typu namedtuple lub dedykowana klasa z nazwanymi polami. To pierwszy filtr: czy osoba interpretująca wynik w ogóle zagląda poza p?
Przykład dla testu t dwóch prób niezależnych:
from scipy import stats
res = stats.ttest_ind(group_a, group_b, equal_var=False)
print(res)
print(res.statistic)
print(res.pvalue)
Wynik przypomina:
TtestResult(statistic=-2.13, pvalue=0.036, df=...)
Oprócz p value istotne są:
- statistic – jak bardzo wynik odbiega od wartości oczekiwanej przy H0,
- df (stopnie swobody) – informacja o liczebności próby i strukturze modelu.
Punkt kontrolny: w kodzie lub raporcie powinno być jasno: jaki to test, jaka statystyka, ile było stopni swobody. Samo „p = 0,03” bez nazwy testu i kontekstu to sygnał ostrzegawczy – taki wynik jest praktycznie nieaudytowalny.
Testy z dodatkowymi polami – przykład chi-kwadrat
Niektóre funkcje zwracają więcej niż dwie liczby. Test chi-kwadrat niezależności:
import numpy as np
from scipy import stats
table = np.array([[...], [...]])
chi2, p, dof, expected = stats.chi2_contingency(table)
Tutaj:
- chi2 – statystyka testu,
- p – p value,
- dof – stopnie swobody,
- expected – oczekiwane liczności przy założeniu H0 (braku związku).
Macierz expected to kopalnia informacji diagnostycznej – pozwala sprawdzić, czy nie łamiemy założeń (np. zbyt małe oczekiwane liczności w komórkach).
Punkt kontrolny: jeśli w interpretacji testu chi-kwadrat nikt nie patrzy na macierz liczności oczekiwanych, a jedynie cytuje „p < 0,05”, to brakuje kluczowej warstwy kontroli jakości. Test może być formalnie uruchomiony, ale praktycznie niewiarygodny.
Eksploracja wyników w statsmodels – więcej niż p value
Modele w statsmodels zwracają bogate obiekty z metodami pozwalającymi na rozbicie wyniku na czynniki pierwsze. Przykład prostego modelu liniowego:
import statsmodels.api as sm
X = sm.add_constant(df["feature"])
y = df["target"]
model = sm.OLS(y, X).fit()
summary = model.summary()
print(summary)
Kluczowe bloki w podsumowaniu:
- coef – oszacowane parametry (wielkość efektu na skali modelu),
- std err – błędy standardowe,
- t lub z – statystyki testowe,
- P>|t| – p value dla każdego parametru,
- [0.025 0.975] – przedziały ufności parametrów,
- metryki ogólne (R², AIC, BIC) – kontekst dopasowania modelu.
Minimum dla interpretacji pojedynczego współczynnika:
- czy znak współczynnika jest zgodny z intuicją i wiedzą domenową,
- czy wielkość współczynnika ma znaczenie praktyczne (nie tylko „jest ≠ 0”),
- czy przedział ufności jest wąski na skali problemu,
- czy obserwacja nie jest oparta na kilku skrajnych punktach (diagnostyka reszt).
Jeśli w omawianiu wyników modelu regresji pojawia się tylko kolumna „P>|t|” i słowo „istotny/nieistotny”, a współczynniki, przedziały ufności i diagnostyka reszt są pominięte, to sygnał ostrzegawczy: p value z modelu traktowane jest jak samodzielny werdykt, bez kontroli jakości modelu.
Jak czytać wynik testu krok po kroku – szkielet analizy
Krok 1: sprawdź, co i na czym zostało przetestowane
Pierwsze pytanie audytora: jaka jest jednostka analizy i jak wyglądała próba? W praktyce chodzi o kilka prostych, ale krytycznych ustaleń:
- czy dane pochodzą z jednej populacji czy z kilku różnych źródeł,
- czy obserwacje są niezależne (brak wielokrotnego zliczania tego samego obiektu),
- czy nie doszło do selektywnego dobierania prób (np. usuwania „niewygodnych” przypadków),
- jakie zmienne weszły do testu i w jakiej postaci (surowe, przekształcone, kategoryzowane).
W Pythonie te informacje powinny być widoczne w kodzie filtrującym dane:
df_clean = (
df
.query("age >= 18")
.drop_duplicates(subset="user_id")
.dropna(subset=["group", "metric"])
)
Punkt kontrolny: jeśli kod przygotowania danych jest nieobecny lub nieczytelny, a opis próby istnieje tylko w prezentacji slajdowej, interpretacja p value jest obarczona trudnym do oszacowania błędem selekcji.
Krok 2: odczytaj pełną definicję hipotez i kierunku testu
Przed spojrzeniem na jakąkolwiek liczbę należy doprecyzować: czy test był jednostronny czy dwustronny, co dokładnie oznacza „większa/niższa konwersja”, „większa/niższa średnia”. W Pythonie:
from scipy import stats
# Przykład: test jednostronny - czy A ma większą średnią niż B?
stat, p_two_sided = stats.ttest_ind(group_a, group_b, equal_var=False)
p_one_sided = p_two_sided / 2 # przy odpowiednim kierunku statystyki
Jeżeli raport mówi „p = 0,02” bez informacji, czy to wynik testu jednostronnego czy dwustronnego, nie wiadomo, jaki był prawdziwy poziom ochrony przed błędem I rodzaju.
Punkt kontrolny: w notatniku lub raporcie musi być jasno zapisany kierunek testu i znaczenie obu hipotez. Brak tej informacji to sygnał ostrzegawczy: nie da się ocenić, czy zastosowany test odpowiada na zadane pytanie.
Krok 3: zidentyfikuj statystykę testową i stopnie swobody
Statystyka testowa mówi, jak „daleko” od H0 znalazł się wynik, po przeskalowaniu przez losową zmienność. Bez niej trudno porównać wyniki między analizami. Przykład:
res = stats.ttest_ind(group_a, group_b, equal_var=False)
print(f"t = {res.statistic:.2f}, df = {res.df:.1f}, p = {res.pvalue:.3f}")
Ta sama wartość p może odpowiadać zupełnie innym wartościom statystyki i stopni swobody w zależności od wielkości próby. Przy bardzo dużych próbach mikroskopijne odchylenia mogą dawać ogromne wartości t i minimalne p.
Punkt kontrolny: jeżeli wyniki testów prezentowane są bez statystyk i stopni swobody, nie da się porównać siły dowodu między badaniami ani ocenić, czy p value wynika z dużej próby, czy z dużej różnicy.
Krok 4: odczytaj wielkość efektu i jego przedział ufności
P value nie mówi nic o tym, jak duża jest różnica. Dlatego po odczytaniu statystyki i p warto od razu wyliczyć wielkość efektu i przedział ufności. Przykład dla średniej różnicy między dwiema grupami:
import numpy as np
from scipy import stats
mean_a, mean_b = np.mean(group_a), np.mean(group_b)
diff = mean_a - mean_b
# Przybliżony 95% CI różnicy średnich (dla uproszczenia)
res = stats.ttest_ind(group_a, group_b, equal_var=False)
se_diff = diff / res.statistic
ci_low = diff - 1.96 * se_diff
ci_high = diff + 1.96 * se_diff
print(f"Różnica: {diff:.3f} (95% CI: {ci_low:.3f}, {ci_high:.3f})")
Dopiero połączenie tych informacji pozwala powiedzieć, czy efekt jest:
- statystycznie istotny (p < alpha),
- stabilny (wąski przedział ufności),
- praktycznie istotny (wielkość efektu przekracza próg znaczenia biznesowego/klinicznego).
Punkt kontrolny: jeśli wszystkie decyzje podejmowane są bez jawnego progu „efekt użyteczny w praktyce”, a jedynym kryterium jest przekroczenie 0,05, p value zaczyna pełnić rolę arbitra biznesowego zamiast statystycznego – to poważny błąd projektowy.
Krok 5: uwzględnij liczbę testów i ryzyko kumulacji błędu
Pojedynczy test z alpha = 0,05 daje 5% ryzyka błędu I rodzaju. Jeśli jednak wykonywanych jest kilkanaście lub kilkadziesiąt testów, łączne ryzyko „znalezienia czegoś przypadkiem” rośnie. W Pythonie korekty można wprowadzić np. przez statsmodels:
from statsmodels.stats.multitest import multipletests
pvals = [...] # lista p value z wielu testów
rej, p_adj, _, _ = multipletests(pvals, alpha=0.05, method="fdr_bh")
Korekta FDR (Benjamini–Hochberg) lub bardziej konserwatywna Bonferroniego to element higieny statystycznej przy analizach wielowymiarowych.
Punkt kontrolny: jeżeli w projekcie z setkami zmiennych raportowane są „istotne wyniki” bez jakiejkolwiek korekty na wielokrotne testowanie, a p value oscylują blisko 0,05, to sygnał ostrzegawczy: wysokie ryzyko fałszywych alarmów.
Krok 6: powiąż wynik testu z kontekstem decyzyjnym
Test statystyczny nie odpowiada na pytanie „czy warto wdrożyć zmianę?”. Odpowiada: „jak bardzo dane są zgodne z hipotezą zerową?”. Brakuje jeszcze dwóch elementów:
- kosztu błędu I rodzaju (wdrożenie nieefektywnej zmiany),
- kosztu błędu II rodzaju (niewdrożenie korzystnej zmiany).
W praktyce A/B testów w Pythonie wygląda to często tak:
if (p < alpha) and (diff >= minimal_detectable_effect):
decision = "Wdrożyć wariant B"
else:
decision = "Pozostawić wariant A"
Punkt kontrolny: jeżeli pipeline decyzyjny opiera się wyłącznie na „p < 0,05” bez minimalnej oczekiwanej wielkości efektu lub analizy kosztów błędów, p value jest używane niezgodnie z przeznaczeniem – jako jedyny regulator decyzji biznesowych.
Założenia testów statystycznych – co sprawdzić przed spojrzeniem na p
Niezależność obserwacji – cichy zabójca ważności testu
Wiele klasycznych testów (t-testy, ANOVA, korelacje Pearsona) zakłada niezależność obserwacji. W świecie danych produktowych i medycznych to założenie bywa łamane niemal automatycznie: ten sam użytkownik wykonuje wiele działań, ten sam pacjent ma wiele pomiarów.
Sygnały ostrzegawcze:
- kolumna
user_id,patient_idlub podobna jest ignorowana, - ta sama jednostka pojawia się wielokrotnie w tej samej analizie bez agregacji,
- powtarzane pomiary w czasie są traktowane jak niezależne w prostym t-teście.
W Pythonie minimum to:
# Przykładowa agregacja do poziomu użytkownika
user_level = (
df
.groupby("user_id")["metric"]
.mean()
.reset_index()
)
Punkt kontrolny: jeśli w danych istnieje naturalna struktura klastrowa (użytkownicy, sklepy, oddziały), a mimo to używany jest prosty test dla niezależnych obserwacji, p value jest zaniżone, a ryzyko błędu I rodzaju znacznie większe niż 5%.
Normalność rozkładu – kiedy ma znaczenie, a kiedy nie
Testy parametryczne dla średnich (t-test, ANOVA) zakładają w uproszczeniu normalność rozkładu błędów lub zmiennej w populacji. Przy dużych próbach centralne twierdzenie graniczne często łagodzi to wymaganie, ale przy małych próbach lub silnie skośnych rozkładach problem wraca.
W Pythonie zamiast ślepego uruchamiania testu normalności (np. Shapiro-Wilka) lepiej połączyć prostą wizualizację z testem:
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
sns.histplot(group_a, kde=True)
plt.show()
stat, p_norm = stats.shapiro(group_a)
print(f"Shapiro-Wilk p = {p_norm:.3f}")
Sam test normalności przy dużych próbach niemal zawsze „coś znajdzie”. Kluczowe pytanie brzmi: czy odchylenie od normalności jest na tyle duże, że podważa wiarygodność t-testu, czy wystarczy przejść na test nieparametryczny (np. mannwhitneyu).
Jednorodność wariancji – kiedy różnice w rozproszeniu wypaczają test
Wiele testów dla porównania średnich (klasyczny t-test, ANOVA) zakłada podobny poziom wariancji w porównywanych grupach. Skrajnie różne rozproszenie wyników potrafi zafałszować wnioski, szczególnie przy nierównych liczebnościach.
Podstawowa diagnostyka w Pythonie:
import numpy as np
from scipy import stats
var_a, var_b = np.var(group_a, ddof=1), np.var(group_b, ddof=1)
print(f"Var A = {var_a:.3f}, Var B = {var_b:.3f}, ratio = {max(var_a, var_b)/min(var_a, var_b):.2f}")
# Test Levene'a – mniej wrażliwy na nienormalność niż F-test
stat_levene, p_levene = stats.levene(group_a, group_b, center="median")
print(f"Levene: stat = {stat_levene:.3f}, p = {p_levene:.3f}")
Jeśli test jednorodności wariancji wskazuje istotną różnicę, lepiej użyć wersji testu odpornej na to naruszenie, np. t-testu Welcha:
# Zamiast equal_var=True (klasyczny t-test) – użyj Welcha:
stat, p = stats.ttest_ind(group_a, group_b, equal_var=False)
W raportach z A/B testów sygnałem ostrzegawczym jest używanie standardowego t-testu przy bardzo różnych wariancjach (np. grupa promocyjna z ekstremalnymi zachowaniami vs. grupa kontrolna) bez choćby wzmianki o tym, jak sprawdzono założenie jednorodności.
Punkt kontrolny: jeśli ratio wariancji przekracza 3–4, p value z t-testu z założeniem równych wariancji jest podejrzane, zwłaszcza przy małych próbach i dużej asymetrii liczebności między grupami.
Skala pomiaru i poziom agregacji – czy test pasuje do pytania
Test jest poprawny tylko wtedy, gdy odpowiada na pytanie zadane na właściwym poziomie danych. Najczęstsze zderzenie z rzeczywistością: liczona jest średnia dziennych przychodów, a hipoteza dotyczy użytkowników; albo konwersja liczona jest po sesjach, a decyzja dotyczy użytkownika.
Przykład niezgodności:
- hipoteza: „czy użytkownicy w wariancie B częściej kupują niż w A?”,
- dane w teście: „liczba transakcji dziennie na poziomie całego sklepu”.
W takim przypadku testuje się zmienność dzienną, a nie zachowanie użytkowników. W Pythonie minimum to doprowadzenie danych do poziomu jednostki decyzyjnej:
user_conv = (
df.assign(converted = df["orders"] > 0)
.groupby(["user_id", "group"], as_index=False)["converted"]
.max()
)
# Dopiero na takiej tablicy test proporcji ma sens
tab = pd.crosstab(user_conv["group"], user_conv["converted"])
print(tab)
Sygnał ostrzegawczy: hipotezy dotyczą użytkowników, lekarzy, szkół, a test opiera się na obserwacjach sesji, wizyt, transakcji bez agregacji do poziomu jednostki, o której mowa w hipotezie.
Punkt kontrolny: jeśli poziom agregacji zmiennej testowanej nie pokrywa się z jednostką z hipotezy (użytkownik, pacjent, oddział), p value opisuje inny problem niż deklarowany – nawet jeśli samo obliczenie testu jest formalnie poprawne.
Stacjonarność i zmienność w czasie – problem w eksperymentach ciągłych
W danych zbieranych w czasie (eksperymenty online, procesy produkcyjne, badania kliniczne z długim follow-upem) kluczowe jest założenie, że warunki badania są względnie stabilne, a różnice między grupami nie wynikają z trendu czasowego czy sezonowości.
Kilka prostych kontroli w Pythonie:
import seaborn as sns
import matplotlib.pyplot as plt
# Dzienne średnie metryki w każdej grupie
daily = (
df
.groupby(["date", "group"])["metric"]
.mean()
.reset_index()
)
sns.lineplot(data=daily, x="date", y="metric", hue="group")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Jeśli widać, że grupy „rozjeżdżają się” dopiero w konkretnym okresie (np. weekend, kampania marketingowa), a test korzysta z całego okresu zbierania danych, p value jest mieszanką efektu eksperymentalnego oraz interferencji z kalendarzem.
Punkt kontrolny: gdy wykresy czasowe pokazują silną sezonowość lub zmiany trendu, testy na danych zagregowanych po całym okresie mogą mieć zafałszowane p value; konieczne bywa modelowanie z efektem czasu (np. regresja z kontrolą dnia tygodnia).

P value w Pythonie – co naprawdę oznacza ta liczba
Definicja operacyjna – co dokładnie mierzy p value
P value to prawdopodobieństwo otrzymania wyniku co najmniej tak ekstremalnego jak zaobserwowany, zakładając, że hipoteza zerowa jest prawdziwa oraz że wszystkie założenia testu są spełnione. W praktyce Python implementuje to jako:
- obliczenie statystyki testowej (t, z, χ², F, U, itp.),
- oszacowanie jej położenia w teoretycznym rozkładzie przy H0,
- zsumowanie „ogonów” rozkładu zgodnie z typem testu (jedno- lub dwustronny).
Przykład: t-test dwustronny w scipy:
from scipy import stats
res = stats.ttest_ind(group_a, group_b, equal_var=False)
print(res) # statistic, pvalue, (czasem df w nowszych wersjach)
Tu p value nie mówi „prawdopodobieństwo, że H0 jest prawdziwa”, tylko „prawdopodobieństwo takiego lub bardziej ekstremalnego t przy założeniu, że w populacji różnicy nie ma”. To rozróżnienie decyduje o tym, jak bardzo można „ufać” pojedynczemu p.
Punkt kontrolny: jeśli w zespołowych dyskusjach p value jest interpretowane wprost jako „szansa, że wyniki są przypadkowe” lub „prawdopodobieństwo, że efekt jest prawdziwy”, to sygnał ostrzegawczy – świadczy o fundamentalnym nieporozumieniu co do jego znaczenia.
Jednostronne vs dwustronne p value – ukryta dźwignia interpretacji
W Pythonie większość klasycznych funkcji (np. ttest_ind, chi2_contingency) zwraca domyślnie p value dla testu dwustronnego. Przejście na test jednostronny wymaga świadomej decyzji i jawnego przekształcenia:
from scipy import stats
import numpy as np
stat, p_two = stats.ttest_ind(group_a, group_b, equal_var=False)
# Załóżmy hipotezę jednostronną: mean_a > mean_b
if stat > 0:
p_one = p_two / 2
else:
p_one = 1 - p_two / 2 # brak dowodu w kierunku zakładanym
W praktyce to „dzielenie przez dwa” bywa wykonywane bez refleksji, już po obejrzeniu wyniku dwustronnego. To klasyczny błąd – test jednostronny musi być zadeklarowany przed spojrzeniem w dane, inaczej zaniża rzeczywiste ryzyko błędu I rodzaju.
Punkt kontrolny: jeśli w repozytorium znajduje się wyłącznie wynik testu dwustronnego, a na slajdach raportowych „magicznie” pojawia się p dla hipotezy jednostronnej, oznacza to manipulowanie interpretacją p value post factum.
P value a wielkość próby – dlaczego „zbyt duże N” potrafi zepsuć wnioski
Im większa próba, tym mniejsza losowa zmienność i tym łatwiej wykryć drobne odchylenia od H0. W skrajnym przypadku przy ogromnych próbach praktycznie każda, nawet nieistotna biznesowo różnica, stanie się „istotna statystycznie”.
Symulacja w Pythonie:
import numpy as np
from scipy import stats
rng = np.random.default_rng(42)
for n in [50, 500, 5000]:
a = rng.normal(loc=0.0, scale=1.0, size=n)
b = rng.normal(loc=0.05, scale=1.0, size=n) # bardzo mała różnica średnich
stat, p = stats.ttest_ind(a, b, equal_var=False)
print(f"n={n}, diff={np.mean(a)-np.mean(b):.3f}, t={stat:.2f}, p={p:.5f}")
Ta sama różnica efektu przy rosnącym n generuje coraz mniejsze p value. Bez zdefiniowanego progu znaczenia praktycznego (np. minimalnej różnicy w konwersji, którą opłaca się ścigać), p value zaczyna sygnalizować jedynie „cokolwiek różnego od zera”.
Punkt kontrolny: jeśli raport zawiera bardzo małe p value przy mikroskopijnej różnicy metryki (np. kilka promili) i gigantycznych próbach, a mimo to wyciągane są daleko idące wnioski biznesowe, oznacza to pomieszanie istotności statystycznej z praktyczną.
P value a brak efektu – czego p value nie mówi
Częste nieporozumienie: „p > 0,05, więc dowód na brak efektu”. Tymczasem wysokie p value oznacza jedynie, że przy zadanym poziomie mocy i próby nie udało się wykryć odchylenia od H0. Nie rozstrzyga, czy efekt jest równy zero, czy tylko niewykrywalny w danym badaniu.
Krok minimalny w Pythonie to powiązanie p value z przedziałem ufności:
import numpy as np
from scipy import stats
mean_a, mean_b = np.mean(group_a), np.mean(group_b)
diff = mean_a - mean_b
res = stats.ttest_ind(group_a, group_b, equal_var=False)
se_diff = diff / res.statistic
ci_low = diff - 1.96 * se_diff
ci_high = diff + 1.96 * se_diff
print(f"p = {res.pvalue:.3f}, diff = {diff:.3f}, 95% CI = ({ci_low:.3f}, {ci_high:.3f})")
Jeżeli przedział ufności jest szeroki i obejmuje zarówno wartości „biznesowo istotne”, jak i zero, duże p value mówi głównie o braku informacji. Dopiero wąski przedział skupiony wokół zera daje realny argument za brakiem efektu o znaczeniu praktycznym.
Punkt kontrolny: jeśli wnioski typu „brak różnic między grupami” formułowane są wyłącznie na podstawie p > 0,05, bez analizy szerokości i położenia przedziału ufności, interpretacja jest zbyt optymistyczna względem mocy badania.
Obliczanie p value z modeli – statystyka to nie tylko gotowe testy
W realnych projektach dane rzadko spełniają idealne założenia prostych testów. Częściej kończy się na modelach regresyjnych, z których trzeba wydobyć współczynniki, błędy standardowe, statystyki i p value. W Pythonie naturalnym narzędziem jest statsmodels.
import statsmodels.api as sm
import statsmodels.formula.api as smf
# Model liniowy z grupą i kowariantą wieku
df_model = df_clean.copy()
model = smf.ols("metric ~ C(group) + age", data=df_model).fit()
print(model.summary())
W tabeli wyników dla każdego parametru widnieją: estymata, std err, t, p>|t| oraz przedział ufności. P value przypisane do współczynnika grupy oznacza zgodność danych z hipotezą:
- H0: brak różnicy po uwzględnieniu wieku,
- H1: różnica ≠ 0 po uwzględnieniu wieku.
W analizach regresyjnych sygnałem ostrzegawczym jest wyciąganie pojedynczego p value z jednego współczynnika w rozbudowanym modelu bez refleksji nad:
- liczbą testowanych współczynników,
- stabilnością modelu (kolinearność, przeuczenie),
- jakością dopasowania (reszty, wpływ obserwacji odstających).
Punkt kontrolny: jeśli scenariusz decyzyjny opiera się na „p < 0,05” dla jednego współczynnika z dużej regresji wielowymiarowej, ale brak analizy diagnostycznej modelu, to p value ma znacznie niższą wiarygodność niż sugeruje liczba.
Symulacje i bootstrap – alternatywne spojrzenie na p value
Przy skomplikowanych rozkładach lub naruszonych założeniach testów parametrycznych dobrym zabezpieczeniem jest symulacyjne oszacowanie rozkładu statystyki. W Pythonie stosunkowo łatwo przeprowadzić permutacyjny test różnicy średnich czy bootstrap różnicy.
Permutacyjny test p value dla różnicy średnich:
import numpy as np
rng = np.random.default_rng(42)
all_values = np.concatenate([group_a, group_b])
n_a = len(group_a)
obs_diff = np.mean(group_a) - np.mean(group_b)
n_perm = 10000
diffs = np.empty(n_perm)
for i in range(n_perm):
rng.shuffle(all_values)
a_perm = all_values[:n_a]
b_perm = all_values[n_a:]
diffs[i] = np.mean(a_perm) - np.mean(b_perm)
# Dwustronne p value
p_perm = np.mean(np.abs(diffs) >= np.abs(obs_diff))
print(f"Permutacyjne p = {p_perm:.4f}")
Takie p value nie opiera się na założeniu normalności ani równych wariancji – oszacowany jest bezpośrednio z permutowanego rozkładu. Porównanie p value z testu parametrycznego i permutacyjnego to dobry test odporności wniosków.
Punkt kontrolny: jeżeli rozkład zmiennych jest ekstremalnie skośny, liczności małe, a mimo to używany jest wyłącznie klasyczny t-test, warto przynajmniej porównać wynik z prostym testem permutacyjnym; duże rozbieżności p value to czerwone światło dla wiarygodności wniosków.
Najważniejsze punkty
- Sama wartość p jest tylko sygnałem, a nie wyrokiem – pokazuje, jak „nietypowe” są dane przy założeniu prawdziwości hipotezy zerowej i spełnionych założeń testu, ale nie mówi nic o wielkości efektu, jakości danych ani sensowności modelu. Jeśli decyzja opiera się wyłącznie na progu p < 0,05, to sygnał ostrzegawczy, że analiza została spłaszczona do jednego numerka.
- Istotność statystyczna to nie to samo co istotność biznesowa – przy bardzo dużych próbach nawet śladowa różnica (np. 0,1 punktu na skali 1–10) da „ładne” p value, choć efekt będzie praktycznie bez znaczenia. Jeśli raport milczy o wielkości efektu, wpływie na KPI i koszcie wdrożenia, punkt kontrolny mówi jasno: analityk myli wynik testu z decyzją biznesową.
- Interpretacja p value musi być osadzona w kontekście: pytanie badawcze, koszt błędu, sposób zbierania danych i konsekwencje decyzji tworzą filtr, przez który należy przepuścić wynik testu. Jeśli w eksperymencie marketingowym i w decyzji strategicznej używa się tego samego progu p bez refleksji nad ryzykiem, to mocny sygnał ostrzegawczy dla audytora.
- Bez jawnie zdefiniowanej hipotezy zerowej (H0) i alternatywnej (H1) każdy test statystyczny w Pythonie staje się czarną skrzynką – trudno zrozumieć, co właściwie „obalamy” lub „uprawdopodobniamy”. Punkt kontrolny: przed uruchomieniem testu osoba analizująca powinna w dwóch zdaniach zapisać H0 i H1; jeśli nie potrafi, interpretacja p value będzie w dużej mierze zgadywaniem.
Źródła informacji
- Statistical Methods for Research Workers. Oliver and Boyd (1925) – Klasyczne wprowadzenie do testów istotności i p-value
- The ASA Statement on p-Values: Context, Process, and Purpose. American Statistical Association (2016) – Oficjalne zalecenia ASA dotyczące interpretacji p-value
- Statistical Inference. Duxbury Press (2001) – Omówienie hipotez, błędów I i II rodzaju, mocy testu
- Practical Statistics for Data Scientists. O’Reilly Media (2020) – Praktyczne użycie testów statystycznych i p-value w analizie danych






