Kiedy w ogóle używać testu chi kwadrat i czy to się opłaca
Co tak naprawdę bada test chi kwadrat
Test chi kwadrat niezależności sprawdza, czy dwie zmienne jakościowe (kategoryczne) są ze sobą powiązane, czy można je traktować jako niezależne. Zmienna jakościowa to taka, która przyjmuje ograniczoną liczbę kategorii, np. płeć, grupa wiekowa, kanał zakupu, status klienta (aktywny/nieaktywny), wynik kampanii (konwersja tak/nie).
W praktyce test chi kwadrat odpowiada na pytanie: czy rozkład jednej zmiennej różni się istotnie pomiędzy poziomami drugiej zmiennej. Przykładowo – czy odsetek klientów kupujących produkt X jest inny w różnych grupach wiekowych, czy może jest w zasadzie taki sam (różnice to tylko losowy szum).
Cała analiza opiera się na tabeli kontyngencji (tabeli krzyżowej), czyli prostej tabeli zliczeń: w wierszach kategorie pierwszej zmiennej, w kolumnach kategorie drugiej. Test porównuje:
- liczebności obserwowane – realne zliczenia w tabeli,
- liczebności oczekiwane – takie, jakie byłyby, gdyby zmienne były całkowicie niezależne.
Im bardziej te wartości się różnią, tym mocniejsza statystyczna przesłanka, że zmienne są powiązane.
Typowe zastosowania biznesowe i analityczne
Test chi kwadrat w Pythonie szczególnie dobrze sprawdza się w powtarzalnych zadaniach, gdzie mamy dużo danych, a decyzje trzeba oprzeć na czymś więcej niż „na oko”. Kilka najczęstszych scenariuszy:
- Analiza ankiet – zależność między odpowiedziami na pytania zamknięte, np. „czy poleciłbyś produkt?” × „grupa wiekowa”, „płeć” × „sposób dowiedzenia się o marce”.
- Kampanie marketingowe – „kanał dotarcia” × „konwersja (tak/nie)”, „wariant kreacji” × „czy użytkownik kliknął”. Test chi kwadrat pozwala weryfikować, czy różnice w klikach/konwersji między kanałami są przypadkowe, czy też statystycznie istotne.
- Churn i retencja – „typ abonamentu” × „czy klient odszedł”, „segment klienta” × „odnowienie subskrypcji”.
- A/B testy dla danych kategorycznych – wariant A vs B a wynik typu „kupił/nie kupił”, „kliknął/nie kliknął”. Dla takich prostych tabel 2×2 test chi kwadrat jest jednym z podstawowych narzędzi.
- E-commerce – „typ urządzenia” × „konwersja”, „kategoria wejścia na stronę” × „rodzaj finalnego zakupu”, itp.
We wszystkich tych przypadkach test chi kwadrat w Pythonie można osadzić w prostym pipeline: wczytanie danych → zbudowanie tabeli krzyżowej w pandas → test w scipy.stats → prezentacja wyniku (np. wykres słupkowy).
Porównanie z innymi testami statystycznymi
Aby nie używać testu chi kwadrat nie tam, gdzie trzeba, przyda się krótka mapa:
| Typ danych / pytanie | Przykład | Typowy test |
|---|---|---|
| Dwie zmienne kategoryczne | płeć × konwersja (tak/nie) | test niezależności chi kwadrat |
| Średnie w dwóch grupach (zmienna ciągła) | średni koszyk w grupie A vs B | test t-Studenta |
| Korelacja dwóch zmiennych ciągłych | czas na stronie vs koszyk | korelacja Pearsona/Spearmana |
| Jedna zmienna kategoryczna vs rozkład zadany | czy rozkład odpowiedzi pasuje do 50/50? | test dopasowania chi kwadrat |
Test chi kwadrat z tabeli kontyngencji nie służy do badania średnich, median czy korelacji liczbowych. Dotyczy tylko zliczeń w kategoriach. Jeśli dane są liczbowe, najczęściej lepsze będą inne testy (t-Student, ANOVA, korelacje).
Test chi kwadrat w pipeline analizy danych w Pythonie
W typowym, pragmatycznym procesie analizy danych Python pełni rolę narzędzia do:
- ETL – wczytanie danych z CSV/Excel/bazy, proste czyszczenie (pandas).
- Eksploracja – szybkie statystyki opisowe, tabele krzyżowe (
pd.crosstab), podgląd danych. - Test statystyczny – w tym przypadku test niezależności chi kwadrat (
scipy.stats.chi2_contingency). - Wizualizacja – wykresy słupkowe, skumulowane słupki, heatmapa z seaborn.
Test chi kwadrat jest jednym elementem tego łańcucha, a nie celem samym w sobie. Kod da się łatwo zautomatyzować w notebooku Jupyter lub w prostym skrypcie, który systematycznie przegląda różne pary zmiennych kategorycznych.
Efekt vs wysiłek: kiedy to ma sens
Z punktu widzenia „efekt vs wysiłek”:
- Niski próg wejścia – jeśli i tak używasz pandas, dodanie testu chi kwadrat to raptem kilka linii kodu i instalacja SciPy (czas instalacji vs potencjalna informacja – zwykle się opłaca).
- Realna wartość dodana – w sytuacjach spornych („wygląda, że kanał X działa lepiej”) test daje obiektywny argument, który można pokazać na slajdzie.
- Kiedy wystarczy crosstab bez testu – przy bardzo małych próbkach, silnie nierównych liczebnościach lub kiedy pytanie jest wyłącznie eksploracyjne („jak to mniej więcej wygląda”), zwykła tabela kontyngencji bywa całkowicie wystarczająca.
Jeśli przy pojedynczej parze zmiennych trzeba spędzać godzinę na czyszczeniu danych, a decyzja biznesowa i tak będzie intuicyjna, ciężki test statystyczny może być zbyt dużym wysiłkiem. Natomiast w projektach, gdzie takie analizy powtarzają się co tydzień, inwestycja w nauczenie się testu chi kwadrat w Pythonie zwraca się bardzo szybko.
Podstawy testu chi kwadrat bez nadmiarowej matematyki
Intuicja: porównanie obserwacji z tym, co „powinno być”
Sedno testu niezależności chi kwadrat to sprawdzenie, na ile rozkład zliczeń w tabeli odbiega od rozkładu, który zobaczylibyśmy przy braku zależności. Jeśli zmienne są niezależne, udział danej kategorii w wierszu powinien być zbliżony do jej udziału globalnie (i odwrotnie).
Dla każdej komórki w tabeli kontyngencji wylicza się:
- O – liczebność obserwowana (realne zliczenie),
- E – liczebność oczekiwana przy założeniu niezależności.
Następnie zestawia się te dwie wartości w jednej prostej formule. Nie trzeba jej znać na pamięć, kluczowe jest zrozumienie: im większe różnice między O i E, tym większa statystyka testu, a więc mniejsze szanse, że taką tabelę wygenerował czysty przypadek.
Statystyka testowa w bardzo skróconej wersji
Statystyka testowa chi kwadrat ma postać:
chi2 = Σ (O - E)^2 / E
Suma biegnie po wszystkich komórkach tabeli. W praktyce w Pythonie funkcja chi2_contingency:
- przyjmuje tabelę zliczeń jako wejście,
- liczy liczebności oczekiwane,
- oblicza wartość
chi2, - używa rozkładu chi kwadrat, żeby obliczyć p-value.
Im większe chi2, tym bardziej obserwacje „gryzą się” z niezależnością. Ale sama wartość chi2 bez kontekstu (liczby stopni swobody) niewiele mówi, dlatego najczęściej interpretuje się p-value.
Rola rozkładu chi kwadrat i stopni swobody
Statystyka chi2 ma w przybliżeniu rozkład chi kwadrat o pewnej liczbie stopni swobody, jeśli H0 (niezależność) jest prawdziwa. Liczba stopni swobody to:
df = (liczba_wierszy - 1) * (liczba_kolumn - 1)
Dla tabeli 2×2 df = 1, dla 3×4 df = 6 itd. Rozkład chi kwadrat mówi, jak „duża” musi być wartość chi2, żeby zdarzyła się rzadko przy H0. Na tej podstawie liczone jest p-value, czyli prawdopodobieństwo otrzymania statystyki testowej co najmniej tak ekstremalnej jak ta z danych, zakładając poprawność H0.
Hipotezy badane w teście chi kwadrat
Dla testu niezależności chi kwadrat hipotezy wyglądają następująco:
- H0 (hipoteza zerowa): zmienne są niezależne. Innymi słowy, rozkład jednej zmiennej nie zależy od wartości drugiej.
- H1 (hipoteza alternatywna): istnieje zależność pomiędzy zmiennymi. Rozkład jednej zmiennej różni się w poszczególnych kategoriach drugiej.
„Odrzucić H0” oznacza: rozkład zliczeń jest na tyle inny od oczekiwanego przy niezależności, że trudno to zrzucić na losowy przypadek. „Nie odrzucić H0” oznacza: dane nie dają wystarczających podstaw, by twierdzić, że zależność istnieje.
P-value: co naprawdę mówi, a czego nie mówi
W testach chi kwadrat w Pythonie p-value jest zazwyczaj główną liczbą, na którą patrzymy. Warto jasno powiedzieć, czym nie jest:
- Nie jest „szansą, że H0 jest prawdziwa”.
- Nie mówi nic wprost o wielkości efektu (siły zależności).
P-value to prawdopodobieństwo uzyskania tak ekstremalnej statystyki jak obserwowana (lub bardziej ekstremalnej), przy założeniu, że H0 jest prawdziwa. Jeśli jest bardzo małe (np. < 0.05), wniosek brzmi: taki wynik byłby mało prawdopodobny przy niezależności, więc przyjmowanie niezależności jako założenia jest ryzykowne.
Dlatego interpretując test chi kwadrat w Pythonie, zawsze warto łączyć p-value z:
- intuicyjnym spojrzeniem na tabelę kontyngencji,
- miarą siły związku (np. V Craméra),
- wielkością próby (przy ogromnych próbach małe różnice będą niemal zawsze istotne).
Przygotowanie danych jakościowych w pandas – typy, kategorie, porządki
Wczytywanie danych z CSV/Excel z minimalnym nakładem pracy
Podstawowy koszt wejścia w analizę to często samo wczytanie i wstępne ogarnięcie danych. Najprostszy wariant dla pliku CSV:
import pandas as pd
df = pd.read_csv("dane.csv") # kodowanie domyślne
# albo, jeśli są polskie znaki i problemy z encodingiem:
# df = pd.read_csv("dane.csv", encoding="utf-8")
Dla Excela:
df = pd.read_excel("dane.xlsx", sheet_name=0)
Na początek wystarczy df.head() i df.dtypes, żeby sprawdzić, które kolumny wyglądają na kategoryczne. Nie trzeba od razu budować skomplikowanych pipeline’ów ETL – kilka prostych transformacji wystarczy, by odpalić test chi kwadrat w Pythonie.
Rozpoznawanie zmiennych jakościowych w pandas
Zmienna jakościowa w pandas może mieć typ:
- object – najczęściej łańcuch znaków (string),
- category – specjalny typ kategorii (oszczędniejszy pamięciowo),
- int/float z małą liczbą różnych wartości – np. kody 1, 2, 3 dla grup.
Szybki sposób na identyfikację kolumn z małą liczbą unikalnych wartości:
df.nunique().sort_values()Kolumny z kilkoma–kilkunastoma unikalnymi wartościami są często dobrymi kandydatami na zmienne kategoryczne nadające się do tabel kontyngencji.
Konwersja do typu category i korzyści
Dla kolumn, które mają ograniczoną liczbę kategorii, opłaca się przekonwertować je na typ category:
df["plec"] = df["plec"].astype("category")
df["kanal_zakupu"] = df["kanal_zakupu"].astype("category")Korzyści:
- mniejszy użycie pamięci – przy dużych danych ma to znaczenie, szczególnie w notebookach w chmurze z ograniczonym RAM,
Porządkowanie kategorii i łączenie rzadkich wartości
Przy danych jakościowych największym kosztem bywa sprzątanie nazw kategorii. Kilka prostych kroków robi różnicę, a nie wymaga pełnego projektu data cleaning.
Typowe szybkie poprawki:
- uściślenie pisowni (małe/duże litery, spacje):
df["kanal_zakupu"] = (
df["kanal_zakupu"]
.str.strip()
.str.lower()
.replace({
"fb": "facebook",
"fb ads": "facebook",
"facebok": "facebook"
})
.astype("category")
)- łączenie rzadkich kategorii w „inne” – pod test chi kwadrat lepiej mieć kilka sensownych kubełków niż kilkadziesiąt jednorazowych wartości:
counts = df["miasto"].value_counts()
rzadkie = counts[counts < 30].index # próg można ustawić wg potrzeb
df["miasto_skon"] = df["miasto"].where(~df["miasto"].isin(rzadkie), "inne")
df["miasto_skon"] = df["miasto_skon"].astype("category")Przy zbyt rozdrobnionych kategoriach test chi kwadrat traci moc, a macierz zliczeń robi się nieczytelna. Jednorazowe miasta albo kampanie sprzed lat częściej opłaca się wrzucić do „inne”, niż rozciągać analizę na ogon z kilkoma sztukami.
Porządek w kategoriach uporządkowanych (np. poziomy satysfakcji)
Przy zmiennych typu „bardzo niezadowolony” – „bardzo zadowolony” opłaca się wprowadzić porządek. pandas to umożliwia:
poziomy = ["bardzo niezadowolony", "niezadowolony",
"neutralny", "zadowolony", "bardzo zadowolony"]
df["satysfakcja"] = pd.Categorical(
df["satysfakcja"],
categories=poziomy,
ordered=True
)Sam test chi kwadrat nie wykorzystuje informacji o porządku, ale:
- łatwiej zbudować czytelny wykres (słupki w logicznej kolejności),
- łatwiej wizualnie ocenić trend w tabeli kontyngencji (np. im wyższa kategoria, tym częściej występuje w danej grupie).
Radzenie sobie z brakami danych i kategorią „brak danych”
Braki danych można potraktować na kilka sposobów, w zależności od pytania biznesowego i skali problemu:
- Odrzucenie wierszy z brakami w analizowanych kolumnach – najszybszy i często wystarczający wariant:
sub = df[["plec", "kanal_zakupu"]].dropna()- Traktowanie braku jako osobnej kategorii, gdy „brak informacji” jest sam w sobie informacją (np. brak odpowiedzi w ankiecie):
df["plec_filled"] = df["plec"].fillna("brak_danych").astype("category")Gdy braków jest dużo, warto choć raz porównać wyniki testu z i bez tych wierszy. Jeśli wnioski się nie zmieniają, można spokojnie wybierać prostszą ścieżkę na kolejne analizy.

Budowa tabeli kontyngencji w pandas
Prosta tabela krzyżowa z pd.crosstab
Do testu chi kwadrat potrzebna jest zwykła tabela zliczeń. Najprostsza droga:
tab = pd.crosstab(df["plec"], df["kanal_zakupu"])
print(tab)Domyślnie dostaje się czyste zliczenia. Dla eksploracji często przydają się od razu udziały procentowe:
# procent w wierszu (np. struktura kanałów w każdej płci)
tab_row = pd.crosstab(df["plec"], df["kanal_zakupu"], normalize="index")
# procent w kolumnie (np. struktura płci w każdym kanale)
tab_col = pd.crosstab(df["plec"], df["kanal_zakupu"], normalize="columns")Do samego testu podaje się wersję bez normalizacji, czyli czyste zliczenia. Udziały procentowe służą wyłącznie do interpretacji.
Ograniczanie się do najczęstszych kategorii
Przy dużej liczbie kategorii w jednej ze zmiennych (np. setki miast) lepiej od razu ściąć ogon:
top_miasta = df["miasto"].value_counts().head(10).index
df_top = df[df["miasto"].isin(top_miasta)]
tab_top = pd.crosstab(df_top["miasto"], df_top["kanal_zakupu"])Taka selekcja mocno obniża koszt obliczeń i zwiększa czytelność tabeli. Zamiast wyświetlać wielką macierz, operator czy menedżer zobaczy paręnaście komórek do omówienia.
Filtrowanie próby przed stworzeniem tabeli
Zanim przejdzie się do testu chi kwadrat, często rozsądnie jest zawęzić dane:
- do konkretnego okresu (np. ostatni kwartał zamiast trzech lat),
- do konkretnych segmentów (np. nowi klienci, jeden kraj),
- do obserwacji po czyszczeniu (wykluczone testowe zamówienia, pracownicy itd.).
mask = (
(df["data"] >= "2023-01-01") &
(df["data"] <= "2023-12-31") &
(df["typ_klienta"] == "nowy")
)
df_sub = df.loc[mask]
tab = pd.crosstab(df_sub["plec"], df_sub["kanal_zakupu"])Filtr warto ustalić raz i potem konsekwentnie wykorzystywać przy wszystkich parach zmiennych. Chroni to przed sytuacją, w której każda tabela i każdy test liczone są na innym wycinku danych.
Test chi kwadrat w Pythonie krok po kroku
Wywołanie scipy.stats.chi2_contingency
Podstawowe użycie funkcji jest proste i nie wymaga znajomości rozkładów:
from scipy.stats import chi2_contingency
tab = pd.crosstab(df["plec"], df["kanal_zakupu"])
chi2, p, dof, expected = chi2_contingency(tab)
print("chi2:", chi2)
print("p-value:", p)
print("stopnie swobody:", dof)
print("oczekiwane liczebności:")
print(expected)Wyniki:
chi2– wartość statystyki testowej,p– p-value do porównania z poziomem istotności (np. 0.05),dof– stopnie swobody,expected– tablica liczebności oczekiwanych przy niezależności.
Do raportu menedżerskiego zwykle trafia samo p-value i ewentualnie krótkie stwierdzenie „istotna/nieistotna zależność”. Dla siebie opłaca się jednak zawsze obejrzeć także expected – od razu widać, która komórka ciągnie różnicę.
Minimalna funkcja pomocnicza do wielokrotnych testów
Jeśli takich testów ma być dużo, prosty helper w jednym module oszczędzi sporo powtarzanego kodu:
from scipy.stats import chi2_contingency
import pandas as pd
def chi2_for_cols(df, col_x, col_y, min_count=5):
tab = pd.crosstab(df[col_x], df[col_y])
if (tab.values < min_count).any():
print(f"Uwaga: w tabeli dla {col_x} vs {col_y} są komórki < {min_count}")
chi2, p, dof, expected = chi2_contingency(tab)
return {
"x": col_x,
"y": col_y,
"chi2": chi2,
"p": p,
"dof": dof,
"n": tab.values.sum()
}Później wystarczy jedna pętla po parach kolumn i od razu powstaje mini-raport w formie DataFrame.
Automatyczne przeglądanie wielu par zmiennych kategorycznych
W typowym projekcie marketingowym czy produktowym takich par jest kilkanaście–kilkadziesiąt. Ręczne klikanie łączyń typu „kanał × segment” jest stratą czasu. Lepiej zdefiniować listę kolumn i wygenerować wyniki jednym strzałem:
cat_cols = ["plec", "kanal_zakupu", "kraj", "segment", "typ_klienta"]
results = []
for i, col_x in enumerate(cat_cols):
for col_y in cat_cols[i+1:]:
res = chi2_for_cols(df, col_x, col_y)
results.append(res)
wyniki = pd.DataFrame(results).sort_values("p")
print(wyniki.head())Taka tabelka od razu pokazuje, gdzie zależności są najsilniej udokumentowane statystycznie. Zamiast szukać na oślep, można przejść od razu do najbardziej „obiecujących” par.
Prosty próg istotności i filtr p-value
Z perspektywy biznesowej zwykle wystarcza ustalony z góry próg, np. 0.05. Wtedy raport można przyciąć do par z p-value poniżej tego progu:
alpha = 0.05
istotne = wyniki[wyniki["p"] < alpha]
print(istotne[["x", "y", "p", "n"]])Przy większej liczbie testów przydają się korekty na wielokrotne porównania (np. Bonferroni lub Benjamini-Hochberg), ale w małych, jednorazowych analizach często nie jest to pierwszorzędny problem. Jeśli liczba testów rośnie do setek, wtedy warto dorzucić prostą korektę w tym samym module.
Założenia testu chi kwadrat i typowe pułapki
Wymóg odpowiednio dużych liczebności oczekiwanych
Standardowe zalecenie mówi, że w teście chi kwadrat liczebności oczekiwane nie powinny być zbyt małe (często podaje się próg 5). Przy zbyt małych komórkach:
- rozrzut statystyki testowej rośnie,
- p-value przestaje być wiarygodne.
Szybki check w Pythonie:
tab = pd.crosstab(df["plec"], df["kanal_zakupu"])
_, _, _, expected = chi2_contingency(tab)
print((expected < 5).sum(), "komórek z E < 5")Jeśli takich komórek jest sporo, dwa najszybsze remedia to:
- łączenie rzadkich kategorii w „inne”,
- odcięcie ogona (analiza tylko top N kategorii).
Alternatywnie przy tabelach 2×2 można przejść na test dokładny Fishera, ale koszt obliczeń rośnie przy większych tabelach.
Zmienne muszą być kategoryczne – nie testuj surowych liczb
Test chi kwadrat zakłada zmienne jakościowe (nominalne lub porządkowe). Zdarza się, że ktoś wrzuca do tabeli np. zgrupowane wartości ciągłe bez przemyślenia progu. Binning „z palca” potrafi generować sztuczne zależności.
Jeśli trzeba binować (np. wiek), lepiej użyć prostych, zrozumiałych przedziałów:
bins = [0, 25, 35, 50, 120]
labels = ["<25", "25-34", "35-49", "50+"]
df["wiek_grupa"] = pd.cut(df["wiek"], bins=bins, labels=labels, right=False)
Dobrze, gdy takie przedziały potem trafiają także do dashboardów, a nie tylko do jednorazowego notebooka – zmniejsza to chaos definicyjny w organizacji.
Brak kierunku zależności i brak przyczynowości
Test niezależności chi kwadrat mówi tylko, że istnieje statystyczny związek między zmiennymi. Nie mówi:
- która zmienna „wpływa” na którą,
- czy zależność nie wynika z trzeciego, pominiętego czynnika.
Przykładowo związek „typ urządzenia × kanał zakupu” nie oznacza jeszcze, że zmiana domyślnego kanału w aplikacji mobilnej spowoduje identyczny rozkład jak w przeglądarce. Do decyzji produktowych potrzebna jest jeszcze logika biznesowa i czasem prosty test A/B, a nie tylko chi kwadrat z danych historycznych.
Ogromne próby i „wszystko wychodzi istotne”
Przy setkach tysięcy obserwacji nawet mikroskopijne różnice będą dawały ekstremalnie małe p-value. Wtedy sam test odpowiada głównie na pytanie „czy różnica istnieje w ogóle?”, a nie „czy ma znaczenie w praktyce”.
W takiej sytuacji lepiej ograniczyć uwagę do:
- wielkości efektu (miary typu V Craméra),
- skali różnic w konkretnych komórkach (np. +2 p.p. vs +20 p.p.),
- wpływu na wynik finansowy (różnice przemnożone przez margin, wolumen itd.).

Miara siły związku: V Craméra w Pythonie
Dlaczego sam test chi kwadrat nie wystarcza
Test chi kwadrat mówi „istotne / nieistotne”, ale nie mówi, czy zależność jest silna. W praktyce przydaje się jedna liczba w skali 0–1, którą można porównać między różnymi parami zmiennych. Do danych kategorycznych najchętniej używa się V Craméra.
Wzór na V Craméra i implementacja
V Craméra definiuje się jako:
V = sqrt(chi2 / (n * (min(r-1, c-1))))
gdzie:
chi2– statystyka testu chi kwadrat,n– całkowita liczba obserwacji,r– liczba wierszy w tabeli,
Interpretacja V Craméra w praktyce
W literaturze można spotkać propozycje progów typu „słaba / umiarkowana / silna” zależność, ale w zastosowaniach biznesowych bardziej przydaje się prosty, kontekstowy schemat:
V < 0.1– zależność raczej kosmetyczna, zwykle niewarta samodzielnego działania,0.1 ≤ V < 0.3– związek widoczny, dobry kandydat do segmentacji i testów A/B,0.3 ≤ V– mocny efekt, który zazwyczaj ma wyraźny ślad w KPI.
Progi traktuj orientacyjnie. Dla produktu z milionami użytkowników nawet „kosmetyczne” V może oznaczać dziesiątki tysięcy transakcji rocznie. Z drugiej strony w małym B2B z kilkuset kontraktami drobna zależność nie ma żadnej wartości operacyjnej.
Funkcja pomocnicza do liczenia V Craméra
Do bieżącej pracy wygodnie jest mieć jedną prostą funkcję, która policzy zarówno test chi kwadrat, jak i V Craméra:
import numpy as np
import pandas as pd
from scipy.stats import chi2_contingency
def chi2_with_cramers_v(df, col_x, col_y, min_count=5):
tab = pd.crosstab(df[col_x], df[col_y])
if (tab.values < min_count).any():
print(f"Uwaga: w tabeli dla {col_x} vs {col_y} są komórki < {min_count}")
chi2, p, dof, expected = chi2_contingency(tab)
n = tab.values.sum()
r, c = tab.shape
k = min(r - 1, c - 1)
if k == 0:
v = np.nan
else:
v = np.sqrt(chi2 / (n * k))
return {
"x": col_x,
"y": col_y,
"chi2": chi2,
"p": p,
"dof": dof,
"n": n,
"v_cramer": v
}Dzięki takiemu helperowi tabela zbiorcza od razu zawiera miarę siły związku i nie trzeba wracać do surowych wyników, żeby ocenić sensowność różnic.
Ranking par zmiennych po V Craméra
Jeśli w projekcie liczy się czas, najlepiej od razu posortować wszystkie pary według siły związku. Pozwala to odsiać technicznie „istotne”, ale praktycznie słabe efekty:
cat_cols = ["plec", "kanal_zakupu", "kraj", "segment", "typ_klienta"]
results = []
for i, col_x in enumerate(cat_cols):
for col_y in cat_cols[i+1:]:
res = chi2_with_cramers_v(df, col_x, col_y)
results.append(res)
wyniki = pd.DataFrame(results)
wyniki = wyniki.sort_values("v_cramer", ascending=False)
print(wyniki[["x", "y", "p", "v_cramer"]].head(10))W kilku linijkach powstaje „mapa” najsilniejszych zależności w danych. Na początek można przeanalizować tylko top 5–10 par, zamiast przeglądać całą listę na oślep.
Łączenie testu chi kwadrat z prostą wizualizacją
Gołe liczby często nie przekonują biznesu. Zwykły wykres słupkowy bywa tańszy i skuteczniejszy niż dziesięć linijek interpretacji. Najprostszy schemat:
- Policz test chi kwadrat i V Craméra.
- Wyrysuj rozkład procentowy po jednej z osi tabeli.
- Na wykresie podaj p-value i
Vw tytule lub podtytule.
import seaborn as sns
import matplotlib.pyplot as plt
tab = pd.crosstab(df["plec"], df["kanal_zakupu"], normalize="index") * 100
tab = tab.reset_index().melt(id_vars="plec", var_name="kanal", value_name="proc")
chi2, p, dof, exp = chi2_contingency(pd.crosstab(df["plec"], df["kanal_zakupu"]))
n = len(df)
r, c = pd.crosstab(df["plec"], df["kanal_zakupu"]).shape
v = np.sqrt(chi2 / (n * min(r-1, c-1)))
plt.figure(figsize=(6,4))
sns.barplot(data=tab, x="plec", y="proc", hue="kanal")
plt.title(f"Pleć × kanał zakupu (p={p:.4f}, V={v:.3f})")
plt.ylabel("Udział [%]")
plt.tight_layout()
plt.show()Taka kombinacja „liczba + obrazek” zwykle skraca dyskusję podczas spotkań i ułatwia przejście od analizy do konkretnych decyzji (np. „robimy osobną kreację dla aplikacji mobilnej”).
Porównywanie wielu tabel: heatmapa V Craméra
Przy kilkunastu zmiennych jakościowych przydaje się zbiorczy widok siły zależności między każdą parą. Zamiast przeklikiwać kolejne DataFrame, można zbudować prostą heatmapę:
cat_cols = ["plec", "kanal_zakupu", "kraj", "segment", "typ_klienta"]
v_matrix = pd.DataFrame(
np.nan, index=cat_cols, columns=cat_cols
)
for i, col_x in enumerate(cat_cols):
for col_y in cat_cols[i+1:]:
tab = pd.crosstab(df[col_x], df[col_y])
chi2, p, dof, exp = chi2_contingency(tab)
n = tab.values.sum()
r, c = tab.shape
k = min(r - 1, c - 1)
v = np.sqrt(chi2 / (n * k)) if k > 0 else np.nan
v_matrix.loc[col_x, col_y] = v
v_matrix.loc[col_y, col_x] = v
plt.figure(figsize=(6,5))
sns.heatmap(v_matrix, annot=True, fmt=".2f", cmap="Blues")
plt.title("V Craméra między zmiennymi kategorycznymi")
plt.tight_layout()
plt.show()W kilka sekund widać, które pary są praktycznie niezależne, a które „sklejone” ze sobą do tego stopnia, że segmentacja na obu naraz nie ma sensu (duplikowanie informacji).
Test chi kwadrat w modelowaniu i selekcji zmiennych
Wstępna selekcja cech do modeli klasyfikacyjnych
Przy prostych modelach klasyfikacyjnych (np. logistic regression, drzewka) test chi kwadrat bywa wygodnym filtrem: pozwala szybko przesiać mnóstwo kategorii i zostawić tylko te, które w ogóle „rozmawiają” ze zmienną docelową. To tani etap przed droższym strojenie modelu.
Przykładowy workflow dla zmiennej binarnej y i zestawu cech kategorycznych:
target = "czy_kupil"
cat_cols = ["kanal_zakupu", "segment", "kraj", "typ_klienta"]
rows = []
for col in cat_cols:
res = chi2_with_cramers_v(df, col, target)
rows.append(res)
fs = pd.DataFrame(rows).sort_values("p")
print(fs)Dalej można zdefiniować prosty próg, który przytniesz listę cech:
alpha = 0.01
min_v = 0.05
wybrane = fs[(fs["p"] < alpha) & (fs["v_cramer"] > min_v)]["x"].tolist()
print("Zostawiamy zmienne:", wybrane)To tania metoda, która nie wymaga trenowania wielu modeli. Daje też transparentne kryterium: „cecha zostaje, jeśli istotnie i choć trochę sensownie koreluje z targetem”.
Ostrożność przy bardzo wielu cechach
Gdy liczba zmiennych kategorycznych idzie w dziesiątki–setki, prosta filtracja po p-value będzie narażona na fałszywe alarmy. Wtedy przydaje się:
- korekta na wielokrotne porównania (np. Benjamini-Hochberg na p-value),
- połączenie progu p-value z progiem
V, - krótka walidacja: uruchomienie niedużego modelu tylko na wybranych cechach i sprawdzenie jakości.
Koszt czasowy takich zabezpieczeń jest mały, a ogranicza ryzyko, że model oprze się na czysto losowych różnicach.
Test chi kwadrat a encodery dla zmiennych kategorycznych
W wielu pipeline’ach machine learningu zmienne kategoryczne są zamieniane na liczby (One-Hot, Target Encoding, WOE itp.). Test chi kwadrat dobrze komponuje się z prostymi encoderami:
- przed One-Hot – odsiejesz kolumny, które nic nie wnoszą, zanim jeszcze rozbije się je na dziesiątki dummy,
- przed Target Encoding – ograniczysz ryzyko przeuczenia na losowym szumie, zostawiając tylko zmienne z jakąkolwiek realną zależnością.
Technicznie to ten sam kod co przy klasycznej analizie zależności, zmienia się jedynie lista kolumn, która trafia dalej do encodera.
Ekonomiczne podejście do wielu testów chi kwadrat
Priorytetyzacja par do testowania
W dużych tabelach cech liczba par rośnie szybko, a każdy test to dodatkowy czas obliczeń i interpretacji. Zamiast testować wszystko „na hurra”, można zastosować prosty filtr wstępny:
- pominąć pary z bardzo dużą liczbą kategorii po obu stronach (np. 50 × 40) – zostawić je na później lub w ogóle,
- najpierw przetestować zmienne, które są biznesowo kluczowe (np. kraj, kanał, segment),
- w drugiej kolejności dorzucić mniej ważne cechy (np. kolor motywu UI, preferencje newslettera).
Nawet tak prosta heurystyka skraca analizę o połowę bez poważnej straty informacji.
Ograniczanie liczby kategorii przed testem
Testy na surowych, długich ogonach kategorii (np. tysiące kampanii, setki małych miejscowości) są kosztowne i mało czytelne. W większości przypadków wystarczy:
- zostawić kilka–kilkanaście najpopularniejszych kategorii,
- resztę zgrupować do jednej etykiety „inne”,
- lub zdefiniować proste reguły biznesowe (np. miasta wojewódzkie vs reszta).
top_n = 10
col = "kampania"
top_vals = df[col].value_counts().head(top_n).index
df[col + "_grp"] = np.where(df[col].isin(top_vals), df[col], "inne")
tab = pd.crosstab(df["segment"], df[col + "_grp"])
chi2, p, dof, exp = chi2_contingency(tab)Takie uproszczenie obniża koszty analizy (mniej komórek, mniej testów, szybsza interpretacja), a jednocześnie lepiej odzwierciedla realne decyzje, jakie można potem podjąć (nikt nie będzie osobno optymalizował 347 kampanii).
Bufor bezpieczeństwa na małe liczebności
Zamiast każdorazowo ręcznie sprawdzać liczebności oczekiwane, opłaca się zbudować prosty automat:
def safe_chi2(df, col_x, col_y, min_expected=5):
tab = pd.crosstab(df[col_x], df[col_y])
chi2, p, dof, expected = chi2_contingency(tab)
too_small = (expected < min_expected).sum()
return {
"x": col_x,
"y": col_y,
"chi2": chi2,
"p": p,
"dof": dof,
"n": tab.values.sum(),
"cells_lt_min": int(too_small)
}Później wystarczy jedno filtrowanie:
res = []
for i, col_x in enumerate(cat_cols):
for col_y in cat_cols[i+1:]:
res.append(safe_chi2(df, col_x, col_y))
wyn = pd.DataFrame(res)
ok = wyn[wyn["cells_lt_min"] == 0]Dzięki temu nie marnuje się czasu na interpretację tabel, dla których założenia testu i tak są złamane.
Test chi kwadrat w monitoringu procesów i dashboardach
Prosty monitoring „czy coś się zmieniło” w czasie
Częsty scenariusz: produkt działa, co miesiąc spływają dane, a zadanie brzmi „wyłapuj odchylenia zanim będą drogie”. Test chi kwadrat dobrze nadaje się do porównywania rozkładów między okresami:
okres_1 = df[df["miesiac"] == "2023-10"]
okres_2 = df[df["miesiac"] == "2023-11"]
tab = pd.crosstab(okres_1["kanal_zakupu"], okres_2["kanal_zakupu"])
# Uwaga: to nie jest klasyczna tabela kontyngencji: tu raczej porównujemy rozkłady.
tab1 = okres_1["kanal_zakupu"].value_counts()
tab2 = okres_2["kanal_zakupu"].value_counts()
tabela = pd.concat([tab1, tab2], axis=1, keys=["okres_1", "okres_2"]).fillna(0)
chi2, p, dof, exp = chi2_contingency(tabela.T)Interpretacja jest prosta: jeśli p-value jest małe, struktura kanałów w nowym miesiącu różni się od poprzedniego bardziej, niż można by się spodziewać po losowych fluktuacjach. W praktyce taki test można zautomatyzować i odpalać cyklicznie.
Integracja z dashboardem bez ciężkiej infrastruktury
Nie zawsze opłaca się budować pełny backend statystyczny. Często wystarczy:
- prosty skrypt Python uruchamiany raz dziennie lub raz w tygodniu (cron, Airflow, CI/CD),
- wynik testu (p-value, V Craméra, flaga „istotne/nieistotne”) zapisany do tabeli w bazie lub do pliku CSV,
- lekka warstwa wizualna (np. Looker Studio, Power BI, Metabase), która tylko czyta gotowe liczby.
Koszt wdrożenia jest niski, a zespół produktowy zyskuje jasny sygnał, że np. zmienił się profil urządzeń, krajów lub kanałów akwizycji.
Alerty na podstawie p-value i wielkości efektu
Najczęściej zadawane pytania (FAQ)
Kiedy używać testu chi kwadrat w Pythonie, a kiedy lepszy będzie inny test?
Test chi kwadrat stosuje się, gdy masz dwie zmienne kategoryczne i tabelę zliczeń (tabelę kontyngencji), np. „kanał dotarcia” × „konwersja (tak/nie)” albo „typ abonamentu” × „czy klient odszedł”. Jeśli interesują Cię różnice w średnich (np. średni koszyk A vs B), używasz raczej testu t-Studenta lub ANOVA. Gdy patrzysz na zależność dwóch zmiennych liczbowych (np. czas na stronie vs koszyk), stosujesz korelację Pearsona lub Spearmana.
Prosty filtr: jeżeli dane, które wrzucasz do testu, są zliczeniami w kategoriach (tabela krzyżowa), test chi kwadrat ma sens. Jeśli operujesz na surowych liczbach i chcesz porównać średnie lub obliczyć korelację, szukaj innego narzędzia, bo test chi kwadrat nie jest do tego zaprojektowany.
Jak krok po kroku zrobić test chi kwadrat w Pythonie (pandas + SciPy)?
Najbardziej budżetowa ścieżka to: przygotować dane w pandas, zbudować tabelę krzyżową i podać ją do scipy.stats.chi2_contingency. Typowy schemat wygląda tak: wczytanie danych (CSV/Excel/baza) → proste czyszczenie → pd.crosstab → chi2_contingency → odczytanie p-value. To kilka linijek kodu, które da się potem wpiąć w stały pipeline.
W kodzie sprowadza się to zwykle do wzorca:
- tabela = pd.crosstab(df[„zmienna1”], df[„zmienna2”])
- from scipy.stats import chi2_contingency
- chi2, p, dof, expected = chi2_contingency(tabela)
Na start wystarczy zinterpretować samo p: jeśli p < 0.05, traktujesz wynik jako statystycznie istotny, przy czym próg 0.05 to uzgodniona konwencja, a nie prawo fizyki.
Jak interpretować wynik testu chi kwadrat: p-value, chi2, df?
W praktyce większość decyzji opiera się na p-value. Niska wartość p (np. < 0.05) oznacza, że tak duże różnice w tabeli są mało prawdopodobne przy założeniu niezależności, więc odrzucasz hipotezę zerową i mówisz, że zmienne są powiązane. Wysokie p sugeruje, że dane nie dają mocnej podstawy, by twierdzić, że zależność istnieje.
Sama wartość chi2 rośnie, gdy różnice między liczebnościami obserwowanymi i oczekiwanymi są duże. Liczba stopni swobody (df) zależy od rozmiaru tabeli, ale na co dzień rzadko musisz ją ręcznie wykorzystywać – SciPy używa jej „pod spodem” do policzenia p-value. Do prezentacji na slajdzie najczęściej wystarcza prosty komunikat typu: „p < 0.01, zależność między kanałem a konwersją jest istotna statystycznie”.
Jakie są założenia testu chi kwadrat i kiedy wynik może być niewiarygodny?
Test chi kwadrat zakłada, że masz wystarczająco duże liczebności w komórkach tabeli; skrajnie małe wartości (zwłaszcza zera i jedynki) potrafią wypaczyć wynik. Dane powinny też pochodzić z niezależnych obserwacji (ten sam użytkownik nie powinien pojawiać się wielokrotnie jako „osobna” próba w tabeli).
Jeśli tabela jest bardzo rzadka, próba malutka albo rozkład jest mocno nierówny (np. prawie wszyscy w jednej kategorii), test może dawać mylące rezultaty. W takiej sytuacji tańszym i często rozsądniejszym podejściem jest: najpierw zwykła tabela krzyżowa i prosty wykres, a dopiero przy większych próbach – formalny test chi kwadrat lub alternatywy (np. test Fishera dla małych tabel 2×2).
Czy zawsze opłaca się robić test chi kwadrat, czy czasem wystarczy sama tabela kontyngencji?
Przy jednorazowej, małej analizie koszt „dopieszczenia” danych pod test może być większy niż zysk. Jeśli masz kilkadziesiąt obserwacji i chcesz się tylko zorientować „kto mniej więcej kupuje”, często wystarczy pd.crosstab + prosty wykres słupkowy. Daje to szybki obraz bez dokładania warstwy statystycznej, którą i tak trudno obronić przy małej próbie.
Test chi kwadrat zaczyna naprawdę się opłacać, gdy: dane są większe, spory o wyniki są częste („czy kanał X faktycznie jest lepszy?”) i proces analiz powtarza się cyklicznie. Wtedy jednorazowa inwestycja w napisanie prostego skryptu (crosstab + chi2_contingency) zwraca się co tydzień lub co miesiąc.
Jakie są typowe zastosowania testu chi kwadrat w marketingu i e-commerce?
Najczęstsze scenariusze to porównywanie udziałów między kategoriami. Przykłady z życia: zależność „kanał dotarcia” × „konwersja (tak/nie)”, „wariant kreacji” × „czy użytkownik kliknął”, „typ urządzenia” × „czy dodał do koszyka”. W takich sytuacjach test odpowiada na pytanie, czy różnice w współczynnikach konwersji między kategoriami są losowe, czy jednak systematyczne.
W e-commerce i kampaniach performance można zautomatyzować taki test w Pythonie i co raport/iterację kampanii odpalać go dla wybranych par zmiennych. Koszt to raz zrobiony notebook lub skrypt, zysk – twardszy argument przy decyzjach budżetowych niż „na oko słupki wyglądają na inne”.
Czym różni się test niezależności chi kwadrat od testu dopasowania chi kwadrat?
Test niezależności chi kwadrat (opisywany tutaj) sprawdza, czy istnieje zależność między dwiema zmiennymi kategorycznymi i działa na tabeli kontyngencji (wiersze × kolumny). Test dopasowania chi kwadrat pracuje na jednej zmiennej kategorycznej i sprawdza, czy jej rozkład pasuje do z góry założonego rozkładu, np. czy odpowiedzi „tak/nie” są rzeczywiście 50/50.
W Pythonie oba testy korzystają z podobnej idei porównania liczebności obserwowanych i oczekiwanych, ale różni się struktura danych wejściowych. W codziennej analizie danych biznesowych częściej używa się testu niezależności, bo większość praktycznych pytań dotyczy powiązania dwóch atrybutów (np. segment × zachowanie).






