Kolorowy kod Pythona na monitorze symbolizujący analizę danych
Źródło: Pexels | Autor: Markus Spiske
5/5 - (1 vote)

Nawigacja po artykule:

Po co bootstrap w analizie danych – kontekst i motywacja

Realny problem: wynik mamy, ale jak duża jest niepewność?

Średnia, mediana, współczynnik korelacji, różnica konwersji w teście A/B – to liczby, które zwykle pojawiają się w raportach. Problem zaczyna się w momencie, gdy ktoś zadaje proste pytanie: jak bardzo można ufać tym liczbom? Innymi słowy – jak duża jest niepewność estymatora, czyli oszacowania, które powstało na podstawie skończonej próbki danych, a nie całej populacji.

W klasycznej statystyce odpowiedzi szuka się w wzorach na błąd standardowy, odchylenia, rozkłady t‑Studenta i normalny. To działa, jeśli:

  • próba jest dość duża,
  • dane są „w miarę normalne”, bez dużej skośności i ekstremów,
  • statystyka jest prosta (np. średnia),
  • założenia modelu są dobrze spełnione.

W praktyce analizy danych sytuacja wygląda inaczej. Dane są brudne, rozkłady dziwne, a interesująca statystyka to często coś niestandardowego – np. mediana czasu dostawy, różnica median, wskaźnik churn, czy metryka jakości modelu. Wtedy klasyczne wzory przestają być wygodne, a czasem po prostu przestają działać sensownie.

Gdzie klasyczne podejście zaczyna być niewygodne

W kilku typowych sytuacjach analityk napotyka mur:

  • Mała próba – kilkanaście lub kilkadziesiąt obserwacji, gdzie przybliżenia asymptotyczne (dla dużych n) są wątpliwe.
  • Brak rozkładu normalnego – dane mocno skośne, z długim „ogonem” (np. przychody na użytkownika, czas odpowiedzi API, wielkość koszyka w e‑commerce).
  • Złożona statystyka – np. AUC modelu, różnica współczynników korelacji, udział wartości powyżej progu, niestandardowy scoring.
  • Brak prostej formuły na błąd standardowy – teoretyczny wzór byłby tak skomplikowany, że nikt go realnie nie liczy.

Co wiemy? Mamy próbę danych, z której potrafimy policzyć interesującą statystykę (np. średnią). Czego nie wiemy? Rozkładu tej statystyki – czyli jakie wartości mogłaby ona przyjąć, gdyby powtarzać proces losowania danych wiele razy. Klasyczne podejście zakłada rozkład (np. normalny) i próbuje go analitycznie wyprowadzić. Bootstrap idzie w inną stronę.

Intuicyjna idea bootstrapu: próbka jako „mini‑populacja”

Bootstrap proponuje prostą sztuczkę: uznaj, że Twoja próbka jest przybliżeniem populacji. Zamiast liczyć wzory, wykonuj symulacje:

  1. Traktuj istniejące dane jak „mini‑populację”.
  2. Losuj z nich nowe próbki z powtórzeniami (resampling).
  3. Za każdym razem licz interesującą statystykę (średnia, mediana, AUC, itp.).
  4. Zbierz setki lub tysiące takich wartości i spójrz na ich rozkład.

Tak powstaje rozkład statystyki bootstrapowej, który można bezpośrednio zmierzyć: policzyć jego średnią, odchylenie, kwantyle, a na tej podstawie oszacować błąd standardowy czy przedziały ufności. Bez analitycznych wzorów, bez zaawansowanej teorii, ale z wykorzystaniem mocy obliczeniowej i prostych losowań.

Intuicja bootstrapu bez wzorów – jak to naprawdę działa

Mechanizm krok po kroku: losowanie z powtórzeniem

Podstawowy schemat bootstrapu jest zaskakująco krótki:

  1. Mamy wektor danych data o długości n.
  2. Losujemy z niego nową próbkę długości n z powtórzeniem.
  3. Liczymy statystykę T (np. średnią, medianę, współczynnik korelacji).
  4. Powtarzamy kroki 2–3 B razy (np. 1000).

Powstaje seria T_1, T_2, ..., T_B – to właśnie bootstrapowy rozkład statystyki. Nie jest to rozkład w sensie teoretycznym, ale praktyczna, empiryczna chmura wartości, która dobrze przybliża to, jak mogłaby zmieniać się statystyka, gdybyśmy wielokrotnie losowali nowe próbki z populacji.

Przykład z praktyki: średni czas odpowiedzi API

Wyobraźmy sobie logi z systemu, w których zarejestrowano czasy odpowiedzi API dla pewnej usługi. Zastanawiamy się, czy średni czas odpowiedzi jest stabilny i jak duża jest niepewność oszacowania. Klasyczne podejście wymagałoby sprawdzania rozkładu, testów normalności, wzorów na błąd standardowy. Z bootstrapem sytuacja jest prostsza.

Bierzemy wektor czasów odpowiedzi z ostatniego dnia, np. kilkaset lub kilka tysięcy obserwacji. Losujemy z nich wielokrotnie nowe próbki tej samej długości, oczywiście z powtórzeniami (niektóre odpowiedzi mogą się powtórzyć, inne nie trafią do konkretnej próbki). Dla każdej próbki liczymy średni czas. Po kilkuset powtórzeniach mamy już rozkład średnich czasów odpowiedzi. Teraz wystarczy spojrzeć na jego:

  • średnią – powinna być zbliżona do pierwotnej średniej z danych,
  • odchylenie standardowe – praktyczny błąd standardowy średniej,
  • kwantyle – np. 2,5% i 97,5% jako granice 95% przedziału ufności.

Bez jednego wzoru analitycznego otrzymujemy pełen obraz niepewności oszacowania. Zostaje pytanie: czy ten obraz jest wiarygodny?

Interpretacja rozkładu bootstrapowego statystyki

Chmura wartości z bootstrapu to podstawowe źródło wiedzy o niepewności. Kilka kluczowych elementów interpretacji:

  • Średnia rozkładu bootstrapowego – często traktowana jako poprawione oszacowanie parametru (choć w wielu zastosowaniach po prostu używa się oryginalnej statystyki z danych).
  • Odchylenie standardowe – empiryczny błąd standardowy estymatora.
  • Kwantyle – wyznaczają przedziały ufności typu percentylowego.
  • Kształt rozkładu – informuje, czy niepewność jest symetryczna, czy np. istnieje długi ogon w jedną stronę.

Najważniejsza różnica względem tradycyjnego podejścia: zamiast jednego punktowego wyniku i jednego błędu standardowego, mamy cały rozkład. To pozwala lepiej ocenić ryzyko. Na przykład, jeśli rozkład jest silnie skośny, to górna granica przedziału ufności może być dużo dalej od estymatora niż dolna, co ma znaczenie w analizie ryzyka negatywnych scenariuszy.

Jeden wynik kontra chmura możliwych wyników

W praktyce biznesowej często raportuje się jedną liczbę: „średni przychód na użytkownika wynosi X”. Bootstrap pozwala pokazać coś bardziej uczciwego: chmurę możliwych X przy ponownym losowaniu użytkowników. To zmienia sposób rozmowy:

  • z „wynik to X” na „najbardziej prawdopodobny wynik to X, ale typowy błąd to mniej więcej ±E”,
  • z „model A ma AUC 0.85, model B ma 0.86” na „różnica AUC między modelami w większości powtórzeń mieści się w zakresie [d1, d2]”.

Punktem wyjścia staje się pytanie: jak szeroka jest ta chmura i jak często wartości przekraczają progi, które są dla nas kluczowe (np. czy różnica konwersji rzadko spada poniżej zera). Taka perspektywa jest łatwiejsza do zakomunikowania niż skomplikowana teoria rozkładów, a bootstrap w Pythonie pozwala te liczby uzyskać kilkoma linijkami kodu.

Minimalne podstawy statystyki potrzebne do bootstrapu

Estymator, błąd standardowy, przedział ufności – operacyjne definicje

Bootstrap w Pythonie da się stosować skutecznie znając kilka krótkich definicji praktycznych:

  • Parametr – liczba opisująca całą populację (np. prawdziwa średnia czasu odpowiedzi wszystkich requestów, prawdziwa konwersja wszystkich użytkowników).
  • Statystyka – liczba policzona na podstawie próby (np. średnia z zarejestrowanych logów, konwersja w bieżącej kampanii).
  • Estymator – przepis, który mówi, jak z próby policzyć oszacowanie parametru (np. „weź średnią arytmetyczną z danych”). Ten przepis można zastosować do dowolnej próby.
  • Błąd standardowy – typowa zmienność estymatora przy powtarzaniu losowania próby z populacji.
  • Przedział ufności – zakres wartości, w którym parametr populacji ma się znaleźć z zadanym poziomem ufności (np. 95%).

Bootstrap niczego tu nie zmienia – zmienia tylko sposób, w jaki te wielkości się oblicza. Zamiast odwoływać się do wzorów, korzysta się z empirycznego rozkładu statystyki otrzymanego z resamplingu.

Parametry vs statystyki: co mierzymy, czego szukamy

Na co dzień mierzymy statystyki: średnią, medianę, wariancję w danych, metrykę modelu na zbiorze walidacyjnym. W tle zawsze istnieje parametrowy odpowiednik – np. „prawdziwa” dokładność modelu na całej, nieobserwowanej populacji przypadków. Problem polega na tym, że tej populacji nie znamy. Bootstrap odtwarza więc nie tyle „prawdziwy” świat, co świat, w którym próbka jest jego miniaturą.

Za każdym razem, gdy losujemy z próbki i liczymy statystykę, symulujemy alternatywny scenariusz: „co by było, gdybyśmy mieli trochę inne dane, ale o podobnym charakterze?”. Rozkład takich scenariuszy daje przybliżenie rozkładu estymatora parametru. Nie ma tu magii – jest tylko pragmatyczne założenie, że dane są reprezentatywne.

Klasyczne podejście kontra podejście symulacyjne

Klasyczna statystyka parametryczna zazwyczaj działa w schemacie:

  1. Przyjmij założenia o rozkładzie danych (np. normalny, Poissona, dwumianowy).
  2. Wyprowadź/wykorzystaj znane wzory na rozkład estymatora.
  3. Na podstawie tych wzorów oblicz błąd standardowy i przedziały ufności.

Podejście bootstrapowe wygląda inaczej:

  1. Unikaj (lub minimalizuj) założeń o rozkładzie.
  2. Symuluj działanie estymatora na wielu losowych próbkach z danych.
  3. Użyj empirycznego rozkładu wyników do oszacowania niepewności.

Te dwa podejścia można zestawić w prostej tabeli.

PodejścieKluczowe założeniaJak liczymy niepewnośćZastosowania
Klasyczne parametryczneZnany rozkład danych lub estymatoraWzory analityczne, tabele rozkładówŚrednia, proporcja, proste modele
BootstrapPróba reprezentatywna, niezależne obserwacjeSymulacje, resampling z powtórzeniemMałe próby, złożone statystyki, brak prostego wzoru

Jak bootstrap przybliża rozkład estymatora

Intuicyjnie bootstrap zakłada, że sposób, w jaki estymator reaguje na losowe fluktuacje w próbie, można „podejrzeć” na próbce, którą mamy. Zamiast zastanawiać się, jak wygląda rozkład średniej przy losowaniu z całej populacji, autorzy bootstrapu pytają: jak średnia zmienia się, gdy losuję z tej konkretnej próbki, którą już mam? Jeśli próbka nie jest ekstremalnie mała i nie łamie kluczowych założeń (np. niezależności obserwacji), takie przybliżenie działa zaskakująco dobrze.

Programista przy dwóch monitorach analizuje kod w zaciemnionym pokoju
Źródło: Pexels | Autor: Mikhail Nilov

Pierwsza implementacja bootstrapu w Pythonie – od zera

Prosty bootstrap z użyciem numpy

Najprościej użyć do bootstrapu w Pythonie biblioteki numpy, która umożliwia szybkie losowanie i obliczenia wektorowe. Podstawowy wariant bootstrapu można zaimplementować samodzielnie w kilku linijkach kodu.

import numpy as np

def bootstrap_statistic(data, func, n_bootstrap=1000, random_state=None):
    """
    Prosty bootstrap dla jednej statystyki.
    
    Parameters
    ----------
    data : array-like
        Dane wejściowe (1D).
    func : callable
        Funkcja licząca statystykę, np. np.mean, np.median lub własna funkcja.
    n_bootstrap : int
        Liczba replik bootstrapowych.
    random_state : int or None
        Ziarno generatora losowego dla powtarzalności.
    
    Returns
    -------
    boot_stats : np.ndarray
        Wektor wartości statystyki z bootstrapu (długości n_bootstrap).

Implementacja funkcji bootstrapującej – dokończenie

Prosta funkcja może zwracać pełny wektor replik bootstrapowych. To pozwala później dowolnie liczyć błędy standardowe, przedziały czy rysować wykresy.

import numpy as np

def bootstrap_statistic(data, func, n_bootstrap=1000, random_state=None):
    """
    Prosty bootstrap dla jednej statystyki.
    
    Parameters
    ----------
    data : array-like
        Dane wejściowe (1D).
    func : callable
        Funkcja licząca statystykę, np. np.mean, np.median lub własna funkcja.
    n_bootstrap : int
        Liczba replik bootstrapowych.
    random_state : int or None
        Ziarno generatora losowego dla powtarzalności.
    
    Returns
    -------
    boot_stats : np.ndarray
        Wektor wartości statystyki z bootstrapu (długości n_bootstrap).
    """
    rng = np.random.default_rng(random_state)
    data = np.asarray(data)
    n = data.shape[0]

    boot_stats = np.empty(n_bootstrap, dtype=float)

    for i in range(n_bootstrap):
        sample_idx = rng.integers(0, n, size=n)
        sample = data[sample_idx]
        boot_stats[i] = func(sample)

    return boot_stats

Tylko tyle kodu wystarcza, żeby mieć w ręku rozkład bootstrapowy dowolnej statystyki – jeśli tylko da się ją policzyć funkcją func na wektorze danych.

Liczenie błędu standardowego i przedziału ufności z bootstrapu

Skoro mamy już wektor boot_stats, kolejne kroki są arytmetyczne. Z punktu widzenia analityka to często najpraktyczniejszy fragment: jak z liczb przejść do konkretnej decyzji.

data = np.array([120, 130, 150, 110, 140, 160])  # np. czasy odpowiedzi w ms

boot_stats = bootstrap_statistic(data, np.mean, n_bootstrap=5000, random_state=42)

# Oryginalna statystyka
orig_mean = np.mean(data)

# Błąd standardowy średniej (empiryczny)
boot_se = np.std(boot_stats, ddof=1)

# Przedział ufności 95% typu percentylowego
ci_lower, ci_upper = np.percentile(boot_stats, [2.5, 97.5])

print("Średnia:", orig_mean)
print("Błąd standardowy (bootstrap):", boot_se)
print("95% CI (bootstrap percentylowy):", (ci_lower, ci_upper))

Fakt: trzy linijki (np.std, np.percentile) dostarczają pełnego opisu niepewności. Pytanie kontrolne: czego nadal nie wiemy? Między innymi tego, czy przedział percentylowy jest dobrym wyborem w konkretnej sytuacji – do tego wrócimy przy typach przedziałów bootstrapowych.

Bootstrap dla kilku statystyk jednocześnie

W praktyce rzadko interesuje jedna liczba. Częściej patrzymy równocześnie na średnią, medianę, może jeszcze odchylenie. Da się to obsłużyć bez powielania pętli.

Wielo-wymiarowy bootstrap statystyk

Najprościej przekazać funkcję, która zwróci kilka wartości naraz. Reszta mechaniki pozostaje bez zmian.

def mean_median(sample):
    return np.mean(sample), np.median(sample)

def bootstrap_multi(data, func, n_bootstrap=1000, random_state=None):
    rng = np.random.default_rng(random_state)
    data = np.asarray(data)
    n = data.shape[0]
    
    # Pierwsze wywołanie, żeby poznać wymiar wyjścia
    first_sample_idx = rng.integers(0, n, size=n)
    first_sample = data[first_sample_idx]
    first_result = np.asarray(func(first_sample))
    
    boot_stats = np.empty((n_bootstrap,) + first_result.shape, dtype=float)
    boot_stats[0] = first_result

    for i in range(1, n_bootstrap):
        sample_idx = rng.integers(0, n, size=n)
        sample = data[sample_idx]
        boot_stats[i] = func(sample)
    
    return boot_stats

boot_multi = bootstrap_multi(data, mean_median, n_bootstrap=5000, random_state=42)
mean_dist = boot_multi[:, 0]
median_dist = boot_multi[:, 1]

Dalej można liczyć błędy standardowe i przedziały osobno dla średniej i mediany, korzystając z tych samych funkcji NumPy. Ta konstrukcja przydaje się np. przy estymacji kilku parametrów modelu lub kilku metryk jakości.

Bootstrap w porównywaniu dwóch grup

Częsty scenariusz: porównanie metryk w wariancie A i B testu eksperymentalnego. Pytanie nie brzmi już „ile wynosi średnia?”, ale „jaka jest różnica średnich i jakiej niepewności się spodziewać?”.

Różnica średnich – prosty przypadek porównania

Załóżmy dwa wektory: czasy odpowiedzi dla wersji starej i nowej API. Interesuje różnica średnich: mean_B - mean_A. Funkcja bootstrapująca może wprost pracować na parze tablic.

def bootstrap_diff_mean(data_a, data_b, n_bootstrap=1000, random_state=None):
    rng = np.random.default_rng(random_state)
    data_a = np.asarray(data_a)
    data_b = np.asarray(data_b)
    
    n_a = data_a.shape[0]
    n_b = data_b.shape[0]
    
    boot_diffs = np.empty(n_bootstrap, dtype=float)
    
    for i in range(n_bootstrap):
        sample_a = data_a[rng.integers(0, n_a, size=n_a)]
        sample_b = data_b[rng.integers(0, n_b, size=n_b)]
        
        boot_diffs[i] = np.mean(sample_b) - np.mean(sample_a)
        
    return boot_diffs

# Przykład użycia
diff_dist = bootstrap_diff_mean(times_old, times_new, n_bootstrap=5000, random_state=123)

diff_mean = np.mean(diff_dist)
ci_lower, ci_upper = np.percentile(diff_dist, [2.5, 97.5])
prob_improvement = np.mean(diff_dist < 0)  # np. krótszy czas = lepszy

print("Średnia różnica (B - A):", diff_mean)
print("95% CI:", (ci_lower, ci_upper))
print("Prawdopodobieństwo, że B jest lepsze:", prob_improvement)

W tym ujęciu „prawdopodobieństwo, że B jest lepsze” to odsetek replik, w których różnica ma pożądany znak. To liczba, którą łatwo podać interesariuszom: zamiast suchego p-value, informacja „w około X% symulacji nowa wersja wygrywa”.

Bootstrap dla proporcji i konwersji

W testach A/B często pracujemy na konwersji (0/1). Bootstrap traktuje ją jak wektor liczb, więc kod pozostaje bardzo podobny. Estymowana statystyka to teraz różnica proporcji.

def conversion_rate(x):
    x = np.asarray(x)
    return np.mean(x)  # średnia 0/1 to proporcja sukcesów

def bootstrap_diff_conversion(conv_a, conv_b, n_bootstrap=1000, random_state=None):
    rng = np.random.default_rng(random_state)
    conv_a = np.asarray(conv_a, dtype=int)
    conv_b = np.asarray(conv_b, dtype=int)
    
    n_a = conv_a.shape[0]
    n_b = conv_b.shape[0]
    
    boot_diffs = np.empty(n_bootstrap, dtype=float)
    
    for i in range(n_bootstrap):
        sample_a = conv_a[rng.integers(0, n_a, size=n_a)]
        sample_b = conv_b[rng.integers(0, n_b, size=n_b)]
        
        boot_diffs[i] = conversion_rate(sample_b) - conversion_rate(sample_a)
        
    return boot_diffs

Interpretacja jest identyczna: rozkład boot_diffs pokazuje, jak bardzo w typowym scenariuszu różni się konwersja. Przy małych próbach i nieregularnych danych podejście bootstrapowe bywa stabilniejsze niż klasyczne testy, które opierają się na założeniach asymptotycznych.

Kolorowy kod Pythona na ekranie podczas analizy danych
Źródło: Pexels | Autor: Myburgh Roux

Warianty przedziałów ufności w bootstrapie

Do tej pory wykorzystywany był najprostszy wariant – przedział percentylowy. To często wystarcza, ale w niektórych sytuacjach lepiej sprawdzają się inne konstrukcje. Co wiemy na pewno: różne przedziały zakładają inną strukturę błędu i symetrii rozkładu bootstrapowego.

Przedział percentylowy – prosty i intuicyjny

Definicja operacyjna: 95% przedział ufności to [2,5-ty; 97,5-ty] percentyl rozkładu bootstrapowego statystyki. Implementacja jest prosta.

def ci_percentile(boot_stats, alpha=0.05):
    lower = 100 * (alpha / 2)
    upper = 100 * (1 - alpha / 2)
    return np.percentile(boot_stats, [lower, upper])

Dobrze sprawdza się, gdy rozkład bootstrapowy jest w miarę gładki, a próbka nie jest ekstremalnie mała. Gorzej, jeśli rozkład ma mocno skośny kształt lub znajdujemy się blisko granic (np. proporcje bliskie 0 lub 1).

Przedział „basic” (odwrócony percentyl)

Przedział typu „basic” korzysta z symetrii błędu względem estymatora z próby. W wersji praktycznej:

  1. Wyznacz wartości percentylowe z bootstrapu.
  2. Odbij je symetrycznie względem oryginalnej statystyki.
def ci_basic(boot_stats, orig_stat, alpha=0.05):
    lower_p = 100 * (alpha / 2)
    upper_p = 100 * (1 - alpha / 2)
    
    q_lower, q_upper = np.percentile(boot_stats, [lower_p, upper_p])
    
    ci_lower = 2 * orig_stat - q_upper
    ci_upper = 2 * orig_stat - q_lower
    return ci_lower, ci_upper

Ten wariant ma sens, gdy widzimy umiarkowaną skośność rozkładu, a chcemy zachować symetrię wokół estymatora z danych. To kompromis między prostotą a korektą dla skośności.

Przedział „normalny” oparty na błędzie standardowym

Jeśli rozkład bootstrapowy jest zbliżony do normalnego, można użyć klasycznej konstrukcji: estymator ± z·SE. Zaletą jest spójność z tradycyjnym raportowaniem (średnia ± 1,96·SE).

from scipy.stats import norm

def ci_normal(boot_stats, orig_stat, alpha=0.05):
    se = np.std(boot_stats, ddof=1)
    z = norm.ppf(1 - alpha / 2)
    return orig_stat - z * se, orig_stat + z * se

Tu bootstrap używany jest wyłącznie do oszacowania SE, a sam kształt rozkładu przyjmujemy jako normalny. To rozsądne np. przy średnich z umiarkowanie dużych prób, gdzie centralne twierdzenie graniczne „robi swoje”.

Wydajność bootstrapu: kiedy Python wytrzyma?

Resampling wymaga wielu powtórzeń, a każde to liczenie statystyki na pełnej próbie. Dla średnich i prostych funkcji działa to zaskakująco szybko, ale gdy w grę wchodzą złożone modele, pętla może stać się wąskim gardłem.

Wektoryzacja i ograniczanie liczby pętli

W przypadku prostych statystyk część pracy da się przerzucić na NumPy, generując od razu całą macierz indeksów i korzystając z operacji macierzowych. Przykład dla średniej:

def bootstrap_mean_vectorized(data, n_bootstrap=1000, random_state=None):
    rng = np.random.default_rng(random_state)
    data = np.asarray(data)
    n = data.shape[0]
    
    # Indeksy: kształt (n_bootstrap, n)
    idx = rng.integers(0, n, size=(n_bootstrap, n))
    samples = data[idx]           # kształt (n_bootstrap, n)
    boot_means = samples.mean(axis=1)
    return boot_means

Takie podejście ogranicza Pythonowe pętle, co przy dużej liczbie replik daje zauważalny zysk czasowy. Ma jednak sens głównie dla prostych funkcji typu mean, median czy np.percentile.

Równoległość: wielordzeniowe CPU i joblib

Jeśli statystyka jest kosztowna (np. trenowanie małego modelu lub skomplikowana agregacja), rozsądne jest uruchomienie replik na kilku rdzeniach. Użycie joblib bywa wystarczające w prostych projektach.

from joblib import Parallel, delayed

def bootstrap_parallel(data, func, n_bootstrap=1000, n_jobs=-1, random_state=None):
    rng = np.random.default_rng(random_state)
    data = np.asarray(data)
    n = data.shape[0]
    
    seeds = rng.integers(0, 2**32 - 1, size=n_bootstrap)
    
    def single_boot(seed):
        local_rng = np.random.default_rng(seed)
        idx = local_rng.integers(0, n, size=n)
        sample = data[idx]
        return func(sample)
    
    boot_stats = Parallel(n_jobs=n_jobs)(delayed(single_boot)(s) for s in seeds)
    return np.asarray(boot_stats)

W tym wariancie każda replika ma własne ziarno, więc losowanie pozostaje niezależne, a obciążenie dzieli się między rdzenie. W projektach analitycznych może to być różnica między minutą a godziną oczekiwania na wyniki.

Bootstrap w ocenie modeli ML

Przy modelach predykcyjnych bootstrap pomaga odpowiedzieć na praktyczne pytania: jak stabilna jest metryka? czy różnica między dwoma modelami jest istotna, czy to tylko przypadkowa fluktuacja walidacyjnego zbioru?

Bootstrap próbek z walidacji dla pojedynczego modelu

Mamy wektor etykiet prawdziwych y_true i przewidywań y_pred_proba (np. prawdopodobieństwa klasy pozytywnej). Na tej podstawie liczymy metrykę, np. AUC.

from sklearn.metrics import roc_auc_score

def bootstrap_auc(y_true, y_score, n_bootstrap=2000, random_state=None):
    rng = np.random.default_rng(random_state)
    y_true = np.asarray(y_true)
    y_score = np.asarray(y_score)
    n = y_true.shape[0]
    
    boot_aucs = np.empty(n_bootstrap, dtype=float)
    
    for i in range(n_bootstrap):
        idx = rng.integers(0, n, size=n)
        boot_aucs[i] = roc_auc_score(y_true[idx], y_score[idx])
    
    return boot_aucs

boot_aucs = bootstrap_auc(y_true, y_pred_proba, 3000, random_state=0)
orig_auc = roc_auc_score(y_true, y_pred_proba)
ci_lower, ci_upper = np.percentile(boot_aucs, [2.5, 97.

Najczęściej zadawane pytania (FAQ)

Co to jest bootstrap w statystyce i jak działa w praktyce?

Bootstrap to metoda szacowania niepewności dowolnej statystyki (np. średniej, mediany, AUC) poprzez wielokrotne losowanie z już zebranych danych. Zamiast wyprowadzać wzory na błąd standardowy, symuluje się wiele „alternatywnych” prób z tej samej próby.

Technicznie wygląda to tak: traktujesz swoją próbę jako przybliżenie populacji, losujesz z niej z powtórzeniem próbki tej samej wielkości, za każdym razem liczysz interesującą statystykę i zapisujesz wynik. Z uzyskanej chmury wartości można odczytać błąd standardowy, przedziały ufności i kształt niepewności.

Kiedy warto użyć bootstrapu zamiast klasycznych wzorów statystycznych?

Bootstrap jest szczególnie użyteczny, gdy klasyczne założenia są wątpliwe lub trudne do sprawdzenia. Chodzi m.in. o przypadki, gdy:

  • masz małą próbę (kilkanaście–kilkadziesiąt obserwacji),
  • dane są mocno skośne lub mają długi ogon (np. czas odpowiedzi API, przychód na użytkownika),
  • interesuje cię złożona statystyka, dla której nie ma prostego wzoru na błąd standardowy (np. AUC, różnica współczynników korelacji),
  • model teoretyczny jest niejasny lub trudny do uzasadnienia biznesowo.

W takich warunkach bootstrap pozwala obejść skomplikowaną teorię i oprzeć się na symulacji z realnych danych.

Jak zaimplementować prosty bootstrap w Pythonie?

Najprostsza implementacja używa NumPy. Ogólny schemat: z wektora data długości n losujesz z powtórzeniem nowe próbki długości n, dla każdej liczysz statystykę i zapisujesz wynik. Powtarzasz to setki lub tysiące razy.

Przykład dla średniej:

  • przygotuj dane jako numpy.array,
  • ustal liczbę powtórzeń B (np. 1000),
  • w pętli: idx = np.random.randint(0, n, size=n), potem sample = data[idx] i boot_means.append(sample.mean()).

Z uzyskanego wektora boot_means liczysz odchylenie standardowe (błąd standardowy) i kwantyle (np. 2,5% i 97,5% dla 95% przedziału ufności).

Jak interpretować rozkład bootstrapowy i przedziały ufności?

Rozkład bootstrapowy to zbiór wartości statystyki otrzymanych w kolejnych losowaniach. Co wiemy? Widzimy, jakie wartości statystyka może przyjmować przy ponownym losowaniu próby. Czego nie wiemy? Dokładnego rozkładu teoretycznego w populacji – bootstrap go tylko przybliża.

Kluczowe elementy interpretacji są trzy: średnia z rozkładu (zwykle bliska pierwotnej statystyce), odchylenie standardowe (empiryczny błąd standardowy) oraz kwantyle (np. 2,5% i 97,5% jako granice 95% przedziału ufności). Kształt rozkładu (symetria, ogony) podpowiada, czy niepewność jest „równomierna” w obie strony, czy ryzyko leży głównie po jednej stronie.

Czy bootstrap działa dla mediany, AUC i innych nietypowych statystyk?

Tak, bootstrap jest właśnie po to, aby obsłużyć statystyki, dla których klasyczne wzory są niewygodne lub nieistnieją. W praktyce możesz „bootstrapować” dowolną funkcję z próby: medianę, różnicę median, współczynnik korelacji, AUC, udział obserwacji powyżej progu, metryki jakości modelu, a nawet niestandardowe wskaźniki biznesowe.

Warunek jest jeden: musisz umieć tę statystykę policzyć dla pojedynczej próby w Pythonie. Resampling (losowanie z powtórzeniem) działa identycznie, zmienia się tylko funkcja, którą uruchamiasz na każdej bootstrapowej próbce.

Jak dobrać liczbę powtórzeń bootstrapu (B) w analizie danych?

W prostych zastosowaniach analitycznych najczęściej stosuje się od kilkuset do kilku tysięcy powtórzeń. Dla orientacyjnych oszacowań 500–1000 replikacji zwykle wystarcza, aby przedział ufności był stabilny. Przy bardziej wrażliwych decyzjach (np. ranking modeli) często stosuje się 5000 lub więcej powtórzeń, jeśli pozwala na to czas obliczeń.

Praktyczny test jest prosty: uruchom bootstrap z różnymi wartościami B (np. 500, 1000, 2000) i sprawdź, czy wyniki (błąd standardowy, przedziały) mocno się zmieniają. Jeśli zmiany są niewielkie, liczba powtórzeń jest wystarczająca z punktu widzenia decyzji biznesowej.

Jakie są ograniczenia bootstrapu i kiedy może wprowadzać w błąd?

Bootstrap zakłada, że próbka dobrze reprezentuje populację. Jeśli danych jest bardzo mało lub są silnie zniekształcone (np. selekcja tylko „sukcesów”), symulacje z takiej próby będą systematycznie mylić rzeczywistość. Problemem są też bardzo zależne obserwacje (np. serie czasowe) – wtedy potrzebne są warianty typu block bootstrap.

W praktyce bootstrap nie „naprawia” złego zbierania danych. Umożliwia natomiast uczciwszą ocenę niepewności na tym, co już mamy. Jeśli rozkład bootstrapowy jest szeroki lub dziwny, to jest informacja sama w sobie: wynik jest niepewny i trudno go raportować jednym, pewnym numerem.

Najważniejsze wnioski

  • Kluczowy problem w analizie danych to nie tylko wartość statystyki (średnia, mediana, AUC, różnica konwersji), lecz oszacowanie jej niepewności – czyli odpowiedź na pytanie, jak bardzo można ufać liczbie wyliczonej z ograniczonej próbki.
  • Klasyczne wzory na błąd standardowy i przedziały ufności działają dobrze tylko przy dużych próbach, w miarę normalnych rozkładach i prostych statystykach; przy małych próbach, skośnych danych i złożonych metrykach szybko stają się kłopotliwe lub niepraktyczne.
  • Bootstrap traktuje dostępną próbkę jako „mini‑populację”: zamiast wyprowadzać wzory, wielokrotnie losuje z tej próby nowe zbiory danych z powtórzeniami i za każdym razem liczy interesującą statystykę, budując jej empiryczny rozkład.
  • Rozkład bootstrapowy statystyki pozwala bezpośrednio oszacować błąd standardowy, przedziały ufności (np. z kwantyli 2,5% i 97,5%) oraz sprawdzić kształt niepewności – czy jest symetryczna, czy z długim ogonem w jedną stronę.
  • Procedura bootstrapu jest koncepcyjnie prosta: z danych długości n losujemy n obserwacji z powtórzeniami, liczymy statystykę, powtarzamy ten cykl setki lub tysiące razy, a następnie analizujemy powstałą chmurę wartości T₁, T₂, …, Tᴮ.
  • W praktycznych zastosowaniach – np. przy szacowaniu średniego czasu odpowiedzi API czy różnicy konwersji – bootstrap pozwala ocenić stabilność wyniku bez sprawdzania normalności, bez zaawansowanej teorii i bez żmudnego wyprowadzania wzorów.