Osoba czyta książkę o Pythonie do administracji systemami Linux
Źródło: Pexels | Autor: Christina Morillo
Rate this post

Nawigacja po artykule:

Po co w ogóle wykres pudełkowy i kiedy go używać

Średnia, mediana czy pełny rozkład – co naprawdę analizujesz?

Przy prostych raportach wiele osób zatrzymuje się na średniej. Średnie wynagrodzenie, średni czas odpowiedzi serwera, średni wynik testu. Czy w Twoim przypadku też tak jest? Problem pojawia się, gdy rozkład jest skośny lub ma kilka ekstremalnych wartości. Wtedy średnia zaczyna kłamać, a decyzje oparte na takim streszczeniu danych przestają mieć sens.

Wykres pudełkowy (boxplot) porządkuje tę sytuację. Skupia się na medianie, kwartylach i rozstępie, czyli statystykach odpornych na wartości skrajne. Dzięki temu pokazuje nie tylko „typowy” wynik, ale też rozproszenie i asymetrię rozkładu. Dla wielu zastosowań to właśnie mediana i rozstęp są o wiele ważniejsze niż średnia, bo lepiej opisują „przeciętne doświadczenie” użytkownika czy ucznia.

Jeśli zastanawiasz się, czy w Twoim projekcie kluczowa jest średnia, czy raczej kształt rozkładu, postaw sobie jedno pytanie: czy kilka ekstremalnych przypadków może wypaczyć wynik? Jeśli tak – wykres pudełkowy powinien znaleźć się w Twoim toolboxie.

Boxplot vs histogram i wykres słupkowy

Histogram pokazuje rozkład poprzez podział na koszyki (biny). Widzisz kształt – czy wartości są skupione, czy rozlane, czy jest ogon. Wykres słupkowy zwykle porównuje agregaty (np. średnie) między kategoriami. Gdzie w tym miejscu jest wykres pudełkowy?

Boxplot daje zagęszczone streszczenie w kilku liczbach: Q1, mediana (Q2), Q3, rozstęp ćwiartkowy (IQR) i zakres (min–max, ewentualnie ograniczony przez definicję wąsów). Nie pokazuje wszystkich detali jak histogram, ale:

  • zajmuje bardzo mało miejsca – idealny do porównania wielu grup naraz,
  • eksponuje medianę zamiast średniej,
  • zaznacza potencjalne wartości odstające,
  • dobrze działa nawet przy stosunkowo małej liczbie obserwacji.

Do czego prowadzi to w praktyce? Histogram wybierasz, gdy chcesz zobaczyć dokładny kształt jednego rozkładu. Wykres słupkowy – aby porównać np. średnie między kilkoma grupami. Boxplot wykorzystujesz wtedy, gdy:

  • masz kilka–kilkanaście grup i chcesz porównać ich rozkłady jednym rzutem oka,
  • interesują Cię mediany i rozproszenie, a nie drobne szczegóły kształtu,
  • chcesz szybko złapać grupy z największym rozrzutem lub wieloma outlierami.

Kiedy wykres pudełkowy naprawdę upraszcza życie

W wielu projektach analizy danych pojawia się moment: „musimy porównać kilka kategorii”. Jakie masz teraz dane? Jedną zmienną czy kilka grup do porównania? Boxplot świetnie sprawdzi się w takich scenariuszach:

Dane eksperymentalne – porównanie rozkładu wyników w grupach eksperymentalnych i kontrolnych. Zamiast porównywać same średnie, możesz pokazać, że np. mediana poprawiła się nieznacznie, ale przy okazji zmniejszyła się zmienność wyników.

A/B testy – analiza czasu na stronie, wartości koszyka, liczby odsłon. Boxplot pomaga odpowiedzieć, czy wariant B tylko lekko poprawił średnią, czy może ograniczył sporą liczbę skrajnie złych przypadków. W badaniach UX to często ważniejsze niż sama średnia.

Wyniki egzaminów i testów – proste porównanie klas, roczników czy grup trenerów. W jednym rzędzie można zestawić kilkanaście pudełek i szybko wyłapać:

  • która grupa ma najwyższą medianę,
  • gdzie jest największe zróżnicowanie wyników,
  • czy są grupy z nietypowo dużą liczbą słabych lub bardzo dobrych wyników.

Czasy odpowiedzi systemu – w DevOps i SRE boxplot potrafi od razu pokazać problem: mediana może być akceptowalna, ale ogon rozkładu tak długi, że co dziesiąty użytkownik przeżywa dramat. Na histogramie trzeba więcej miejsca, na wykresie słupkowym ten problem w ogóle się ukryje.

Jedna zmienna czy wiele grup – kluczowe pytanie przed rysowaniem

Zanim wejdziesz w kod Pythona, odpowiedz sobie jasno: co chcesz zobaczyć: pojedynczy rozkład czy porównanie wielu kategorii?

Dla pojedynczej zmiennej boxplot sprawdzi się głównie jako szybka diagnoza: gdzie jest mediana, jak szerokie jest pudełko, czy masz od razu widoczne outliery. Jeśli jednak masz naturalny podział na grupy (płeć, wariant kampanii, szkoły, serwery, regiony), wykres pudełkowy nabiera mocy.

To właśnie porównanie wielu pudełek obok siebie pokazuje pełnię możliwości tej wizualizacji. Python (Matplotlib, Seaborn, Plotly) ułatwia takie zestawienia, ale najpierw trzeba dobrze zrozumieć, co dokładnie oznaczają elementy pojedynczego pudełka.

Programista piszący kod Pythona na laptopie w biurze obok książki
Źródło: Pexels | Autor: Christina Morillo

Minimum teorii: mediana, kwartyle, rozstęp, IQR

Mediana – odporny środek rozkładu

Mediana to wartość „środkowa” uporządkowanej listy danych. Połowa obserwacji leży poniżej, połowa powyżej. Jeśli liczba obserwacji jest nieparzysta – mediana to po prostu środkowy element. Jeśli parzysta – często przyjmuje się średnią z dwóch środkowych elementów.

Dlaczego mediana jest bardziej odporna na skrajne obserwacje niż średnia? Wyobraź sobie wynagrodzenia w małym zespole, gdzie wszystkie osoby zarabiają podobnie, a jedna osoba ma wynagrodzenie znacznie wyższe (np. właściciel). Średnia zostanie „pociągnięta” mocno w górę i przestanie reprezentować typowego pracownika. Mediana pozostanie praktycznie niezmieniona, bo jedna skrajna wartość nie zmienia położenia środka listy.

Boxplot kładzie nacisk właśnie na medianę. Linia w środku pudełka pokazuje poziom, wokół którego skupia się połowa danych. Przy raportowaniu często lepiej powiedzieć: „mediana czasu odpowiedzi wynosi 300 ms” niż operować średnią, która w obecności kilku bardzo wolnych odpowiedzi może być katastrofalnie zawyżona.

Kwartyle Q1, Q2, Q3 – subtelne różnice w definicjach

Boxplot opiera się przede wszystkim na kwartylach:

  • Q1 – 25. percentyl (wartość, poniżej której leży 25% obserwacji),
  • Q2 – 50. percentyl, czyli mediana,
  • Q3 – 75. percentyl (wartość, poniżej której leży 75% obserwacji).

Proste? Teoretycznie tak. W praktyce istnieje kilka konwencji liczenia kwartylów, szczególnie przy małych próbach. Czy wiesz, jak oblicza je Twoje narzędzie?

W Pythonie warto znać różnice:

  • NumPy – funkcja np.percentile (i nowsza np.quantile) ma parametr method (dawniej interpolation), który określa sposób wyznaczania percentyli. Domyślna metoda może się różnić między wersjami, dlatego dobrze jest ją jawnie podać.
  • pandas – metoda Series.quantile używa również parametru interpolation (w nowszych wersjach zachowanie jest bardziej spójne z NumPy). Najczęściej stosowana jest wersja liniowo interpolująca między obserwacjami.
  • Excel, R i inne narzędzia – często używają swoich domyślnych metod (np. definicja Tukeya, inne wersje „exclusive”/„inclusive”).

Przy dużych próbach różnice między tymi definicjami są małe. Przy małych próbkach (np. 8–12 obserwacji) wartości Q1 i Q3 mogą się jednak nieco różnić między narzędziami. Dlatego przy analizach, gdzie liczy się dokładna wartość IQR, dobrze jest:

  • sprawdzić dokumentację funkcji,
  • ewentualnie ustawić świadomie parametr metody liczenia percentyli,
  • w raportach wspomnieć narzędzie, jeśli różnice mogą mieć znaczenie.

Rozstęp (min–max) vs rozstęp ćwiartkowy (IQR)

Rozstęp to prosty zakres od wartości minimalnej do maksymalnej. Pokazuje pełny rozrzut danych, łącznie z potencjalnie skrajnymi punktami. Z kolei rozstęp ćwiartkowy (IQR – interquartile range) to różnica między Q3 a Q1:

IQR = Q3 - Q1

IQR mówi, jak bardzo „rozrzucona” jest środkowa połowa danych. Jest odporny na kilka ekstremalnych wartości, bo opiera się na kwartylach, nie na min i max. W klasycznym boxplocie:

  • granice pudełka to Q1 i Q3,
  • wysokość pudełka jest proporcjonalna do IQR,
  • zwykle wąsy nie sięgają po prostu min–max, ale kończą się na pewnej granicy związanej z IQR.

Po co dwa różne zakresy? Min–max pokazują pełne ekstremum, ale są wrażliwe na pojedyncze outliery. IQR lepiej oddaje „typową” zmienność. Przy opisie wyników rozsądnie jest łączyć oba podejścia: „mediana 120, IQR od 100 do 140, a pojedyncze skrajne wartości sięgają 200”. Taki opis jest zrozumiały także dla odbiorców nietechnicznych.

Pudełko, wąsy i punkty – co oznacza klasyczny boxplot

Klasyczny boxplot według konwencji Tukeya definiuje wąsy na podstawie IQR. Schemat wygląda tak:

  • pudełko: od Q1 do Q3,
  • linia w pudełku: mediana (Q2),
  • dolny „teoretyczny” koniec wąsa: Q1 – 1.5 × IQR,
  • górny „teoretyczny” koniec wąsa: Q3 + 1.5 × IQR,
  • rzeczywisty dolny wąs: najniższa obserwacja nie mniejsza niż Q1 – 1.5 × IQR,
  • rzeczywisty górny wąs: najwyższa obserwacja nie większa niż Q3 + 1.5 × IQR,
  • punkty poza wąsami: potencjalne wartości odstające (outliery).

Niektóre implementacje (w tym Matplotlib) pozwalają zmienić parametr whis, który określa zasięg wąsów. Może to być liczba (np. 1.5), percentyle (np. [5, 95]) lub konfiguracja typu „min–max”. Czy wiesz, jaki schemat stosuje Twoja domyślna funkcja rysująca?

Znajomość tej definicji jest kluczowa przy poprawnej interpretacji. Jeśli nie wiesz, jak liczone są wąsy, możesz źle ocenić, czy punkt oznaczony jako „outlier” faktycznie jest nietypowy z punktu widzenia Twojej dziedziny, czy po prostu przekracza arbitralną granicę 1.5 × IQR.

Czy wiesz, jak liczona jest mediana w Twoim narzędziu?

Przy analizach w Pythonie mediana zwykle liczona jest funkcjami:

  • np.median() (NumPy),
  • Series.median() (pandas),
  • w Seaborn i Matplotlib – pośrednio poprzez wewnętrzne wywołania.

Dla dużych prób nie będzie to miało znaczenia, ale przy małych zbiorach dobrze jest przetestować kilka prostych przykładów „na kartce” i porównać z wynikiem funkcji. Przy okazji zyskasz pewność, jak współgrają ze sobą biblioteki w Twoim stacku.

Jeśli pracujesz w środowisku, gdzie dane są równolegle analizowane np. w Excelu lub R, warto sporządzić krótką tabelę porównawczą sposobu liczenia percentyli i mediany. Ułatwi to komunikację z zespołem i unikniesz sytuacji, w której dwa narzędzia dają „różne wyniki” tylko dlatego, że stosują inne metody interpolacji.

Środowisko pracy: przygotowanie Pythona do rysowania wykresów pudełkowych

Podstawowe biblioteki: NumPy, pandas, Matplotlib, Seaborn, Plotly

Aby wygodnie tworzyć wykresy pudełkowe w Pythonie, wystarczą głównie cztery–pięć bibliotek:

  • NumPy – operacje numeryczne, obliczanie kwartylów, mediany, IQR,
  • pandas – wygodne przechowywanie danych tabelarycznych (DataFrame), z których łatwo rysować boxploty,
  • Matplotlib – podstawowa biblioteka do tworzenia wykresów, w tym funkcja plt.boxplot(),
  • Seaborn – nakładka na Matplotlib, upraszczająca tworzenie estetycznych wykresów statystycznych (sns.boxplot()),
  • Plotly (opcjonalnie) – interaktywne boxploty przydatne w dashboardach i eksploracji.

Instalacja przez pip jest prosta:

pip install numpy pandas matplotlib seaborn plotly

Jeśli używasz środowiska Anaconda, część z tych paczek będzie już zainstalowana. Możesz je ewentualnie zaktualizować:

Konfiguracja środowiska interaktywnego: Jupyter, VS Code, IDE

Gdzie będziesz najwygodniej rysować boxploty? Jeśli dopiero zaczynasz, odpowiedz sobie na pytanie: potrzebujesz bardziej „notatnika”, czy kompletnego IDE?

  • Jupyter Notebook / JupyterLab – świetny wybór do eksploracji danych, eksperymentów i prezentacji krok po kroku. Dobrze współpracuje z Matplotlib, Seaborn i Plotly.
  • VS Code – ma wbudowaną obsługę notebooków, a jednocześnie wygodną edycję plików .py. Możesz płynnie przechodzić od prototypu do „produkcyjnego” skryptu.
  • PyCharm, Spyder, inne IDE – przydatne, jeśli tworzysz większe projekty analityczne, gdzie wykresy są tylko częścią całości.

W środowisku notebookowym najlepiej od razu włączyć tryb wyświetlania wykresów w komórkach:

%matplotlib inline  # klasyczny tryb (Jupyter)
# albo
%matplotlib notebook # interaktywne okna (zoom, pan)

Z kolei w plikach .py (np. uruchamianych z terminala) trzeba zadbać o wyświetlenie okna z wykresem:

import matplotlib.pyplot as plt

# ... kod generujący wykres ...
plt.show()

Zastanów się: chcesz głównie klikać i eksplorować, czy generować raporty automatycznie? Od odpowiedzi zależy, czy lepiej postawić na notebooki, czy na skrypty uruchamiane z linii komend lub pipeline’u CI.

Ustawienia czcionek i stylu – czytelność ważniejsza niż „ładność”

Boxplot sam w sobie jest prosty, ale szybko robi się nieczytelny, gdy:

  • masz wiele grup na jednym wykresie,
  • etykiety na osi X są długie,
  • prezentujesz wyniki na projektorze lub wydruku.

Przed pierwszym wykresem odpowiedz sobie: gdzie będzie oglądany ten boxplot – na ekranie, w PDF, na prezentacji? Od tego zależy, czy podkręcisz rozmiary czcionek i figury.

import matplotlib.pyplot as plt

plt.style.use("seaborn-v0_8-whitegrid")  # czytelne tło z siatką
plt.rcParams["figure.figsize"] = (8, 5)
plt.rcParams["font.size"] = 11
plt.rcParams["axes.titlesize"] = 13
plt.rcParams["axes.labelsize"] = 12

Takie ustawienia możesz wrzucić na początku notatnika czy skryptu, żeby każdy kolejny boxplot wyglądał spójnie. Przy prezentacjach na rzutniku warto zwiększyć figure.figsize i font.size, żeby mediana i opis rozstępu były czytelne z końca sali.

Zbliżenie ekranu z kodem w Pythonie do analizy danych
Źródło: Pexels | Autor: Pixabay

Pierwszy wykres pudełkowy w Matplotlib – absolutne podstawy

Minimalny przykład z surową listą liczb

Najprościej zacząć od zwykłej listy wartości. Masz np. czasy odpowiedzi API, pomiary w eksperymencie, wyniki testu? Załóżmy, że to zwykła lista liczb zmiennoprzecinkowych.

import matplotlib.pyplot as plt

# przykładowe dane - możesz wstawić tu swoje pomiary
times = [230, 245, 260, 310, 280, 275, 290, 450, 270, 265, 255, 400]

fig, ax = plt.subplots()
ax.boxplot(times)

ax.set_title("Czas odpowiedzi API (ms)")
ax.set_ylabel("Czas [ms]")

plt.show()

Po uruchomieniu zobaczysz klasyczny boxplot z jednym pudełkiem. Pytanie kontrolne: czy potrafisz na nim wskazać medianę, Q1, Q3 i potencjalne outliery? Jeśli nie, wróć na chwilę do definicji z poprzedniej sekcji i spróbuj dopasować je do elementów wykresu.

Kontrola wąsów parametrem whis

Domyślnie Matplotlib stosuje konwencję podobną do Tukeya (1.5 × IQR). Możesz to jednak zmienić w zależności od celu analizy. Zastanów się: chcesz akcentować wartości odstające, czy raczej pokazać pełny zakres min–max?

fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)

# 1) klasyczny boxplot (whis = 1.5)
axes[0].boxplot(times, whis=1.5)
axes[0].set_title("whis = 1.5 (Tukey)")

# 2) krótsze wąsy - bardziej „agresywne” outliery
axes[1].boxplot(times, whis=1.0)
axes[1].set_title("whis = 1.0")

# 3) wąsy na min–max
axes[2].boxplot(times, whis=[0, 100])
axes[2].set_title("whis = [0, 100] (min–max)")

for ax in axes:
    ax.set_ylabel("Czas [ms]")

plt.tight_layout()
plt.show()

Na drugim wykresie więcej punktów wyląduje jako „outliery”, bo granice wąsów będą bliżej pudełka. Trzeci wykres pokaże pełen zakres danych w wąsach, przez co outliery znikną. Która wersja lepiej służy Twojemu pytaniu biznesowemu lub badawczemu?

Ręczne obliczanie mediany i IQR dla kontroli

Żeby nie ufać ślepo wykresowi, dobrze jest raz na jakiś czas policzyć medianę i IQR ręcznie lub NumPy i porównać z tym, co widzisz na boxplocie. Masz już pod ręką NumPy?

import numpy as np

times_arr = np.array(times)

q1 = np.percentile(times_arr, 25)
median = np.percentile(times_arr, 50)
q3 = np.percentile(times_arr, 75)
iqr = q3 - q1

print(f"Q1 = {q1:.1f}, mediana = {median:.1f}, Q3 = {q3:.1f}, IQR = {iqr:.1f}")

Dzięki temu możesz np. sprawdzić, czy linia mediany na wykresie faktycznie zgadza się z wyświetloną wartością. Przy debugowaniu dziwnych wyników (np. gdy część danych jest pusta lub zawiera NaN) taki krok bywa zbawienny.

Etykieta kategorii dla pojedynczego boxplota

Domyślny boxplot z Matplotlib numeruje kategorie od 1. Przy pojedynczym pudełku wygląda to mało mówiąco. Dusisz się w raportach opisem „grupa 1”? Dodaj jasną etykietę.

fig, ax = plt.subplots()
ax.boxplot(times)

ax.set_xticklabels(["Czas odpowiedzi"])  # pojedyncza etykieta kategorii
ax.set_ylabel("Czas [ms]")
ax.set_title("Czas odpowiedzi API – boxplot")

plt.show()

Przy większej liczbie grup to podejście rozszerzysz do listy etykiet, zsynchronizowanej z kolejnością danych przekazywanych do boxplot().

Boxplot dla wielu grup: porównywanie rozkładów w Matplotlib i Seaborn

Wiele pudełek obok siebie w Matplotlib

Częsta sytuacja: masz kilka wariantów eksperymentu, zespoły, wersje aplikacji i chcesz porównać rozkład wyników. Pytanie: porównujesz poziomy mediany, czy interesuje Cię też różnica w rozrzucie? Boxplot umożliwia jedno i drugie.

import numpy as np
import matplotlib.pyplot as plt

# przykładowe dane dla 3 wersji API
np.random.seed(0)
v1 = np.random.normal(loc=280, scale=20, size=50)
v2 = np.random.normal(loc=260, scale=25, size=50)
v3 = np.random.normal(loc=300, scale=30, size=50)

data = [v1, v2, v3]
labels = ["v1.0", "v1.1", "v2.0"]

fig, ax = plt.subplots()
ax.boxplot(data, labels=labels, showmeans=False)

ax.set_ylabel("Czas [ms]")
ax.set_title("Porównanie rozkładu czasu odpowiedzi dla wersji API")

plt.show()

Od razu widzisz, która wersja ma niższą medianę, która jest bardziej zmienna, a gdzie pojawia się więcej outlierów. Zastanów się: który aspekt jest kluczowy dla Twojej decyzji – poziom, czy stabilność?

Dodanie średniej do boxplota

Czasem potrzebujesz jednocześnie mediany (odpornej na outliery) i średniej (używanej w innych raportach). Matplotlib pozwala dodać marker średniej do pudełka.

fig, ax = plt.subplots()
ax.boxplot(
    data,
    labels=labels,
    showmeans=True,
    meanline=False,      # False - punkt, True - linia
    meanprops={
        "marker": "D",
        "markerfacecolor": "red",
        "markeredgecolor": "black",
        "markersize": 6,
    },
)

ax.set_ylabel("Czas [ms]")
ax.set_title("Mediana (linia) i średnia (czerwony romb)")

plt.show()

Różnica między średnią a medianą bywa kluczowa przy danych z długim ogonem. Jeśli romb znacząco „odjeżdża” od linii mediany, zadaj sobie pytanie: co ciągnie średnią – pojedyncze bardzo duże wartości, czy ogólnie asymetryczny rozkład?

Boxploty z DataFrame w Seaborn – lżejsza składnia

Gdy pracujesz na pandas.DataFrame, Seaborn oferuje wygodniejszą składnię, szczególnie przy wielu grupach i zmiennych kategorycznych. Masz już dane w formacie tabelarycznym?

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# budujemy DataFrame z poprzednich przykładowych danych
df = pd.DataFrame({
    "time": np.concatenate([v1, v2, v3]),
    "version": (["v1.0"] * len(v1) +
                ["v1.1"] * len(v2) +
                ["v2.0"] * len(v3))
})

fig, ax = plt.subplots(figsize=(7, 4))
sns.boxplot(
    data=df,
    x="version",
    y="time",
    ax=ax
)

ax.set_xlabel("Wersja API")
ax.set_ylabel("Czas odpowiedzi [ms]")
ax.set_title("Czas odpowiedzi według wersji (Seaborn boxplot)")

plt.show()

Seaborn sam zadba o rozmieszczenie pudełek i etykiety osi X. Możesz skupić się na logice danych i opisie wyników, zamiast na ręcznym ustawianiu etykiet.

Porównanie wielu grup w dwóch wymiarach (hue)

Drugi częsty scenariusz: porównujesz nie tylko wersję systemu, ale też np. środowisko (testowe vs produkcyjne), typ użytkownika, kraj. Wtedy dochodzi kolejna kategoria – czy chcesz rozdzielić grupy kolorami?

# dodajmy kolejną kolumnę - np. środowisko
env = (["test"] * (len(v1) // 2) + ["prod"] * (len(v1) - len(v1) // 2) +
       ["test"] * (len(v2) // 2) + ["prod"] * (len(v2) - len(v2) // 2) +
       ["test"] * (len(v3) // 2) + ["prod"] * (len(v3) - len(v3) // 2))

df["env"] = env

fig, ax = plt.subplots(figsize=(8, 4))
sns.boxplot(
    data=df,
    x="version",
    y="time",
    hue="env",        # dodatkowy podział na środowisko
    ax=ax
)

ax.set_xlabel("Wersja API")
ax.set_ylabel("Czas odpowiedzi [ms]")
ax.set_title("Czas odpowiedzi według wersji i środowiska")
ax.legend(title="Środowisko")

plt.show()

Takie zestawienie szybko ujawnia, czy np. w środowisku produkcyjnym wariancja jest większa niż w testowym, mimo podobnej mediany. Pytanie kontrolne: czy Twoje dane też można naturalnie rozbić na taki drugi wymiar?

Logarytmiczna skala osi – gdy rozkład jest bardzo skośny

Przy mocno skośnych rozkładach (np. czasach odpowiedzi, wielkości transakcji) różnice między medianą a długim ogonem są ogromne. Na zwykłej skali pudełko może wyglądać „ściśnięte” przy osi, a outliery dominują wykres. Czy miałeś już sytuację, że pudełko było nieczytelne, bo wąsy sięgały bardzo daleko?

fig, ax = plt.subplots(figsize=(7, 4))
sns.boxplot(data=df, x="version", y="time", ax=ax)
ax.set_yscale("log")  # logarytmiczna skala osi Y

ax.set_ylabel("Czas odpowiedzi [ms] (skala log)")
ax.set_title("Boxplot w skali logarytmicznej")

plt.show()

Taka transformacja nie zmienia relacji między wartościami, ale pozwala lepiej zobaczyć strukturę rozkładu w całym zakresie – środek i ogon jednocześnie.

Zbliżenie kolorowego kodu Pythona na ekranie monitora
Źródło: Pexels | Autor: Myburgh Roux

Jak poprawnie opisywać medianę i rozstęp na wykresie

Automatyczne etykiety z medianą i IQR na Matplotlib

Często sama linia mediany i wysokość pudełka nie wystarczają. Odbiorcy raportu chcą konkretnych liczb: „ile dokładnie wynosi mediana?”, „jaki jest IQR?”. Zamiast tłumaczyć werbalnie, możesz te wartości umieścić bezpośrednio na wykresie.

Dodanie etykiet mediany i IQR na pojedynczym boxplocie

Najprostszy scenariusz: jedno pudełko, chcesz jasno napisać, gdzie jest mediana i jaki jest rozstęp międzykwartylowy. Zastanów się: czy Twoi odbiorcy są w stanie „czytać” boxplot wizualnie, czy potrzebują liczby obok?

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)
times = np.random.gamma(shape=2.0, scale=80, size=120)

fig, ax = plt.subplots(figsize=(5, 4))
box = ax.boxplot(times, vert=True, patch_artist=True)

# wyciągnięcie współrzędnych z boxplota
# box["boxes"][0] - samo pudełko
# get_ydata() zwraca [Q1, Q3, Q3, Q1, Q1] (zamknięty kształt)
y_box = box["boxes"][0].get_ydata()
q1, q3 = y_box[0], y_box[2]
median = box["medians"][0].get_ydata()[0]
iqr = q3 - q1

# koordynaty X środka pudełka (indeks kategorii = 1)
x = box["medians"][0].get_xdata().mean()

# etykieta mediany
ax.text(
    x + 0.1, median,
    f"Mediana = {median:.1f} ms",
    va="center", ha="left",
    fontsize=9, color="black"
)

# etykieta IQR (pod pudełkiem)
ax.text(
    x + 0.1, q1 - 0.05 * (ax.get_ylim()[1] - ax.get_ylim()[0]),
    f"IQR = {iqr:.1f} ms",
    va="top", ha="left",
    fontsize=9, color="dimgray"
)

ax.set_xticklabels(["Czas odpowiedzi"])
ax.set_ylabel("Czas [ms]")
ax.set_title("Boxplot z podpisaną medianą i IQR")

plt.tight_layout()
plt.show()

Zwróć uwagę, że korzystasz z tego, co już policzył Matplotlib. Nie liczysz kwartylów drugi raz, tylko odczytujesz je z geometrii wykresu. Gdy następnym razem ktoś zakwestionuje liczby w raporcie, możesz jednym spojrzeniem porównać je z tym, co widnieje na pudełku.

Przy jednym boxplocie można sobie pozwolić na tekst obok. Przy dziesięciu już nie – wykres zmieni się w „choinkę”. Pytanie: potrzebujesz liczby dla każdej grupy, czy wystarczy tabela pod wykresem?

Etykiety median dla wielu pudełek – minimalizm zamiast ściany tekstu

Gdy porównujesz kilka wersji lub zespołów, zazwyczaj wystarczy pokazać mediany w postaci liczb nad każdym pudełkiem. Zastanów się, czy naprawdę potrzebujesz tam też IQR, czy wystarczy sam kształt pudełka.

fig, ax = plt.subplots(figsize=(7, 4))
box = ax.boxplot(data, labels=labels, patch_artist=True)

# odczytujemy mediany i ich pozycje X
for median_line in box["medians"]:
    x = median_line.get_xdata().mean()
    y = median_line.get_ydata()[0]
    ax.text(
        x, y,
        f"{y:.0f}",
        va="bottom", ha="center",
        fontsize=9, color="black",
        bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="none", alpha=0.7)
    )

ax.set_ylabel("Czas [ms]")
ax.set_title("Mediany podpisane na pudełkach")

plt.tight_layout()
plt.show()

Takie delikatne etykiety pozwalają szybko odczytać wartości bez zagłębiania się w legendy czy tabele. Dobrze się sprawdzają, gdy robisz przegląd kilku wariantów i chcesz od razu odpowiedzieć na pytanie: „o ile szybciej jest v1.1 względem v1.0?”.

Opis mediany i rozstępu w podpisie pod wykresem

Nie zawsze trzeba wszystko pakować na rysunek. Czasem lepiej ograniczyć się do prostego boxplota, a liczby przenieść do podpisu lub akapitu obok. Jaki masz cel: wizualne porównanie, czy dokumentacja konkretnej wartości?

Przykładowy schemat opisu:

  • Mediana: czas odpowiedzi wynosił X ms (Q1–Q3: Y–Z ms).
  • Rozstęp międzykwartylowy (IQR): różnica między Q3 a Q1 wynosiła W ms.

Taki opis jest zrozumiały nawet dla osób, które boxplot widzą rzadko. Zachęcaj odbiorców do zadawania pytań: „czy X to akceptowalny poziom mediany?”, „czy rozrzut (IQR) nie jest za duży jak na Wasz SLA?”.

Adnotacje z wartościami Q1 i Q3 – gdy ważna jest cała „skrzynka”

Bywają przypadki, gdy nie tylko mediana, ale też dokładne wartości Q1 i Q3 są istotne. Przykład: porównujesz czas odpowiedzi API z wymaganiami kontraktu, który definiuje „typowy zakres” reakcji.

fig, ax = plt.subplots(figsize=(6, 4))
box = ax.boxplot(times, vert=True, patch_artist=True)

y_box = box["boxes"][0].get_ydata()
q1, q3 = y_box[0], y_box[2]
median = box["medians"][0].get_ydata()[0]
x = box["medians"][0].get_xdata().mean()

# linie pomocnicze dla Q1 i Q3
ax.hlines([q1, q3], x - 0.2, x + 0.2, colors="gray", linestyles="dotted")

# opisy Q1 i Q3
ax.text(x + 0.25, q1, f"Q1 = {q1:.1f}", va="center", ha="left", fontsize=8)
ax.text(x + 0.25, q3, f"Q3 = {q3:.1f}", va="center", ha="left", fontsize=8)

# opis IQR jako zakresu
ax.text(
    x + 0.25, (q1 + q3) / 2,
    f"IQR = {q3 - q1:.1f}",
    va="center", ha="left", fontsize=8, color="dimgray"
)

ax.set_xticklabels(["Czas odpowiedzi"])
ax.set_ylabel("Czas [ms]")
ax.set_title("Boxplot z opisanymi Q1, medianą i Q3")

plt.tight_layout()
plt.show()

Taki wykres może być już „gęsty”, ale w raportach technicznych bywa bardzo użyteczny. Pytanie pomocnicze: czy czytelnik naprawdę musi znać Q1 i Q3 z dokładnością do jednej dziesiątej, czy wystarczy zaokrąglenie?

Spójne jednostki i opisy osi – typowe wpadki

Najczęstsze błędy przy opisywaniu mediany i rozstępu wynikają z chaosu w jednostkach lub skrótach. Widziałeś kiedyś wykres, gdzie oś była w sekundach, a podpis mediany w milisekundach?

  • Ustal jedną jednostkę (np. ms, zł, dni) i stosuj ją konsekwentnie w osi, tytule i etykietach.
  • Jeśli zmieniasz jednostkę (np. ms → s), zrób to jawnie: Czas [s] i przeskaluj dane przed rysowaniem.
  • Nazwy typu „czas”, „wartość” uzupełnij kontekstem: „Czas odpowiedzi [ms]”, „Kwota transakcji [PLN]”.

Zanim pokażesz wykres innym, zadaj sobie pytanie kontrolne: czy ktoś, kto nie zna Twojego kodu, odczyta poprawnie, co oznacza mediana 250?

Wartości odstające na boxplocie – definicja operacyjna

Wykres pudełkowy ma konkretną „definicję operacyjną” wartości odstających: to punkty leżące poza wąsami, które zwykle kończą się na Q1 - 1.5 * IQR i Q3 + 1.5 * IQR. Czy to oznacza, że wszystkie te punkty są błędne? Niekoniecznie.

Schemat Tukeya używany w boxplocie to kompromis: identyfikuje obserwacje, które są „nietypowe” względem reszty, ale nie mówi, czy należy je usuwać. To Twoje zadanie: ocenić, czy to błąd pomiaru, czy interesujące zjawisko.

Jak Matplotlib i Seaborn oznaczają outliery

W Matplotlib domyślne markery wartości odstających to kółka (fliers), w Seabornie wyglądają podobnie, ale styl zależy od motywu.

fig, ax = plt.subplots(figsize=(6, 4))
ax.boxplot(times, showfliers=True)  # domyślnie True

ax.set_ylabel("Czas [ms]")
ax.set_title("Boxplot z wartościami odstającymi")

plt.tight_layout()
plt.show()

Jeśli chcesz odróżnić outliery wizualnie od reszty (np. mocniejszym kolorem), możesz użyć flierprops:

fig, ax = plt.subplots(figsize=(6, 4))
ax.boxplot(
    times,
    showfliers=True,
    flierprops={
        "marker": "o",
        "markerfacecolor": "red",
        "markeredgecolor": "black",
        "markersize": 5,
        "alpha": 0.7,
    }
)

ax.set_ylabel("Czas [ms]")
ax.set_title("Outliery wyróżnione na czerwono")

plt.tight_layout()
plt.show()

Zastanów się: czy odbiorcy rozumieją, że czerwone punkty to obserwacje ekstremalne, a nie „błędy systemu”? Jeśli nie, dopisz krótkie wyjaśnienie w legendzie lub podpisie.

Zmiana kryterium outlierów – gdy 1.5 * IQR to za mało lub za dużo

Parametr whis kontroluje, jak daleko sięgają wąsy, a tym samym, które punkty uznasz za odstające. Jeśli Twoje dane naturalnie mają długie ogony (np. czasy odpowiedzi przy rzadkich przeciążeniach), klasyczne 1.5 * IQR może oznaczać „za dużo czerwieni” na wykresie.

Kilka wariantów:

  • whis=2.0 – łagodniejsze kryterium, mniej punktów jako outliery.
  • whis=[5, 95] – wąsy na wybranych percentylach (5–95%).
  • whis=[0, 100] – wąsy od minimum do maksimum (brak outlierów).

Jakie masz dane teraz? Czy naturalne jest, że 5–10% wartości jest bardzo wysokich, czy raczej to sygnał błędu lub incydentu?

Ukrywanie wartości odstających – kiedy to ma sens

Czasem outliery dominują wykres i utrudniają ocenę „typowego” rozkładu. Wtedy możesz je chwilowo ukryć, ale pamiętaj, że to decyzja komunikacyjna, a nie czysto techniczna.

fig, ax = plt.subplots(figsize=(6, 4))
ax.boxplot(times, showfliers=False)  # ukryj punkty odstające

ax.set_ylabel("Czas [ms]")
ax.set_title("Boxplot bez rysowania wartości odstających")

plt.tight_layout()
plt.show()

Zwróć uwagę: wartości nadal wpływają na medianę i kwartyle, po prostu nie są oznaczone osobnymi punktami. Jeżeli outliery są kluczowe (np. SLA mówi o maksymalnym czasie odpowiedzi), lepiej pokazać je osobno, na osobnym wykresie lub jako tekst: „Najdłuższy czas odpowiedzi: … ms”.

Identyfikacja konkretnych punktów odstających – most do debugowania

Jeśli widzisz na boxplocie kilka ekstremalnych punktów, naturalne pytanie brzmi: które to dokładnie obserwacje? Sam wykres tego nie powie, ale bez trudu połączysz go z prostym filtrowaniem w Pandas.

import pandas as pd

df_times = pd.DataFrame({"time": times})
q1 = df_times["time"].quantile(0.25)
q3 = df_times["time"].quantile(0.75)
iqr = q3 - q1

lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr

outliers = df_times[(df_times["time"] < lower) | (df_times["time"] > upper)]
print(outliers.head())

Teraz możesz sprawdzić w logach, co działo się w tych momentach, albo połączyć te rekordy z innymi kolumnami (np. identyfikatorem użytkownika, wersją systemu). To często pierwszy krok do sensownej decyzji: „czy usuwać te punkty z analizy, czy raczej naprawić przyczynę?”.

Dodanie opisu liczby outlierów do wykresu

Nie zawsze musisz zaznaczać każdy outlier osobno. Czasem wystarczy informacja typu: „5 punktów powyżej górnej granicy, 2 poniżej dolnej”. To szczególnie wygodne przy bardzo dużych zbiorach danych.

n_upper = (df_times["time"] > upper).sum()
n_lower = (df_times["time"] < lower).sum()

fig, ax = plt.subplots(figsize=(6, 4))
ax.boxplot(times, showfliers=False)

ax.set_ylabel("Czas [ms]")
ax.set_title("Boxplot z liczbą wartości odstających")

text = f"Outliery: {n_lower} poniżej, {n_upper} powyżej granic Tukeya"
ax.text(
    1, ax.get_ylim()[1],
    text,
    ha="center", va="top",
    fontsize=9,
    bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="gray", alpha=0.8)
)

plt.tight_layout()
plt.show()

Zastanów się, który wariant lepiej służy Twojemu celowi: pokazanie każdego punktu, czy tylko informacji zbiorczej? Przy dashboardach dla menedżerów drugi wariant zwykle jest czytelniejszy.

Outliery a skala logarytmiczna – pułapki interpretacyjne

Gdy przechodzisz na skalę logarytmiczną, odległości na osi przestają być liniowe. Linie Q1 ± 1.5 * IQR nadal liczone są w skali oryginalnej, ale widzisz je po transformacji logarytmicznej. Dla części odbiorców to może być mylące.

Dobrą praktyką jest wtedy:

  • wyraźnie napisać w tytule/osi: „skala logarytmiczna”,
  • Kluczowe Wnioski

  • Jeśli kilka ekstremalnych obserwacji może mocno wypaczyć średnią (np. pojedynczy bardzo bogaty pracownik, kilka skrajnie wolnych odpowiedzi serwera), lepszym streszczeniem danych jest mediana i rozstęp, a nie średnia – wtedy boxplot staje się podstawowym narzędziem.
  • Wykres pudełkowy skupia się na medianie, kwartylach i rozstępie (IQR), czyli statystykach odpornych na wartości skrajne; dzięki temu pokazuje „typowe doświadczenie” oraz rozproszenie i asymetrię rozkładu w znacznie bardziej wiarygodny sposób niż pojedyncza średnia.
  • Gdy chcesz zrozumieć szczegółowy kształt jednego rozkładu, wybierasz histogram; gdy porównujesz agregaty między kategoriami – wykres słupkowy; gdy Twoim celem jest porównanie wielu rozkładów pod kątem mediany, rozrzutu i outlierów jednym rzutem oka, wybierasz boxplot.
  • W projektach z wieloma kategoriami (grupy eksperymentalne vs kontrolne, warianty A/B, klasy szkolne, różne serwery czy regiony) szereg pudełek obok siebie natychmiast pokazuje, która grupa ma wyższą medianę, większą zmienność lub nietypowo dużo skrajnych wyników – jakie grupy chcesz właśnie porównać?
  • Dla pojedynczej zmiennej boxplot działa jak szybki „skaner”: wskazuje poziom mediany, szerokość pudełka (IQR) oraz ewentualne wartości odstające; gdy tylko pojawia się naturalny podział na grupy, ta wizualizacja gwałtownie zyskuje na użyteczności.

1 KOMENTARZ

  1. Bardzo wartościowe artykuł, który szczegółowo opisuje, jak stworzyć wykres pudełkowy w Pythonie oraz jak interpretować medianę i rozstęp danych. Bardzo przydatne informacje dla osób, które chcą zgłębić temat analizy danych i wizualizacji. Cieszę się, że autor omówił krok po kroku proces tworzenia wykresu pudełkowego, co sprawia, że nawet początkujący będą mogli z łatwością zastosować te techniki. Jednakże brakuje mi odniesienia do interpretacji wartości odstających oraz możliwości zastosowania wykresu pudełkowego w konkretnych analizach danych. Pomimo tego, artykuł jest bardzo pomocny i zdecydowanie warto go przeczytać dla poszerzenia wiedzy na temat wizualizacji danych w Pythonie.

Komentarze są aktywne tylko po zalogowaniu.