W tym wpisie przedstawiam 3 sprawdzone metody wykrywania obserwacji odstających w Python. Zapraszam do przeczytania. 🙂
Czym są obserwacje odstające?¶
Już sama definicja "obserwacji odstającej" może przysporzyć sporo problemów. Według definicji dostępnej na Wikipedii jest to "obserwacja relatywnie odległa od pozostałych elementów próby". Co oznacza owa relatywność i ile wynosi? To jest już kwestia umowna i zmienia się w zależności od metody i definicji, na której dana metoda bazuje.
Po co wykrywać obserwacje odstające?¶
Wiele metod używanych w statystyce i uczeniu maszynowym jest wrażliwych na występowanie obserwacji odstających, tzw. outlierów. Poprzez wrażliwość należy rozumieć możliwość zaburzenia i zniekształcenia wyników uzyskanych za pomocą danych metod. Przykładem "wrażliwców" mogą być: regresja liniowa i sieci neuronowe.
Jak wykryć obserwacje odstające?¶
O tym, czy dana obserwacja jest odstająca decydować może wiele czynników. Najważniejszym z nich jest rozkład. W zależności od rozkładu zmiennej dobiera się odpowiednią metodę. W tym wpisie skupiam się na trzech podstawowych, które w dosłownie kilka minut pozwalają stwierdzić, czy dana zmienna zawiera wartości odstające, oraz ew. ile ich jest. 🙂
W części praktycznej mojej analizy skorzystam ze zbioru, którego używałem również w poprzednim wpisie dotyczącym badania normalności rozkładu.
Najważniejsze informacje o zbiorze:
- Wine quality
- Opis zbioru
- Opublikowany przez: Paulo Cortez.
1. Wczytanie niezbędnych bibliotek.¶
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import scipy.stats
2. Wczytanie zbioru.¶
white_wines = pd.read_csv('data/winequality-white.csv', sep = ';')
red_wines = pd.read_csv('data/winequality-red.csv', sep = ';')
Na potrzeby dalszej analizy łącze oba zbiory w jeden.
wines = pd.concat([white_wines, red_wines], axis = 0)
3. Podgląd scalonego zbioru.¶
wines.head()
print('Zbiór zawiera {} obserwacji i {} zmiennych.'.format(wines.shape[0], wines.shape[1]))
print('Lista zmiennych dostępnych w zbiorze: {}'.format(list(wines.columns)))
Chcesz pozbyć się skośności i wartości odstających ze zbioru? Sprawdź wpisy o kategoryzacji zmiennych ciągłych i transformacji z użyciem metody WoE.
4. Analiza obserwacji odstających.¶
4.1. Weryfikacja z użyciem podstawowych statystyk.¶
Pierwsze przesłanki można odczytać, spoglądając na statystyki dotyczące:
- odchylenia standardowego,
- mediany,
- kwartyli,
- percentyli.
wines.agg(['std'])
wines.quantile([0.01, 0.25, 0.5, 0.75, 0.99])
Doskonale widać, czego możemy się spodziewać po poszczególnych zmiennych. Dla przykładu zmienna "fixed acidity" osiąga odstające wartości na "górze", a więc w okolicach 99 percentyla jest znacznie dalej od mediany, niż po stronie przeciwnej (1 percentyl). Świadczy to o prawostronnie skośnym rozkładzie. Jeśli dodamy do tego:
- odległości od mediany (1.9 dla 1 percentyla i 5 dla 99 percentyla),
- odchylenie standardowe = 1.296,
- rozstęp międzykwartylowy = 1.3,
to śmiało możemy stwierdzić, że znaczna część wartości odstających znajduje się w jej przypadku "u góry". Skąd moja pewność? Jak można sprawdzić to w sposób obiektywny? Przejdźmy do drugiej metody. 🙂
4.2. Reguła 1.5 wartości rozstępu międzykwartylowego - metoda analityczna oparta na statystykach.¶
Metoda ta jest aplikowalna dla dowolnej zmiennej numerycznej bez względu na rozkład. U jej podstaw leży założenie, że wszystkie "typowe" obserwacje leżą pomiędzy punktami wyznaczonymi przez odległość 1.5 IQR (ang. interquartile range):
- "na lewo" od granicy pomiędzy pierwszym i drugim kwartylem,
- "na prawo" od granicy pomiędzy trzecim i czwartym kwartylem.
Czym jest rozstęp międzykwartylowy?
Jest to różnica między pierwszym a trzecim kwartylem. Im większa różnica, tym większe zróżnicowanie względem danej zmiennej.
Wyznaczenie statystyk niezbędnych do aplikacji metody
Będziemy potrzebować trzech elementów: wartości zmiennej na granicy pierwszego i drugiego kwartyla, wartości zmiennej na granicy trzeciego i czwartego kwartyla oraz rozstępu.
q1 = wines.quantile(0.25) # wartości zmiennej na granicy pierwszego i drugiego kwartyla
q3 = wines.quantile(0.75) # wartości zmiennej na granicy trzeciego i czwartego kwartyla
iqr = q3 - q1 # rozstęp międzykwartylowy
Podejrzyjmy wartości rozstępu międzykwartylowego.
iqr
Teraz dzięki możliwościom jakie daje Pandas, zbudujmy tabelę z podsumowaniem.
low_boundary = (q1 - 1.5 * iqr)
upp_boundary = (q3 + 1.5 * iqr)
num_of_outliers_L = (wines[iqr.index] < low_boundary).sum()
num_of_outliers_U = (wines[iqr.index] > upp_boundary).sum()
outliers_15iqr = pd.DataFrame({'lower_boundary':low_boundary, 'upper_boundary':upp_boundary,'num_of_outliers_L':num_of_outliers_L, 'num_of_outliers_U':num_of_outliers_U})
outliers_15iqr
Jak czytać tabelę?
- lower_boundary i upper_boundary - są to wartości graniczne, czyli wcześniej wyznaczone
q1
iq3
, - num_of_outliers_L - jest to liczba wartości odstających poniżej granicy wyznaczonej przez q1 - iqr (przyrostek L od ang. lower),
- num_of_outliers_U - jest to liczba wartości odstających powyżej granicy wyznaczonej przez q3 + iqr (przyrostek U od ang. upper),
Zgodnie z moją intuicją i wartościami z punktu 4.1. potwierdza się fakt, że znaczna część wartości odstających dla zmiennej "fixed acidity" leży powyżej trzeciego kwartyla.
4.3. Reguła 1.5 wartości rozstępu międzykwartylowego - metoda wizualna.¶
Regułę 1.5 IQR łatwo można zobrazować z pomocą wykresu pudełkowego. Podłużna skrzynka na jego środku to obszar pomiędzy pierwszym a trzecim kwartylem. Jej rozmiar odnosi się do rozstępu międzykwartylowego. 🙂 Dalej od środka widoczne są wąsy, których zakończeniem są granice dla typowych obserwacji. Wszystko, co znajduje się na zewnątrz poza granicami to zgodnie z regułą 1.5 IQR obserwacje odstające, które zostały oznaczone kropkami.
Czemu długość wąsów nie jest równa?
Przyczyna jest prosta: w momencie, gdy nie w zbiorze nie ma żadnej obserwacji odstającej (dla danej zmiennej i po wybranej stronie), wąs kończy się na największej, bądź najmniejszej zaobserwowanej wartości.
Wizualizacja zmiennych na wykresie pudełkowym
Skala, na jakiej umieszczone są poszczególne zmienne, znacznie się różni (np średnia 'volatile acidity' wynosi ok 0.33, a średnia 'total sulfur dioxide' wynosi ok. 116), dlatego jest dosyć trudno zobrazować rozkład zmiennych na jednym wykresie. Decyduję się więc podzielić zmienne na trzy grupy i oddzielne wykresy.
melted_wines_df_1 = pd.melt(wines, value_vars=wines.drop(['total sulfur dioxide', 'free sulfur dioxide', 'residual sugar', 'fixed acidity', 'alcohol', 'quality'], axis = 1).columns, var_name=['feature_name'], value_name = 'value')
melted_wines_df_2 = pd.melt(wines, value_vars=wines[['fixed acidity', 'alcohol', 'quality', 'residual sugar', 'fixed acidity', 'alcohol']].columns, var_name=['feature_name'], value_name = 'value')
melted_wines_df_3 = pd.melt(wines, value_vars=wines[['total sulfur dioxide', 'free sulfur dioxide']].columns, var_name=['feature_name'], value_name = 'value')
plt.figure(figsize=(8,5))
sns.set(font_scale=1.4)
sns.boxplot(data = melted_wines_df_1, y = 'feature_name', x = 'value', palette = 'Blues_d').set(title = 'Rozkład zmiennych - grupa 1', ylabel = 'nazwy zmiennych')
plt.show()
plt.figure(figsize=(8,5))
sns.set(font_scale=1.4)
sns.boxplot(data = melted_wines_df_2, y = 'feature_name', x = 'value', palette = 'Blues_d').set(title = 'Rozkład zmiennych - grupa 2', ylabel = 'nazwy zmiennych')
plt.show()
plt.figure(figsize=(8,5))
sns.set(font_scale=1.4)
sns.boxplot(data = melted_wines_df_3, y = 'feature_name', x = 'value', palette = 'Blues_d').set(title = 'Rozkład zmiennych - grupa 3', ylabel = 'nazwy zmiennych')
plt.show()
Na powyższych wykresach widać, że każda zmienna zawiera jakieś obserwacje odstające. Jedne są mniej, a inne mniej widoczne. W niektórych przypadkach, jak np. "residual sugar", widać outlier-a, który wręcz wydaje się błędem w próbie.
4.4. Reguła trzech sigm.¶
Metoda ta jest aplikowalna tylko dla zmiennych pochodzących z rozkładu normalnego. W poprzednim wpisie ustaliłem, że żadna ze zmiennych nie cechuje się rozkładem normalnym. Normalność rozkładu nie jest jednak cechą zero-jedynkową. Nawet najlepsze przykłady zmiennych o rozkładzie normalnym nie cechują się 100% normalnością - np. wzrost człowieka, który nigdy nie będzie ujemny. 🙂
Na potrzeby tej analizy założę, że zmienne: "total sulfur dioxide" i "pH" posiadają rozkład normalny.
Sigma odnosi się do litery z greckiego alfabetu, poprzez którą oznaczane jest odchylenie standardowe. Reguła trzech sigm mówi, że 99,7% wartości danej zmiennej leży w odległości mniejszej lub równej wartości trzech odchyleń standardowych od wartości oczekiwanej. Wszystkie obserwacje nie spełniające tej reguły możemy zatem nazwać obserwacjami nietypowymi. Spróbujmy zatem zrobić podobne podsumowanie jak dla reguły 1.5 IQR. 🙂
zmienne = ['total sulfur dioxide', 'pH']
sigma = wines[zmienne].std()
srednia = wines[zmienne].mean()
sigma
srednia
low_boundary = (srednia - 3 * sigma)
upp_boundary = (srednia + 3 * sigma)
num_of_outliers_L = (wines[zmienne] < low_boundary).sum()
num_of_outliers_U = (wines[zmienne] > upp_boundary).sum()
outliers_3sigma = pd.DataFrame({'lower_boundary':low_boundary, 'upper_boundary':upp_boundary,'num_of_outliers_L':num_of_outliers_L, 'num_of_outliers_U':num_of_outliers_U})
outliers_3sigma
Przeczytaj również wpis, dotyczący analizy normalności rozkładu w Python: 3 metody analizy normalności rozkładu w Python.
5. Usuwanie obserwacji odstających ze zbioru.¶
W części przypadków będziemy chcieli pozbyć się obserwacji odstających ze zbioru. Poniżej przedstawia Ci sposób, w jaki mozna to zrobić korzystając z przygotowanych tabel pomocniczych: outliers_15iqr
, outliers_3sigma
.
wines_without_outliers = wines.copy()
print('Rozmiar zbioru z obserwacjami odstającymi:', wines_without_outliers.shape[0])
for row in outliers_15iqr.iterrows():
wines_without_outliers = wines_without_outliers[(wines_without_outliers[row[0]] >= row[1]['lower_boundary']) & (wines_without_outliers[row[0]] <= row[1]['upper_boundary'])]
for row in outliers_3sigma.iterrows():
wines_without_outliers = wines_without_outliers[(wines_without_outliers[row[0]] >= row[1]['lower_boundary']) & (wines_without_outliers[row[0]] <= row[1]['upper_boundary'])]
print('Rozmiar zbioru po usunięciu obserwacji odstających:', wines_without_outliers.shape[0])
6. Podsumowanie.¶
To by było na tyle, jeśli chodzi o analizę obserwacji odstających. 🙂 Oczywiście powyższe metody nie wyczerpują tematu. W zasadzie to są dopiero wstępem do zagadnienia znacznie obszerniejszego: wykrywania zjawisk rzadkich. Za jakiś czas chciałbym również i ten temat poruszyć na blogu.
Mam nadzieję, że wpis przypadł Ci do gustu, a metody filtracji obserwacji odstających oparte o tabele pomocnicze nieco zautomatyzują Twoją pracę. 🙂
PODOBAŁ CI SIĘ TEN ARTYKUŁ?
Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach.
Dodatkowo w prezencie wyślę Ci bezpłatny poradnik :-)
Super wpis. Kawał dobrej roboty!
Dziękuję Grzegorz! 🙂
Fantastyczny wpis. Swoją drogą, jakiś czas temu sam korzystałem z metody 1.5 IQR przy wykrywaniu obserwacji odstających dla danych HR-ych.
Oj, coś czuję, że ten blog zagości na mojej liście codziennie przeglądanych serwisów. Dobrze jest poczytać „wypociny” zdolnych ludzi. Pozdrawiam!
Krzysztof, dziękuję za szalenie miły komentarz i zapraszam do częstszych odwiedzin! 🙂