Po co w ogóle zajmować się obserwacjami odstającymi? Kontekst biznesowy i analityczny
Błąd danych kontra rzadkie, ale prawdziwe zdarzenie
Obserwacje odstające w Pythonie to nie tylko problem statystyczny, ale przede wszystkim decyzja biznesowa. Ten sam ekstremalny punkt w danych może być jednocześnie krytycznym błędem pomiaru albo bezcenną informacją o rzadkim zdarzeniu. Automatyczne „czyszczenie” wszystkiego, co odstaje od reszty, bywa wygodne, ale potrafi zniszczyć dokładnie te informacje, które są najbardziej wartościowe.
Typowy przykład to dane transakcyjne. Jedna ogromna transakcja może być:
- efektem błędu systemu (np. duplikacja, błąd waluty, pomylona liczba zer),
- fraudem, który trzeba wykryć i przeanalizować, a nie wyrzucić,
- efektem rzadkiej, ale legalnej kampanii sprzedażowej lub kluczowego klienta.
W danych z sensorów IoT pojedynczy skok wartości może oznaczać:
- glitch urządzenia (chwilowe zakłócenie, przerwa w zasilaniu),
- fizyczne zjawisko (nagły skok ciśnienia, temperatury),
- początek awarii, który dopiero się rozwija.
Usunięcie takiej obserwacji „bo odstaje” sprawia, że model predykcyjny traci sygnał o tym, co go najbardziej interesuje – przyszłych awariach.
Jak outliery wypaczają statystyki opisowe i modele
Ekstremalne punkty mają ogromny wpływ na klasyczne metryki. Średnia i odchylenie standardowe są bardzo czułe na outliery. Wystarczy kilka skrajnych wartości, by średnia przestała dobrze reprezentować „typowego” klienta, transakcję czy pomiar. Z kolei korelacje potrafią się odwrócić, jeśli kilka punktów o dużej wartości danej cechy leży „po złej stronie” wykresu.
W modelach regresyjnych outliery często:
- przesuwają linię regresji w stronę ekstremów, przez co model gorzej opisuje 90–95% typowych obserwacji,
- powodują duże residua, co z kolei wpływa na estymację parametrów i przedziały ufności,
- łamią założenia o rozkładzie błędów (np. normalność, homoscedastyczność).
W modelach klasyfikacyjnych sytuacja jest inna, ale też problematyczna – pojedyncze ekstremalne punkty w przestrzeni cech potrafią zaburzyć granicę decyzyjną, zwłaszcza w modelach opartych na odległości, jak kNN.
Na poziomie inżynierii cech outliery wpływają również na skalowanie. Przy użyciu standardowego skalowania (StandardScaler) kilka skrajnych wartości może „ściśnąć” resztę punktów do bardzo małego zakresu, utrudniając trenowanie modeli, zwłaszcza sieci neuronowych.
Praktyczne przykłady z analizy sprzedaży, IoT i finansów
W analizie sprzedaży outliery to najczęściej:
- dni z wybuchową sprzedażą (Black Friday, kampanie, wyprzedaże),
- błędy integracji (podwójne załadowanie danych, złe kursy walut),
- zamówienia specjalne (np. duży kontrakt B2B w sklepie B2C).
Z perspektywy modelowania prognoz sprzedaży, wyrzucenie dni kampanii może poprawić dopasowanie w dniach „normalnych”, ale zniszczy zdolność modelu do prognozowania okresów promocyjnych. Dobrym kompromisem bywa oznaczenie tych dni flagą i budowa modeli z osobną obsługą sezonowości promocyjnej.
W danych z sensorów (IoT, monitoring maszyn) outliery są często kluczem do predictive maintenance. Skok temperatury, drgań czy ciśnienia przed awarią jest dokładnie tym, czego system ma się nauczyć. Agresywne czyszczenie odstających punktów usuwa sygnał przejściowy, pozostawiając tylko „zdrowe” okresy działania.
W finansach outliery mogą oznaczać:
- fraud (podejrzane przelewy, niecodzienne wzorce płatności),
- nagłe zmiany rynku (krach, paniczna sprzedaż),
- zwykłą, dużą transakcję instytucjonalną.
Usunięcie punktów o najwyższych stratach czy zyskach sprawia, że model ryzyka jest sztucznie „wygładzony” i nie oddaje prawdziwego ogona rozkładu strat.
Kiedy usunięcie odstającej obserwacji to błąd biznesowy
Usuwanie obserwacji odstających ma sens głównie wtedy, gdy:
- istnieje silne podejrzenie błędu danych (np. techniczny limit niemożliwy do przekroczenia został przekroczony),
- ekstremalne wartości są wynikiem znanych błędów systemu, które nie powtórzą się w produkcji,
- model ma opisywać tylko „typowy” przypadek, a rzadkie zdarzenia będą obsługiwane innymi procesami (np. ręczna weryfikacja).
Zła praktyka to mechaniczne stosowanie reguł typu „odetnij 1% skrajnych wartości”. W danych z frauda, awarii, promocji czy kryzysów taki zabieg może wyrzucić dokładnie to, co jest najcenniejsze dla biznesu. Outliery warto więc:
- najpierw zidentyfikować i zrozumieć,
- przeklasyfikować na kategorie: błąd, zdarzenie jednorazowe, nowy wzorzec,
- dopiero potem zdecydować: usuwać, flagować, osobno modelować.
Dobry workflow analizy danych w Pythonie skupia się nie na samym kasowaniu, ale na mądrym zarządzaniu odstającymi punktami.
Definicje i typy obserwacji odstających: nie każdy ekstremum to problem
Outlier statystyczny, anomalia biznesowa, leverage i influential point
W statystyce słowo outlier ma precyzyjne znaczenie: to punkt znacznie odbiegający od większości danych, zwykle definiowany na podstawie konkretnej reguły (np. powyżej 3 odchyleń standardowych, poza zakresem IQR ± 1.5×IQR). Taka definicja jest wygodna, ale nie obejmuje kontekstu biznesowego.
Anomalia biznesowa to obserwacja, która jest nietypowa z perspektywy procesów biznesowych, a niekoniecznie ekstremalna statystycznie. Przykładowo klient, który codziennie kupuje niewielkie ilości, a nagle przestaje – nie musi generować outlierów w kolumnach liczbowych, ale jest anomalią z punktu widzenia zachowania.
W regresji wyróżnia się:
- leverage points – obserwacje z ekstremalnymi wartościami zmiennych niezależnych (x), które mają duży wpływ na dopasowanie modelu, nawet jeśli ich residuum nie jest ogromne,
- influential points – obserwacje o dużym wpływie na parametry modelu (np. zmiana ich wartości usuwa lub znacząco zmienia współczynniki).
Outlier statystyczny nie musi być influential, a influential point nie zawsze jest ekstremalny w oczywisty sposób. To ważne rozróżnienie, bo decyzja „usuwać / nie usuwać” powinna brać pod uwagę wpływ na model.
Outliery globalne, lokalne i kontekstowe
W praktyce obserwacje odstające w Pythonie często dzieli się na:
- outliery globalne – punkty wyraźnie oddzielone od reszty w całym zbiorze (np. ktoś wydał wielokrotnie więcej niż wszyscy pozostali),
- outliery lokalne – punkty odstające tylko względem swojego „lokalnego sąsiedztwa” w przestrzeni cech,
- outliery kontekstowe – wartości nietypowe tylko w pewnym kontekście, np. w danym czasie, segmencie, warunkach.
Outlier globalny bywa stosunkowo łatwy do wykrycia prostymi narzędziami (IQR, Z-score). Lokalny i kontekstowy wymagają metod opartych na gęstości, odległości lub modelach uczonych na jednym kontekście.
Przykład kontekstowy: sprzedaż 1000 sztuk produktu w ciągu dnia:
- w małym sklepie stacjonarnym to ekstremum i prawdopodobny błąd,
- w dużym e-commerce w czasie Black Friday – absolutna norma.
Ten sam punkt, ta sama wartość, ale inny kontekst (typ sklepu, dzień w roku) zmienia klasyfikację.
Jak rozmawiać o outlierach z decydentami
Osoby biznesowe rzadko interesują się Z-score czy IQR. Zamiast mówić „ta obserwacja przekracza 3 sigma”, lepiej używać prostych porównań:
- „Ta transakcja jest większa niż 99.9% wszystkich transakcji w ostatnich dwóch latach”.
- „Ten odczyt sensora jest poza zakresem, który kiedykolwiek obserwowaliśmy w normalnej pracy maszyny”.
- „Tylko 5 klientów z 50 000 zachowuje się w ten sposób – to naprawdę nietypowe”.
Decydentów interesuje zwykle:
- czy to błąd, czy ciekawe zdarzenie,
- czy to się może powtarzać,
- jak to wpływa na decyzje (limity, prognozy, budżet, ryzyko).
Warto pokazać wykresy: boxplot, histogram, wykres czasowy z zaznaczonymi anomaliami. Krótka wizualizacja robi więcej niż szczegółowe wywody statystyczne.
Kiedy „największa wartość” nie jest odstająca
Największa albo najmniejsza wartość w kolumnie często bywa z automatu oznaczana jako podejrzana. To uproszczenie. Przykładowo:
- dziesięć największych zamówień B2B w roku może być normalnym efektem kontraktów korporacyjnych,
- najwyższa cena akcji w historii spółki może odpowiadać realnemu szczytowi rynku,
- maksymalne obciążenie serwera w dniu dużej konferencji online jest całkowicie spodziewane.
Problem pojawia się wtedy, gdy największa wartość jest:
- nielogiczna w kontekście (np. temperatura > techniczny limit sensora),
- niespójna z innymi polami (np. brak waluty, ale bardzo duża kwota),
- odstająca tylko w jednej grupie (np. region, branża).
Dobre podejście: zanim cokolwiek usuniesz, zadaj pytanie: „czy w tej domenie taka wartość jest możliwa?”. Odpowiedź powinna przyjść od eksperta domenowego, nie tylko z wykresu.
Przygotowanie środowiska i danych w Pythonie: baza do dalszej pracy
Import bibliotek: pandas, numpy, matplotlib, seaborn, scikit-learn, scipy
Workflow analizy danych w Pythonie zwykle opiera się na kilku bibliotekach, które wygodnie połączyć w jeden zestaw narzędzi:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
from sklearn.preprocessing import StandardScaler, RobustScaler, QuantileTransformer
Seaborn i matplotlib posłużą do wizualizacji outlierów, pandas do ich filtrowania, a scikit-learn do bardziej zaawansowanych metod detekcji anomalii, takich jak Isolation Forest czy Local Outlier Factor.
Wczytywanie realnych danych i szybkie sanity checki
Dane rzadko przychodzą w idealnym formacie. Na początek podstawowy schemat wczytywania (CSV jako najczęstszy przypadek):
df = pd.read_csv("transakcje.csv")
df.info()
df.head()
df.describe(include="all")
Przy pierwszym kontakcie z danymi warto przejść szybką checklistę:
- czy typy danych są sensowne (daty jako datetime, liczby jako numeryczne, kody jako stringi),
- czy są oczywiste błędy: ujemny wiek, wartości poza fizycznymi limitami, błędne kody krajów,
- czy rozkłady są choć trochę rozsądne (bez gigantycznych dziur, tysiącami zer w kwotach, itp.),
- czy występują ogromne liczby braków (NaN) w kluczowych kolumnach.
Przykład konwersji i prostego sanity checku:
df["data_transakcji"] = pd.to_datetime(df["data_transakcji"], errors="coerce")
df["kwota"] = pd.to_numeric(df["kwota"], errors="coerce")
# szybka kontrola zakresu dla kolumny "wiek"
wiek_min, wiek_max = df["wiek"].min(), df["wiek"].max()
print(wiek_min, wiek_max)
Jeśli w badanym zbiorze wiek = -5 lub 500, to nie jest outlier statystyczny, tylko błąd danych do naprawy lub usunięcia przed jakąkolwiek analizą outlierów.
Wstępne czyszczenie: brakujące wartości i oczywiste błędy
Przed właściwym wykrywaniem outlierów trzeba rozstrzygnąć kilka kwestii porządkowych:
- jak obchodzić się z brakami (usuwanie wierszy, uzupełnianie, specjalne kategorie),
- czy istnieją oczywiste wartości niemożliwe (np. w e-commerce: ujemna cena brutto, więcej produktów sprzedanych niż było w magazynie),
- czy dane zawierają duplicate’y, które należy skonsolidować.
Przykład usuwania ewidentnych błędów z kolumny „wiek” i „kwota”:
df = df[(df["wiek"] >= 0) & (df["wiek"] <= 110)]
df = df[df["kwota"] >= 0]
Takie restrykcje powinny wynikać z logiki biznesowej lub fizycznej – nie z tego, że wartość „wygląda podejrzanie wysoka”. To etap eliminacji danych ewidentnie błędnych, nie statystycznie odstających.
Przykładowe zbiory danych jako przewodnik
Dalej przydadzą się dwa typowe, realistyczne zbiory, na których da się pokazać wykrywanie outlierów w Pythonie:
Symulowany e-commerce i dane sensorowe – dwa różne światy outlierów
Dane transakcyjne i pomiary z sensorów generują zupełnie inne typy odstępstw. Dobrze mieć pod ręką oba światy, żeby nie wyciągać wniosków na podstawie jednego, „wygodnego” przypadku.
Przykładowy zbiór transakcyjny można zbudować samodzielnie, ale znacznie lepiej oprzeć się na czymś zbliżonym do realnego świata, np. danych zamówień, logach płatności, historii faktur. Jeśli to niemożliwe (RODO, tajemnica przedsiębiorstwa), wystarczy zanonimizować lub zaszumieć część pól, zachowując strukturę rozkładów.
Druga kategoria to dane czasowe z sensorów: odczyty temperatury, wibracji, napięcia, liczników zdarzeń. Tam obserwacje odstające są często pojedynczymi „szpilkami” lub nagłymi skokami, a nie gigantycznymi kwotami. Statystyka wygląda inaczej, ale zasada jest ta sama: najpierw kontekst, potem liczby.
Minimalny szkic przygotowania dwóch ramek danych może wyglądać tak:
# transakcje
df_tr = pd.read_csv("transakcje.csv")
df_tr["data_transakcji"] = pd.to_datetime(df_tr["data_transakcji"], errors="coerce")
# sensory
df_sens = pd.read_csv("odczyty_sensorow.csv")
df_sens["timestamp"] = pd.to_datetime(df_sens["timestamp"], errors="coerce")
df_tr = df_tr.dropna(subset=["data_transakcji", "kwota"])
df_sens = df_sens.dropna(subset=["timestamp", "wartosc"])
W obu przypadkach wykrywanie outlierów będzie korzystało z podobnych narzędzi, ale logika decyzji „co dalej z tym punktem” będzie zupełnie inna. Nadmiarowy pik temperatury możesz zignorować lub „przygładzić”, ale pojedyncza gigantyczna transakcja to czasem właśnie to, co ma największą wartość biznesową.

Proste metody wykrywania outlierów: IQR, Z-score i wizualizacje
Boxplot i IQR: klasyka, która bywa nadużywana
Najpopularniejsza rada: „zrób boxplot, usuń wartości poza wąsami (1.5×IQR)”. Działa to tylko przy umiarkowanie skośnych rozkładach i gdy obserwacji faktycznie jest dużo. W danych sprzedażowych, gdzie większość klientów kupuje mało, a nieliczni kupują bardzo dużo, ta metoda oznaczałaby usunięcie właśnie najcenniejszych klientów.
Podstawowa implementacja IQR w pandas:
def flag_iqr_outliers(series, factor=1.5):
q1 = series.quantile(0.25)
q3 = series.quantile(0.75)
iqr = q3 - q1
lower = q1 - factor * iqr
upper = q3 + factor * iqr
return (series < lower) | (series > upper)
mask_outliers_kwota = flag_iqr_outliers(df_tr["kwota"])
df_tr.loc[mask_outliers_kwota, ["kwota"]].head()
Lepsze wykorzystanie IQR w realnych danych to nie „hurtowe kasowanie”, tylko:
- oznaczenie outlierów w ramce (np. nowa kolumna
is_outlier_iqr), - wykorzystanie tego znaczka do dodatkowej analizy (segmentacja, sprawdzenie źródeł błędów),
- warunkowe filtrowanie tylko w określonych modelach, które są wrażliwe na ekstremalne wartości.
Dodanie flagi w danych transakcyjnych:
df_tr["is_outlier_iqr_kwota"] = flag_iqr_outliers(df_tr["kwota"])
df_tr["is_outlier_iqr_kwota"].value_counts(normalize=True)
Z-score: kłopot przy rozkładach skośnych
Z-score (standaryzacja względem średniej i odchylenia standardowego) bywa proponowany jako uniwersalny detektor outlierów: „oznacz wszystko powyżej |Z| > 3”. W danych z długim ogonem (np. przychody, czas sesji, liczba transakcji na klienta) ta rada psuje obraz – usuwa wszystkie naturalne duże wartości, bo średnia i odchylenie standardowe są przez nie mocno przesunięte.
df_tr["kwota_z"] = stats.zscore(df_tr["kwota"], nan_policy="omit")
df_tr["is_outlier_z_kwota"] = df_tr["kwota_z"].abs() > 3
Z-score ma sens, gdy:
- dane są w przybliżeniu normalne (lub po sensownej transformacji, np. logarytmicznej),
- nie ma ekstremalnie długiego ogona,
- potrzebna jest szybka, orientacyjna diagnostyka w małym podzbiorze.
Alternatywa przy rozkładach silnie skośnych: zastosowanie transformacji, a dopiero potem prostych reguł progowych. Minimalny przykład dla kwoty:
df_tr["kwota_log"] = np.log1p(df_tr["kwota"])
df_tr["kwota_log_z"] = stats.zscore(df_tr["kwota_log"], nan_policy="omit")
df_tr["is_outlier_logz_kwota"] = df_tr["kwota_log_z"].abs() > 3
Po transformacji logarytmicznej outliery często stają się „mniej spektakularne”, ale bardziej sensownie porównywalne między sobą.
Histogramy i wykresy pudełkowe jako detektor zdrowego rozsądku
Prosta wizualizacja potrafi zdemaskować dziwne rekomendacje automatycznych metod. Warto zestawić obok siebie histogram / kernel density estimate i boxplot/wąsy IQR.
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
sns.histplot(df_tr["kwota"], bins=50, ax=axes[0])
axes[0].set_title("Rozkład kwoty (brutto)")
sns.boxplot(x=df_tr["kwota"], ax=axes[1])
axes[1].set_title("Boxplot kwoty")
plt.tight_layout()
plt.show()
Jeśli połowa danych wpada w „outliery” na boxplocie, problem tkwi nie w danych, ale w ślepej interpretacji reguły 1.5×IQR. Wtedy lepszym wyborem jest:
- transformacja (log, Box-Cox, Yeo-Johnson),
- segmentacja (oddzielne outliery dla detalicznych i hurtowych klientów),
- odpuszczenie globalnych progów na rzecz metod lokalnych.
Outliery względem grup: segmentacja zamiast jednego progu
Dlaczego jeden próg dla wszystkich bywa pułapką
W e-commerce kwota zamówienia w Polsce i w USA może mieć różne skale. W danych sensorowych inny poziom „normalności” występuje dla każdej maszyny. Ustawianie jednego globalnego progu (np. kwota > 5000 = outlier) prowadzi do tego, że:
- w krajach o niższej sile nabywczej większość wysokich, ale realnych transakcji ląduje w koszu,
- w krajach bogatszych realne anomalie pozostają niewykryte, bo są niższe niż próg globalny.
Rozwiązanie jest oczywiste, ale czasochłonne: ocena odstępstw w ramach logicznych grup.
Grupowe IQR/Z-score w pandas
Typowy zabieg: dla każdej grupy (np. kraju, typu klienta, sklepu) liczone są osobne statystyki i na tej podstawie flagowane outliery.
def group_iqr_flags(df, value_col, group_cols, factor=1.5):
def _flag_group(g):
mask = flag_iqr_outliers(g[value_col], factor=factor)
return mask
return df.groupby(group_cols, group_keys=False).apply(_flag_group)
df_tr["is_outlier_iqr_kwota_kraj"] = group_iqr_flags(
df_tr,
value_col="kwota",
group_cols=["kraj"],
factor=1.5
)
Podobnie można podejść do Z-score, choć przy małych grupach (niskie liczebności) oszacowania średniej i odchylenia są niestabilne. W takich segmentach lepiej sięgnąć po bardziej „robust” miary, np. medianę i median absolute deviation (MAD):
def mad_based_flags(series, thresh=3.5):
median = series.median()
mad = (series - median).abs().median()
if mad == 0:
return pd.Series(False, index=series.index)
modified_z = 0.6745 * (series - median) / mad
return modified_z.abs() > thresh
df_tr["is_outlier_mad_kwota_kraj"] = (
df_tr.groupby("kraj", group_keys=False)["kwota"]
.apply(mad_based_flags)
)
Takie podejście lepiej znosi pojedyncze ekstremalne wartości i ma sens przy grupach o bardzo różnych skalach.
Segmentacja po czasie: sezonowość i trendy
W danych czasowych jedna z częstszych pułapek to traktowanie całej serii jak stacjonarnej. W praktyce:
- sprzedaż rośnie z roku na rok,
- czasy odpowiedzi serwerów zależą od pory dnia,
- temperatura maszyn zmienia się wraz z porą roku.
Zamiast jednego globalnego progu, sensownie jest wyznaczyć progi osobno dla okresów (np. miesiąc, kwartał, dzień tygodnia) lub w ruchomym oknie czasowym.
df_sens = df_sens.sort_values("timestamp")
df_sens["dzien"] = df_sens["timestamp"].dt.date
# progi IQR dla każdego dnia
daily_bounds = (
df_sens.groupby("dzien")["wartosc"]
.agg(["quantile"])
)
Prostsza i częściej stosowana opcja: obliczenia w ruchomym oknie:
df_sens = df_sens.sort_values("timestamp").reset_index(drop=True)
window = 100 # liczba ostatnich punktów w oknie
rolling = df_sens["wartosc"].rolling(window=window)
q1 = rolling.quantile(0.25)
q3 = rolling.quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
df_sens["is_outlier_iqr_rolling"] = (
(df_sens["wartosc"] < lower) | (df_sens["wartosc"] > upper)
)
Takie lokalne progi lepiej wychwytują nagłe zmiany w otoczeniu, niż jeden próg wyliczony z całej, wielomiesięcznej serii.
Skalowanie i transformacje: przygotowanie danych do metod wielowymiarowych
Kiedy skalowanie zmienia wszystko, a kiedy niewiele
Metody oparte na odległościach i gęstości (kNN, Local Outlier Factor, Isolation Forest w pewnych wariantach) są wrażliwe na skalę cech. Jeśli jedna zmienna ma zakres [0, 1], a druga [0, 100000], to euklidesowa odległość będzie prawie wyłącznie „mierzyć” drugą zmienną. Częsta zła rada: „użyj po prostu StandardScaler”.
StandardScaler normalizuje średnią i odchylenie. W obecności outlierów właśnie te statystyki są najbardziej zdeformowane. W efekcie ekstremalne wartości zostają „spłaszczone”, a reszta danych rozjechana. W wielu zadaniach detekcji anomalii lepiej zachowuje się RobustScaler lub transformacje kwantylowe.
RobustScaler i QuantileTransformer w praktyce
num_cols = ["kwota", "liczba_pozycji", "dni_od_rejestracji"]
scaler_robust = RobustScaler()
X_robust = scaler_robust.fit_transform(df_tr[num_cols])
qt = QuantileTransformer(output_distribution="normal", random_state=42)
X_qt = qt.fit_transform(df_tr[num_cols])
RobustScaler korzysta z mediany i IQR, dzięki czemu pojedyncze ekstremalne wartości nie dominują skali. QuantileTransformer próbuje dopasować rozkład każdej cechy do zadanej formy (np. normalnej), co potrafi okiełznać bardzo skośne dane. Dla outlierów ma to dwie konsekwencje:
- łatwiej zastosować metody zakładające „w miarę normalny” rozkład (np. Z-score po transformacji),
- jednocześnie „ściśnięcie ogonów” może utrudnić wykrycie najdalszych punktów metodom wrażliwym na odległość.
Narzędzie jest to samo, ale cel inny: czasem zależy na uwydatnieniu ogona (analiza ryzyka), czasem na jego przycięciu (trening wrażliwego modelu regresyjnego). Warto wyraźnie rozdzielić te dwa use-case’y i nie stosować jednego pipeline’u do wszystkiego.
Transformacje logarytmiczne i nieliniowe
Popularna rada „zlogarytmuj kwoty” często ma sens: rozkład z ogromnym ogonem staje się bardziej „cywilizowany”. Nie sprawdza się jednak, gdy wartości bywają równe zero lub bliskie zeru (prosty np.log odpada) albo gdy występują liczby ujemne.
Bezpieczniejszy wariant logarytmu przy dodatnich danych:
df_tr["kwota_log1p"] = np.log1p(df_tr["kwota"])
Dla danych, które mogą mieć zera lub wartości ujemne, można użyć transformacji Yeo-Johnson z sklearn:
from sklearn.preprocessing import PowerTransformer
pt = PowerTransformer(method="yeo-johnson", standardize=False)
df_tr["kwota_yj"] = pt.fit_transform(df_tr[["kwota"]])
Takie transformacje są szczególnie przydatne, gdy dalszym krokiem będzie regresja liniowa lub metody oparte na wariancji, które nie lubią ekstremalnej skośności.
Isolation Forest: kiedy las lepiej widzi pojedyncze drzewa
Intuicja metody i typowe nieporozumienia
Isolation Forest nie próbuje dopasować rozkładu danych; zamiast tego losowo tnie przestrzeń cech na podobszary. Punkty, które są „samotne” w swoich regionach, wymagają mniej podziałów do izolacji, więc dostają wyższy score anomalii. W przeciwieństwie do klasyfikatorów, nie trzeba mieć etykiet „normalny / anomalia”.
Typowe nieporozumienia:
- używanie domyślnego
contamination=0.1bez zastanowienia (oznacza to z góry założony 10% udział anomalii), - stosowanie na surowych, nieskalowanych danych o bardzo różnych jednostkach,
Konfiguracja Isolation Forest na danych transakcyjnych
W zastosowaniach biznesowych Isolation Forest często ląduje jako „czarna skrzynka do anomalii”. Da się z niego wycisnąć więcej, jeśli jawnie przełoży się parametry na język danych.
from sklearn.ensemble import IsolationForest
features = ["kwota_yj", "liczba_pozycji", "dni_od_rejestracji"]
X = df_tr[features].values # wcześniej przetransformowane / przeskalowane
iso = IsolationForest(
n_estimators=300, # więcej drzew = stabilniejsze wyniki
max_samples="auto", # losowana podpróba na drzewo
contamination=0.01, # szacowany odsetek anomalii (np. 1%)
max_features=features.__len__(),
random_state=42,
n_jobs=-1
)
iso.fit(X)
# score_samples: im niższa wartość, tym bardziej odstający punkt
df_tr["if_score"] = iso.score_samples(X)
df_tr["is_outlier_if"] = iso.predict(X) == -1
Typowe modyfikacje, które robią różnicę:
n_estimatorsponiżej 100 przy złożonych danych bywa po prostu zbyt małe, żeby stabilnie „pociąć” przestrzeń; przy kilkudziesięciu tysiącach wierszy spokojnie można podnieść do 300–500,max_sampleslepiej ustawić jawnie, gdy próbka jest mała: dla kilku tysięcy rekordów ustawienie na np.max_samples=512albo0.8da stabilniejsze drzewa niż absolutne „auto”,contaminationnie musi odzwierciedlać rzeczywistego udziału anomalii; częściej jest to „budżet na manualny przegląd” (np. 0.5% transakcji dziennie do przejrzenia przez analityka fraudowego).
Kalibracja progów Isolation Forest zamiast ślepego contamination
Domyślne użycie contamination ustawia próg na score tak, aby odsetek anomalii mniej więcej zgadzał się z zadanym parametrem. Gdy istnieje jakikolwiek „złoty standard” (np. historyczne fraudy), sensowniej jest odseparować trenowanie od kalibracji progu.
# trenowanie bez contamination – brak automatycznego progu
iso = IsolationForest(
n_estimators=400,
max_samples=0.7,
contamination="auto", # użyjemy tylko score_samples
random_state=42,
n_jobs=-1
)
iso.fit(X)
df_tr["if_score"] = iso.score_samples(X)
# przyjmijmy, że mamy historyczne fraudy
y_fraud = df_tr["is_fraud"].fillna(False)
# szukamy progu score, który daje sensowny kompromis
scores = df_tr["if_score"]
threshold = np.quantile(scores, 0.02) # 2% najbardziej odstających
df_tr["is_outlier_if_manual"] = scores < threshold
Gdy dostępne są etykiety (choćby częściowe), próg można dopasować bardziej systematycznie, np. maksymalizując F1 lub ustawiając minimalną czułość:
from sklearn.metrics import precision_recall_curve
scores = -df_tr["if_score"] # im wyżej, tym bardziej odstający
precision, recall, thresh = precision_recall_curve(y_fraud, scores)
# przykład: próg zapewniający co najmniej 80% recall
target_recall = 0.8
idx = np.where(recall >= target_recall)[0][0]
best_threshold = thresh[idx]
df_tr["is_outlier_if_calibrated"] = scores >= best_threshold
Takie podejście rozwiązuje częsty problem: „model znajduje głównie dziwne, ale nieistotne przypadki” – bo próg da się powiązać z metryką, na której naprawdę zależy.
Łączenie prostych metod z Isolation Forest
Popularny błąd to podmiana prostych progów (IQR/Z-score) na Isolation Forest „bo jest sprytniejszy”. Często lepiej działa architektura warstwowa: najpierw tanie, prostolinijne filtry, potem droższy model na bardziej „oczyszczonych” danych.
# 1. flagi z prostych metod
df_tr["is_outlier_iqr_kwota"] = flag_iqr_outliers(df_tr["kwota"])
df_tr["is_outlier_mad_kwota_kraj"] = (
df_tr.groupby("kraj", group_keys=False)["kwota"]
.apply(mad_based_flags)
)
# 2. filtracja twardych oczywistości (np. kwota <= 0)
hard_filter = df_tr["kwota"] <= 0
# 3. Isolation Forest na podejrzanych, ale nie oczywistych rekordach
mask_for_if = (~hard_filter)
X_if = df_tr.loc[mask_for_if, features]
iso = IsolationForest(
n_estimators=400,
max_samples=0.6,
contamination=0.02,
random_state=42,
n_jobs=-1
)
iso.fit(X_if)
df_tr.loc[mask_for_if, "is_outlier_if"] = iso.predict(X_if) == -1
# 4. łączna flaga
df_tr["is_outlier_any"] = (
hard_filter
| df_tr["is_outlier_iqr_kwota"]
| df_tr["is_outlier_mad_kwota_kraj"]
| df_tr["is_outlier_if"].fillna(False)
)
W efekcie Isolation Forest nie marnuje „mocy obliczeniowej” na oczywiste zero-kwotowe zamówienia, tylko na subtelniejsze przypadki (np. nietypowa kombinacja kwoty, koszyka i stażu klienta).

DBSCAN i metody gęstości: gdy „dziwny” znaczy „samotny”
Dlaczego domyślny DBSCAN prawie nigdy nie działa
DBSCAN to jedna z najczęściej polecanych metod „do outlierów”, bo ma wbudowany mechanizm wykrywania punktów szumowych. Domyślne parametry (eps=0.5, min_samples=5) są jednak zupełnie arbitralne; przy źle dobranym eps można dostać:
- jeden wielki klaster i zero szumu,
- setki małych klastrów i większość punktów jako „noise”.
Bez skalowania i sensownej heurystyki doboru eps DBSCAN kończy jako generator losowych flag.
Heurystyka k-distance plot i implementacja w Pythonie
Pomocna sztuczka: wykres odległości do k-tego najbliższego sąsiada (często 4–10). „Załamanie” krzywej podpowiada rozsądny eps.
from sklearn.neighbors import NearestNeighbors
import numpy as np
import matplotlib.pyplot as plt
X = X_robust # np. po RobustScaler
k = 5
nn = NearestNeighbors(n_neighbors=k)
nn.fit(X)
distances, _ = nn.kneighbors(X)
k_distances = np.sort(distances[:, -1]) # odległość do k-tego sąsiada
plt.figure(figsize=(6, 4))
plt.plot(k_distances)
plt.ylabel(f"Odległość do {k}-tego sąsiada")
plt.xlabel("Posortowane punkty")
plt.grid(True)
plt.tight_layout()
plt.show()
Odczytanie wartości eps z wykresu jest wciąż sztuką, ale lepszą niż magiczne „0.5”. Po wyborze eps implementacja jest prosta:
from sklearn.cluster import DBSCAN
eps = 0.8
min_samples = 10
db = DBSCAN(
eps=eps,
min_samples=min_samples,
metric="euclidean",
n_jobs=-1
)
labels = db.fit_predict(X)
df_tr["cluster_dbscan"] = labels
df_tr["is_outlier_dbscan"] = labels == -1
Tutaj outlierem jest wszystko, czego nie dało się przypisać do gęstego klastra. Zaletą jest brak założenia o kształcie klastrów; wadą – wrażliwość na skalowanie i to, że jeden zestaw parametrów rzadko pasuje jednocześnie do gęstych i rozproszonych regionów przestrzeni.
Łączenie DBSCAN z redukcją wymiaru
DBSCAN w wysokim wymiarze szybko się gubi – różnice w odległościach się spłaszczają. Popularny ruch „najpierw PCA, potem DBSCAN” ma sens, ale z jednym zastrzeżeniem: PCA potrafi „rozmazać” drobne, ale istotne grupy. Zamiast automatycznego n_components=0.95 lepiej jest świadomie dobrać niedużą liczbę wymiarów.
from sklearn.decomposition import PCA
pca = PCA(n_components=3, random_state=42)
X_pca = pca.fit_transform(X_robust)
db = DBSCAN(eps=0.7, min_samples=15)
labels = db.fit_predict(X_pca)
df_tr["cluster_dbscan_pca"] = labels
df_tr["is_outlier_dbscan_pca"] = labels == -1
Takie połączenie ma sens, gdy outliery są raczej „poza główną chmurą”, a nie mikroskopijną substrukturą ukrytą w słabiej wyjaśnionej części wariancji.
LOF i kNN: lokalne spojrzenie na anomalię
Local Outlier Factor: sąsiad sąsiadowi nierówny
LOF nie szuka globalnego progu gęstości; porównuje gęstość w otoczeniu punktu z gęstością sąsiadów. Dobrze wychwytuje przypadki, gdzie punkt leży w „dziurze” pomiędzy dwoma gęstymi regionami.
from sklearn.neighbors import LocalOutlierFactor
X = X_robust # przeskalowane cechy
lof = LocalOutlierFactor(
n_neighbors=20,
contamination=0.02,
novelty=False # standardowo tylko detekcja na trenigu
)
labels_lof = lof.fit_predict(X)
scores_lof = -lof.negative_outlier_factor_
df_tr["lof_score"] = scores_lof
df_tr["is_outlier_lof"] = labels_lof == -1
Parametr n_neighbors ustawia „skalę lokalności”. Za mała wartość sprawi, że metoda będzie reagować na drobny szum; za duża – zacznie zachowywać się jak globalny detektor.
LOF do monitorowania nowych danych (novelty detection)
Jeśli celem jest monitorowanie strumienia nowych punktów (np. nowych sesji użytkowników), trzeba przełączyć LOF w tryb novelty=True. Model uczy się wtedy wyłącznie na zbiorze uznanym za „w miarę czysty”, a flagowanie dotyczy nowych obserwacji.
from sklearn.model_selection import train_test_split
X_train, X_val = train_test_split(X_robust, test_size=0.2, random_state=42)
lof_novel = LocalOutlierFactor(
n_neighbors=25,
novelty=True, # umożliwia .predict na nowych danych
contamination=0.01
)
lof_novel.fit(X_train)
# ocena na zbiorze walidacyjnym
val_scores = -lof_novel.decision_function(X_val)
val_pred = lof_novel.predict(X_val) == -1
# zastosowanie do nowych danych
X_new = scaler_robust.transform(df_new[num_cols])
df_new["is_outlier_lof"] = lof_novel.predict(X_new) == -1
Ten wariant ma sens tam, gdzie mamy zaufany okres historyczny (np. sprzed dużej kampanii marketingowej czy migracji systemu) i chcemy rejestrować „nowy typ ruchu”.
Prosty kNN-distance jako interpretowalny score
Gdy potrzebna jest prostsza, łatwa do wyjaśnienia metryka, wystarczy odległość do k-tego najbliższego sąsiada. To swoista wersja LOF bez porównywania lokalnych gęstości.
from sklearn.neighbors import NearestNeighbors
k = 20
nn = NearestNeighbors(n_neighbors=k, metric="minkowski", p=2)
nn.fit(X_robust)
distances, _ = nn.kneighbors(X_robust)
k_dist = distances[:, -1] # odległość do k-tego sąsiada
df_tr["knn_kdist"] = k_dist
# próg np. w górnym 1% rozkładu
thr = np.quantile(k_dist, 0.99)
df_tr["is_outlier_knn"] = k_dist > thr
Tak zdefiniowany score można łatwo pokazać w raportach: „ten klient jest dalej od swoich 20 najbliższych sąsiadów niż 99% wszystkich klientów”. W sytuacjach, gdzie priorytetem jest zrozumiałość, bywa to lepsze niż bardziej „magiczne” metody.
Uczenie nadzorowane do usuwania „brudnych” rekordów
Binary classifier jako filtr jakości danych
Jeśli zespół analityczny przez dłuższy czas ręcznie oznaczał błędne rekordy (np. błędne odczyty sensorów, duplikaty, eksperymentalne transakcje testowe), można potraktować problem jako klasyfikację binarną: „do usunięcia / do wykorzystania”. Wtedy model nie tyle wykrywa statystyczne outliery, co rekonstruuje praktyczne kryteria biznesowe.
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
features = ["kwota", "liczba_pozycji", "dni_od_rejestracji"]
X = df_tr[features]
y = df_tr["is_bad_record"] # etykiety z przeglądów danych
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
clf = RandomForestClassifier(
n_estimators=300,
max_depth=None,
min_samples_leaf=5,
n_jobs=-1,
random_state=42,
class_weight="balanced"
)
clf.fit(X_train, y_train)
probs = clf.predict_proba(X_val)[:, 1]
print("AUC:", roc_auc_score(y_val, probs))
df_tr["bad_prob"] = clf.predict_proba(X)[:, 1]
df_tr["is_outlier_supervised"] = df_tr["bad_prob"] > 0.8
Ten kierunek ma kilka zalet: można użyć wyjaśnialności (SHAP, feature importance), dopasować próg do zasobów (np. ile rekordów miesięcznie jesteśmy w stanie ręcznie zweryfikować) oraz zintegrować „miękkie” reguły biznesowe, których nie da się prosto zapisać jako progu na pojedynczej zmiennej.
Trik z modelem predykcyjnym: outliery jako duże błędy
Inne podejście nadzorowane: trenowany jest model predykcyjny do normalnego zadania (np. prognoza kwoty z cech klienta i koszyka). Outliery to obserwacje z największym błędem predykcji, bo trudno je dopasować do ogólnego wzorca.
Najważniejsze punkty
- Obserwacje odstające to przede wszystkim problem decyzyjny, a nie techniczny – ten sam ekstremalny punkt może być błędem danych, fraudem lub bezcenną informacją o rzadkim, biznesowo kluczowym zdarzeniu.
- Mechaniczne „czyszczenie ogona” (np. ucinanie 1% skrajnych wartości) bywa szkodliwe: w sprzedaży usuwa dni kampanii, w IoT sygnały zbliżającej się awarii, a w finansach realne straty i zyski, przez co modele są sztucznie wygładzone.
- Outliery silnie wypaczają metryki statystyczne i modele – przesuwają średnią, zawyżają odchylenie standardowe, przekłamują korelacje i estymację parametrów regresji, a w klasyfikacji potrafią mocno zaburzyć granicę decyzyjną (szczególnie w modelach opartych na odległości).
- Usuwanie odstających punktów ma sens głównie wtedy, gdy istnieje mocny dowód na błąd danych albo gdy model ma opisywać wyłącznie „typowy” przypadek, a ekstremalne sytuacje są obsługiwane innym procesem (np. ręczną weryfikacją lub osobnym modelem ryzyka).
- Lepszym podejściem niż kasowanie jest zarządzanie outlierami: identyfikacja, zrozumienie kontekstu, podział na błąd / zdarzenie jednorazowe / nowy wzorzec, a dopiero potem decyzja – usunąć, oznaczyć flagą, przeskalować czy modelować oddzielnie.
- Nie każdy „outlier” statystyczny jest anomalią biznesową i odwrotnie – klient, który nagle przestaje kupować, może być kluczową anomalią w zachowaniu, mimo że nie generuje ekstremalnych wartości w pojedynczych kolumnach liczbowych.






