Transformacja WoE jest bodaj najpopularniejszym reprezentantem metod tzw. target encodingu. W tym odcinku Data Science Plus opowiadam o jej zastosowaniach, wadach i zaletach. Zapraszam do przeczytania i obejrzenia!
Ten odcinek w sposób bezpośredni odnosi się do poprzednich odcinków Data Science Plus. Jeśli chcesz mieć pełną wiedzę o tym, co robię, to zachęcam Cię do obejrzenia go, zanim zapoznasz się z najnowszą częścią. 🙂
W tym odcinku usłyszysz:
- Czym jest WoE (ang. Weight of Evidence)?
- Po co stosować WoE? Jakie są jej wady i zalety?
- Do jakich zmiennych należy stosować WoE?
- Jak przeprowadzić transformację WoE, bez narażania na przecieki danych?
- O ile punktów Gini transformacja WoE może poprawić model regresji logistycznej?
Czym jest WoE?
WoE (ang. Weight of Evidence) – tłumacząc dosłownie: waga dowodów/przesłanek – jest metodą transformacji zmiennej objaśniającej. Należy do kategorii tzw. target encodingu, lub likelihood encodingu. Polega na zamianie zmiennej nominalnej na zmienną ciągłą. To, co jest niezwykłego w WoE, to fakt, że do przeprowadzenia transformacji używa zmiennej celu.
Powyższy akapit to nie koniec ciekawostek na temat WoE. Transformacja WoE jest niejako „skrojona” pod regresję logistyczną, m.in. stąd jej ogromna popularność w sektorze finansowym. Gdy spojrzymy na wzór WoE, zauważymy logarytm naturalny (patrz poniżej). Ten sam logarytm naturalny jest widoczny we wzorze funkcji logitowej, leżącej u podstaw regresji logistycznej (również poniżej). 😉 Fakt ten niesie ze sobą kilka istotnych zalet:
- Skala logarytmiczna jest skalą naturalną dla regresji logistycznej.
- WoE zapewnia liniowy związek zmiennej objaśniającej – będącej teraz logarytmem naturalnym ilorazów szans danej kategorii – ze zmienną objaśnianą. Dzięki niej jesteśmy zatem zgodni nie tylko z założeniami modelu regresji, ale również z wymaganiami regulatora.
Wzory funkcji logitowej i WoE:
WoE, a przecieki danych¶
Kontrolowany przeciek jest tu konieczny - do transformacji używamy przecież zmiennej celu. Należy jednak pamiętać, że dopuszczamy do niego tylko i wyłącznie w zbiorze uczącym. Transformacja zbiorów: walidacyjnego i testowego jest przeprowadzona z użyciem słownika zbudowanego na zbiorze uczącym. 🙂
Jak przeprowadzić transformację WoE?¶
Transformacja wykonywana jest w trzech krokach (dla zmiennych ciągłych):
- Kategoryzacja zmiennej ciągłej na kilka kategorii (za pomocą drzew decyzyjnych, kwartyli, decyli, lub innej metody). Zakładamy, że wszystkie wartości zmiennej, które zostały zawarte w danej kategorii oddziałują w jednakowym stopniu na zmienną celu.
- Obliczanie WoE dla każdej kategorii transformowanej zmiennej.
- Zamiana pierwotnych wartości zmiennej poprzez wartości WoE.
Dla zmiennych kategorycznych można (choć nie trzeba) pominąć krok pierwszy.
Główne korzyści i zagrożenia¶
Część korzyści już została wymieniona w poprzednich akapitach. Poniżej wymieniam pozostałe i dodaję kilka bolączek metody.
Zalety:¶
- poprawa jakości modelu - mam tu na myśli bezpośrednie przełożenie na statystykę Gini,
- eliminacja części bolączek regresji logistycznej - wrażliwość na wartości odstające, założenia dotyczące rozkładu - po transformacji WoE przestają nas (w dużym stopniu) one interesować,
- ujednolicenie skali zmiennych - podsumowując model i patrząc na współczynniki regresji, jesteśmy w stanie ocenić wpływ zmiennej/kategorii na predykcję; przed transformacją trudno porównać, czy np. rok doświadczenia zawodowego wpływa równie mocno na predykcję, co jeden tysiąc dodatkowego dochodu miesięcznie; po transformacji WoE porównujemy "jabłka do jabłek"; poza tym już podczas budowy modelu możemy w prosty sposób wnioskować o tym, czy np.
stan_cywilny = "żonaty"
będzie mieć taki sam wpływ na ryzyko, jakwyksztalcenie = "średnie"
; jeśli jesteś ciekaw, jaki wpływ na ryzyko ma sytuacja, gdy dana osoba:stan_cywilny = "żonaty" i wyksztalcenie = "średnie"
, to wystarczy zsumować wartości WoE odpowiadające obu kategoriom 🙂 - umożliwia ocenę wpływu danej kategorii na zmienną celu - WoE to logarytm naturalny ilorazu szans na predykcję klas 0 i 1, zatem im mniejsza wartość WoE, tym większe prawdopodobieństwo 1, a im większa wartość WoE, tym mniejsze prawdopodobieństwo defaultu,
- zapewnia monotoniczną relację zmiennej objaśnianej ze zmienną objaśnianą - wraz ze wzrostem wartości WoE maleje default rate, a więc teoretycznie maleje prawdopodobieństwo defaultu,
- WoE jest składową IV - IV, czyli Information Value, to jedna z najważniejszych statystyk używanych do selekcji zmiennych; absolutny standard w bankowości i sektorze finansowym.
Wady:¶
- utrata części informacji - kategoryzacja zmiennej ciągłej sprawia, że tracimy część informacji zawartej w zmiennej,
- podatność na przeuczenie - wynika to z dwóch elementów: źle przeprowadzonej kategoryzacji zmiennej ciągłem, lub/i zastosowaniu niezgodnym z przeznaczeniem - WoE użyte z np. XGBoostem na 99% doprowadzi do przeuczenia - ogólna zasada jest następująca: WoE + logit = OK, WoE + wysublimowane algorytmy = OVERFITTING; 🙂 dla lasów losowych i XGBoost-a znacznie lepiej sprawdzi się mean encoding okraszony odrobiną szumu (poprzez m.in. CV, lub wygładzanie),
- podatność na przecieki danych - do przecieków dochodzi, gdy jest zastosowana w sposób niewłaściwy; należy pamiętać o kalkulacji WoE tylko i wyłącznie na zbiorze uczącym,
- nie uwzględnia zależności pomiędzy zmiennymi - WoE jest transformacją jednowymiarową i nie uwzględnia zależności/interakcji pomiędzy zmiennymi objaśniającymi.
Wystarczy już teoretyzowania. Przejdźmy do praktycznego przykładu. 🙂
1. Import bibliotek.¶
from category_encoders import WOEEncoder
from creme_de_la_creme_of_ML import forward_selection
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.metrics import roc_auc_score, classification_report, confusion_matrix, accuracy_score, recall_score, precision_score, roc_curve
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.tree import DecisionTreeClassifier
from statsmodels.discrete.discrete_model import Logit
import statsmodels.api as sm_api
2. Wczytanie zbioru danych.¶
Tego zbioru używałem przy okazji kilku innych wpisów (przewidywanie defaultu wśród posiadaczy kart kredytowych i kategoryzacja zmiennych z użyciem drzewa decyzyjnego).
x_tr = pd.read_csv('data/x_tr.csv', index_col = 0)
x_te = pd.read_csv('data/x_te.csv', index_col = 0)
x_va = pd.read_csv('data/x_va.csv', index_col = 0)
y_tr = pd.read_csv('data/y_tr.csv', header = None, index_col = 0)
y_te = pd.read_csv('data/y_te.csv', header = None, index_col = 0)
y_va = pd.read_csv('data/y_va.csv', header = None, index_col = 0)
Sprawdzam rozkład zmiennej celu.
y_tr[1].value_counts(normalize = True)
y_te[1].value_counts(normalize = True)
76.5% do 23.5% - ewidentnie możemy tu mówić o zbiorze niezbalansowanym.
Zmiana w zbiorze - zamieniam dummy coding na label encoding. Z pomocą WoE mogę nieco zmniejszyć "szerokość" zbioru bez negatywnego wpływu na model.
x_tr = x_tr.iloc[:, 0:-7]
x_te = x_te.iloc[:, 0:-7]
x_va = x_va.iloc[:, 0:-7]
ef = pd.ExcelFile('data/default_of_credit_card_clients.xls')
df = ef.parse('Data', skiprows=1, names = ['id', 'lim_kredytu', 'plec', 'wyksztalcenie', 'stan_cywilny', 'wiek', 'opozn_plat_wrz', 'opozn_plat_sie', 'opozn_plat_lip', 'opozn_plat_cze', 'opozn_plat_maj', 'opozn_plat_kwi', 'kwota_wyciagu_wrz', 'kwota_wyciagu_sie', 'kwota_wyciagu_lip', 'kwota_wyciagu_cze', 'kwota_wyciagu_maj', 'kwota_wyciagu_kwi', 'platnosc_wrz', 'platnosc_sie', 'platnosc_lip', 'platnosc_cze', 'platnosc_maj', 'platnosc_kwi', 'y'])
df.drop('id', axis = 1, inplace = True)
x_tr = pd.merge(x_tr, df[['wyksztalcenie', 'stan_cywilny']], left_index=True, right_index=True)
x_te = pd.merge(x_te, df[['wyksztalcenie', 'stan_cywilny']], left_index=True, right_index=True)
x_va = pd.merge(x_va, df[['wyksztalcenie', 'stan_cywilny']], left_index=True, right_index=True)
3. Przygotowanie zbioru.¶
3.1. Kategoryzacja zmiennych drzewem.¶
Przygotowanie funkcji do kategoryzacji.
def categorize_data(x, y):
"""Tree based data categorization.
Parameters:
-----------
x : Pandas Series, feature to categorize
y : Pandas Series, target variable
Returns:
--------
dt_model : sklearn.tree.DecisionTreeClassifier, fitted decision tree model
categories : numpy.ndarray, list of categories
"""
dt_model = DecisionTreeClassifier(max_depth = 3)
dt_model.fit(x.values.reshape(-1, 1), y)
categories = dt_model.apply(x.values.reshape(-1, 1))
return dt_model, categories
Sprawdzę jeszcze listę unikalnych wartości w poszczególnych zmiennych.
x_tr.nunique()
# Tylko zmienne, które mają więcej niż 8 wartości unikalanych.
zmienne_do_kategoryzacji = [zmienna for zmienna in x_tr.columns if x_tr[zmienna].unique().shape[0] > 8]
for zmienna in zmienne_do_kategoryzacji:
model, kategorie = categorize_data(x_tr[zmienna], y_tr[1])
x_tr = x_tr.assign(_kat = kategorie)
x_te = x_te.assign(_kat = model.apply(x_te[zmienna].values.reshape(-1, 1)))
x_va = x_va.assign(_kat = model.apply(x_va[zmienna].values.reshape(-1, 1)))
nazwa_zmiennej = zmienna + '_kat'
x_tr.rename(columns = {'_kat' : nazwa_zmiennej}, inplace = True)
x_te.rename(columns = {'_kat' : nazwa_zmiennej}, inplace = True)
x_va.rename(columns = {'_kat' : nazwa_zmiennej}, inplace = True)
3.2. Transformacja WoE.¶
zmienne_do_transformacji_woe = [zmienna for zmienna in x_tr.columns if x_tr[zmienna].unique().shape[0] <= 8]
zmienne_do_transformacji_woe.remove('intercept')
woe_encoder = WOEEncoder(cols = zmienne_do_transformacji_woe)
x_tr = woe_encoder.fit_transform(x_tr, y_tr[1])
x_te = woe_encoder.transform(x_te)
x_va = woe_encoder.transform(x_va)
W tym miejscu muszę zaznaczyć kilka elementów:
- Biblioteka z której korzystam o transformacji WoE jest moim niedawnym odkryciem. Testuję ją od około miesiąca.
- Autorzy zakładają, że "Good" jest oznaczany w zmiennej celu za pomocą 1, czyli odwrotnie niż w przypadku międzynarodowego sektora finansowego. W sektorze finansowym "Good", czyli dobry kredytobiorca jest oznaczany za pomocą 0. Jeśli zdecydujesz się na używanie biblioteki, to można z tego wybrnąć mnożąc uzyskane wyniki WoE przez -1.
- Domyślnie klasa
WOEEncoder
zakłada niewielką regularyzację. Jej celem są: uniknięcie błędu związanego z dzieleniem przez 0 i uniknięcie zbyt wysokiego dopasowania do zbioru uczącego. Jeśli pracujesz w branży regulowanej prawnie, sprawdź dokładne parametry klasy przed jej zastosowaniem.
4. Modelowanie.¶
4.1. Selekcja zmiennych.¶
x_tr.head()
finalne_zmienne = forward_selection(pd.concat([x_tr, x_va]), pd.concat([y_tr, y_va]))
Czemu zredukowałem liczbę zmiennych do 9? Modele zawierające kilkadziesiąt zmiennych są ciężkie w utrzymaniu. W praktyce unika się tego typu sytuacji.
Data Scientistów korzystających z Pandas-a można podzielić na dwie grupy:
- tych, którzy mieli problemy z pamięcią RAM,
- tych, który będą mieć problemy z pamięcią RAM. 😉
Jeśli należysz do drugiej grupy, sprawdź mój wpis o 5 sposobach na radzenie sobie z dużymi zbiorami w Pandas.
4.2. Budowa modelu z wybranymi zmiennymi.¶
model_rl = Logit(y_tr, x_tr[finalne_zmienne]).fit()
prawd_rl = model_rl.predict(x_te[finalne_zmienne])
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(3))
5. Porównanie wyników.¶
x_tr = pd.read_csv('data/x_tr.csv', index_col = 0)
x_te = pd.read_csv('data/x_te.csv', index_col = 0)
y_tr = pd.read_csv('data/y_tr.csv', header = None, index_col = 0)
y_te = pd.read_csv('data/y_te.csv', header = None, index_col = 0)
x_tr = x_tr.iloc[:, 0:-7]
x_va = x_va.iloc[:, 0:-7]
ef = pd.ExcelFile('data/default_of_credit_card_clients.xls')
df = ef.parse('Data', skiprows=1, names = ['id', 'lim_kredytu', 'plec', 'wyksztalcenie', 'stan_cywilny', 'wiek', 'opozn_plat_wrz', 'opozn_plat_sie', 'opozn_plat_lip', 'opozn_plat_cze', 'opozn_plat_maj', 'opozn_plat_kwi', 'kwota_wyciagu_wrz', 'kwota_wyciagu_sie', 'kwota_wyciagu_lip', 'kwota_wyciagu_cze', 'kwota_wyciagu_maj', 'kwota_wyciagu_kwi', 'platnosc_wrz', 'platnosc_sie', 'platnosc_lip', 'platnosc_cze', 'platnosc_maj', 'platnosc_kwi', 'y'])
df.drop('id', axis = 1, inplace = True)
x_tr = pd.merge(x_tr, df[['wyksztalcenie', 'stan_cywilny']], left_index=True, right_index=True)
x_te = pd.merge(x_te, df[['wyksztalcenie', 'stan_cywilny']], left_index=True, right_index=True)
finalne_zmienne
Usuwam przyrostki, których nie ma w surowym zbiorze.
finalne_zmienne_bez_przyrostkow = ['opozn_plat_wrz',
'lim_kredytu',
'opozn_plat_cze',
'platnosc_sie',
'opozn_plat_kwi',
'platnosc_cze',
'wyksztalcenie',
'opozn_plat_lip',
'platnosc_wrz']
model_rl = Logit(y_tr, x_tr[finalne_zmienne_bez_przyrostkow]).fit()
prawd_rl = model_rl.predict(x_te[finalne_zmienne_bez_przyrostkow])
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(3))
5.1.2. Zmienne po drzewie, przed WoE.¶
# Tylko zmienne, które mają więcej niż 8 wartości unikalanych.
zmienne_do_kategoryzacji = [zmienna for zmienna in x_tr.columns if x_tr[zmienna].unique().shape[0] > 8]
for zmienna in zmienne_do_kategoryzacji:
model, kategorie = categorize_data(x_tr[zmienna], y_tr)
x_tr = x_tr.assign(_kat = kategorie)
x_te = x_te.assign(_kat = model.apply(x_te[zmienna].values.reshape(-1, 1)))
nazwa_zmiennej = zmienna + '_kat'
x_tr.rename(columns = {'_kat' : nazwa_zmiennej}, inplace = True)
x_te.rename(columns = {'_kat' : nazwa_zmiennej}, inplace = True)
model_rl = Logit(y_tr, x_tr[finalne_zmienne]).fit()
prawd_rl = model_rl.predict(x_te[finalne_zmienne])
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(3))
5.1.3. Zmienne po obu transformacjach.¶
Wynik widoczny w punkcie 4.2: Gini score = 0.563. Wyniki dla 9 zmiennych wyglądają więc następująco:
Zmienne surowe (9 zmiennych) | Zmienne kategoryzowane drzewem (9 zmiennych) | Zmienne kategoryzowane drzewem + transformowane WoE (9 zmiennych) | |
---|---|---|---|
Gini | 0.461 | 0.517 | 0.563 |
5.2. Porównanie najlepszego modelu z innymi modelami budowanymi na tym samym zbiorze.¶
Logit (29 zmiennych) | Logit + kategoryzacja drzewem (49 zmiennych) | Logit + kategoryzacja drzewem + WoE + selekcja zmiennych (9 zmiennych) | XGBoost + randomized search + Boruta + kategoryzacja drzewem (16 zmiennych) | |
---|---|---|---|---|
Gini | 0.481 | 0.566 | 0.563 | 0.58 |
Po zastosowaniu WoE wynik spadł z 0.566 do 0.563. Nie uznaję tego jednak za porażkę, gdyż zredukowałem liczbę zmiennych do 9. 🙂 Do pobicia wyniku uzyskanego z pomocą XGBoost i kilku zaawansowanych metod troszkę jeszcze brakuje. Nie powiedziałem jednak ostatniego słowa i mam asa w rękawie (patrz ogłoszenie na końcu wpisu). 😉
6. Podsumowanie.¶
To już koniec prac nad tym zbiorem. Jestem zadowolony z postaci modelu, wykonanych transformacji, liczby finalnych zmiennych i końcowego wyniku. W kolejnych wpisach i odcinkach Data Science Plus będę brać na tapetę kolejne tematy z zakresu zaawansowanej analityki. Do usłyszenia! 🙂
KRÓTKIE OGŁOSZENIE W najbliższy dniach udostępnię kilkudziesięciominutowe wideo szkoleniowe w postaci screencastu, które przygotowałem specjalnie dla subskrybentów bloga. Budując newsletter założyłem, że będę się dzielić z subskrybentami materiałami premium, niedostępnymi dla szerszego grona czytelnikow. Wspomniany we wpisie "as z rękawa" idealnie pasuje na główny temat tego typu materiału. :) Uchylając rąbka tajemnicy napiszę, że jest głównym tematem screencastu będzie nietrywialna technika, która pozwala znacznie poprawić jakość modelu i zbliżyć się, lub wręcz pobić wynik osiągany z użyciem black-boxów. Jeśli jeszcze tego nie zrobiłeś, dołącz do subskrybentów, by otrzymywać darmowe poradniki, informacje ode mnie i materiały premium. :)
Linki:
- Link do kanału na YouTube: Data Science Plus.
- Link do subskrybowania kanału: subskrybuj.
- Scoring kredytowy z zastosowaniem XGBoost.
- Pozostałe odcinki Data Science Plus.
- Funkcja logitowa.
- Regresja logistyczna.
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 :-)
Dodaj komentarz