DSP 004: Kategoryzacja zmiennych z użyciem drzewa decyzyjnego

W poprzednich odcinkach ustaliliśmy metodę pozwalającą na dobór odpowiedniego punkt odcięcia. Teraz pora na poprawę rentowności i jakości modelu. Użyję do tego drzewa decyzyjnego, które wiele wnosi do modelu regresji. Zapraszam do obejrzenia!

Ten odcinek w sposób bezpośredni odnosi się do poprzedniego odcinka 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 kategoryzacja zmiennych?
  • Jak kategoryzować zmienne z użyciem algorytmu drzewa decyzyjnego?
  • Jak osiągnąć znaczną poprawę wyniku, unikając przecieków danych i przeuczenia modelu?

Czym jest kategoryzacja zmiennych?

Kategoryzacja zmiennych, nazywana też grupowaniem (ang. binning), to proces budowania nowych zmiennych na podstawie zmiennych już istniejących. Każda zmienna (ew. wybrane zmienne) numeryczna lub kategoryczna jest transformowana do zmiennej kategorycznej.

W najprostszym przypadku proces transformacji jest wykonywany z użyciem wartości średniej, mediany, kwartyli, lub decyli. Mają one jednak tę poważną wadę: są aplikowalne jedynie do zmiennych numerycznych.

Rozwinięciem wspomnianych metod jest klasyfikacja z użyciem płytkiego drzewa decyzyjnego. Ograniczenie jego głębokości ma zabezpieczać przed przeuczeniem modelu i uzyskaniem zbyt dużej liczby kategorii.

Jak działa ta metoda? Otóż zmienną celu w podstawowym modelu drzewa jest y ze zbiory wejściowego, a jedyną zmienną objaśniającą jest zmienna transformowana. Na podstawie wartości Entropii lub statystyki Giniego następuje podział zmiennej wejściowej. Każdorazowo głównym celem jest podział maksymalizujący moc predykcyjną zmiennej. W rezultacie otrzymujemy liście, które będą kategoriami nowej zmiennej.

Główne korzyści i zagrożenia

Do niewątpliwych plusów tej metody można zaliczyć:

  • uproszczenie zbioru - po kategoryzacji zmienne zawarte w zbiorze posiadają mniejszą liczbę kategorii i unikalnych wartości,
  • redukcja szumu zawartego w zbiorze - pochodna poprzedniego punktu - usuwamy ze zmiennych nieistotne/nadmiarowe informacje,
  • możliwość uwzględnienia braków danych – w przypadku zmiennych kategorycznych nie trzeba usuwać niekompletnych obserwacji, gdyż brak można ująć jako odrębną kategorię (dotyczy to przypadku, w którym nowa zmienna zastępuje starą),
  • poprawa wyniku bez utraty interpretowalności modelu – kategoryzacja jest jednym z zabiegów, po którego odpowiednim wykonaniu nie tracimy na interpretowalności modelu (np. „Klientowi o identyfikatorze xyz nie udzielono kredytu, gdyż znalazł się w grupie osób o zarobkach pomiędzy 1800 zł, a 2100 zł, która charakteryzuje się wysokim ryzykiem.”),
  • zapewnienie interpretowalności modelu – niekiedy zdarza się, że zmienna jest nieintuicyjna w swej interpretacji. Odpowiednia kategoryzacja pozwala niekiedy zapewnić monotoniczność zmiennej.

Nie ma jednak róży bez kolców, dlatego warto w tym miejscu wspomnieć o minusach:

  • utrata części informacji – w procesie dyskretyzacji tracona jest część informacji,
  • czasem jej implementacja wymaga dodatkowego nakładu pracy – w przypadku niektórych branż konieczne jest zapewnienie monotoniczności zmiennych, co powoduje, że w proces trzeba włożyć nieco więcej wysiłku.

1. Import bibliotek.

In [1]:
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.tree import DecisionTreeClassifier
from statsmodels.discrete.discrete_model import Logit

2. Wczytanie zbioru danych.

In [32]:
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)

Sprawdzam rozkład zmiennej celu.

In [33]:
y_tr[1].value_counts(normalize = True)
y_te[1].value_counts(normalize = True)
Out[33]:
0    0.764957
1    0.235043
Name: 1, dtype: float64

76.5% do 23.5% - ewidentnie możemy tu mówić o zbiorze niezbalansowanym. Bardzo dobry zbiór, by zobrazować, jak istotny jest wybór odpowiedniego punktu odcięcia.

3. Modelowania.

In [34]:
def podsumowanie_modelu(punkt_odciecia):
    tn, fp, fn, tp = confusion_matrix(y_te, prawd_rl>punkt_odciecia).ravel()
    specyficznosc = tn / (tn+fp)
    czulosc = tp / (tp + fn)
    return tn, fp, fn, tp
3.1. Punkt odcięcia = 0.5.
In [35]:
model_rl = Logit(y_tr, x_tr).fit()
prawd_rl = model_rl.predict(x_te)
Optimization terminated successfully.
         Current function value: 0.479973
         Iterations 7
In [36]:
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(3))
Gini score: 0.481
In [37]:
tn, fp, fn, tp = podsumowanie_modelu(0.5)
liczba_kredytow = tn + fn
print('Osiągnięta rentowność: {}%'.format(np.abs((tn * -15 + fn * 75)/liczba_kredytow).round(2)))
Osiągnięta rentowność: 2.12%
3.2. Punkt odcięcia - dobór metodą "złotego środka".
In [38]:
fpr, tpr, threshold = roc_curve(y_te, prawd_rl)
m = np.argmax(tpr - fpr)
nowy_punkt_odciecia = threshold[m]
In [39]:
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(3))
Gini score: 0.481
In [12]:
tn, fp, fn, tp = podsumowanie_modelu(nowy_punkt_odciecia)
liczba_kredytow = tn + fn
print('Osiągnięta rentowność: {}%'.format(np.abs((tn * -15 + fn * 75)/liczba_kredytow).round(2)))
Osiągnięta rentowność: 2.69%
3.3. Punkt odcięcia - dobór metodą uwzględniającą założenia biznesowe.
In [40]:
uczenie maszynowe,punkty_odciecia = np.arange(0, 1.01, 0.01)

wyniki = {}
for punkt in punkty_odciecia:
    tn, fp, fn, tp = confusion_matrix(y_te, prawd_rl>punkt).ravel()
    koszt = tn * -15 + fn * 75
    wyniki[punkt] = koszt
# Wyniki zapisuję w serii danych.
wyniki = pd.Series(wyniki)

Sprawdzam na której pozycji znajduje się najniższa wartość kosztu.

In [41]:
najnizszy_koszt = wyniki.iloc[wyniki.argmin()]
najlepszy_punkt_odciecia = wyniki.iloc[wyniki.argmin():wyniki.argmin()+1].index[0]
print('Najmniejszy koszt wynosi: {}. Odpowiadający mu punkt odcięcia wynosi: {}.'.format(najnizszy_koszt, najlepszy_punkt_odciecia))
Najmniejszy koszt wynosi: -11715. Odpowiadający mu punkt odcięcia wynosi: 0.23.
In [42]:
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(4))
Gini score: 0.481
In [43]:
tn, fp, fn, tp = podsumowanie_modelu(najlepszy_punkt_odciecia)
liczba_kredytow = tn + fn
print('Osiągnięta rentowność: {}%'.format(np.abs((tn * -15 + fn * 75)/liczba_kredytow).round(2)))
Osiągnięta rentowność: 3.78%
3.4. Punkt odcięcia metodą z punktu 3.3. + kategoryzacja zmiennych drzewem decyzyjnym.

Przygotowanie funkcji do kategoryzacji.

In [44]:
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

Wyznaczenie listy zmiennych do kategoryzacji.

In [45]:
# 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)

Budowa modelu.

In [46]:
model_rl = Logit(y_tr, x_tr).fit()
prawd_rl = model_rl.predict(x_te)
Optimization terminated successfully.
         Current function value: 0.452430
         Iterations 7

Poszukiwanie nowego punktu odcięcia.

In [47]:
punkty_odciecia = np.arange(0, 1.01, 0.01)

wyniki = {}
for punkt in punkty_odciecia:
    tn, fp, fn, tp = confusion_matrix(y_te, prawd_rl>punkt).ravel()
    koszt = tn * -15 + fn * 75
    wyniki[punkt] = koszt
# Wyniki zapisuję w serii danych.
wyniki = pd.Series(wyniki)

Sprawdzam na której pozycji znajduje się najniższa wartość kosztu.

In [48]:
najlepszy_punkt_odciecia = wyniki.iloc[wyniki.argmin():wyniki.argmin()+1].index[0]
In [50]:
gini = 2*roc_auc_score(y_te, prawd_rl)-1
print('Gini score:', gini.round(3))
Gini score: 0.566
In [27]:
tn, fp, fn, tp = podsumowanie_modelu(najlepszy_punkt_odciecia)
liczba_kredytow = tn + fn
print('Osiągnięta rentowność: {}%'.format(np.abs((tn * -15 + fn * 75)/liczba_kredytow).round(2)))
Osiągnięta rentowność: 6.31%

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. Porównanie wyników.

Punkt odcięcia = 0.5 Metoda "złotego środka" Metoda uwzględniająca biznes Metoda uwzględniająca biznes + kategoryzacja drzewem
Gini 0.481 0.481 0.481 0.566
Rentowność 2.12% 2.69% 3.78% 6.31%

5. Możliwe rozwinięcia.

Ten wpis nie wyczerpuje tematu kategoryzacji obserwacji. Istnieje wiele możliwych rozwinięć, które mogą wpłynąć na jakość procesu i finalny wynik. Można do nich zaliczyć m.in.:

  • Ustalenie maksymalnej liczby kategorii w nowej zmiennej – zabieg ten ma uchronić przed budowaniem zmiennych o dużej liczbie kategorii i zbyt wysokim dopasowaniem modelu do danych uczących.
  • Ustalenie minimalnej liczby obserwacji potrzebnych do zbudowania kategorii – wartość ta może być ujęta jako minimalny odsetek wszystkich obserwacji, które zostały przydzielone do danej kategorii.
  • Zmiana kodowania zmiennych kategorycznych – pod tym hasłem kryje się użycie tzw. Likelihood Encoding (WoE, mean encoding, count encoding, diff encoding) jako alternatywy dla tradycyjnego label encodingu. Ogólna zasada mówi, że im bardziej skomplikowana i mniej liniowa zależność pomiędzy zmienną celu a wybraną zmienną, tym większą wartość wnoszą wymienione metody.
  • Monotoniczność powstałej zmiennej – zadbanie o intuicyjność w interpretacji modelu i zgodność z obostrzeniami prawnymi narzuconymi przez regulatora danej branży. Należy zapewnić by wraz ze wzrostem danej zmiennej malało bądź rosło ryzyko. Sama monotoniczność może być również rozważana jako gwarant większej stabilności zmiennych.
  • Użycie alternatywnych metod kategoryzacji – drzewo decyzyjne jest jednym z wielu sposobów kategoryzacji zmiennych i istnieją inne metody, które w zależności od przypadku mogą dawać lepsze rezultaty.

6. Podsumowanie.

To koniec dzisiejszego wpisu. Po raz pierwszy bezpośrednio zaingerowałem dziś w poprawę statystyki Gini i przyniosło to wymierny efekt: Gini wzrosło o ponad 8 punktów, a rentowność o ok. 2.5%. Wszystko za pomocą jednej prostej metody: kategoryzacji zmiennych ciągłych drzewem decyzyjnym.

To jednak nie koniec prac nad tym modelem. Jest tu jeszcze sporo elementów do zmiany. To z czego nie jestem zadowolony, to liczba zmiennych w modelu i ich postać. Zbiór uczący obecnie jest mieszanką zmiennych ciągłych i dyskretnych, z których część jest kategoryzowana drzewem decyzyjnym i ma postać liczb całkowitych. Nie jest to coś, z czego byłbym szczególnie dumny, dlatego postaram się to zmienić. Będziesz mógł to zobaczyć już niebawem w kolejnych wpisach na blogu i filmach na kanale Data Science Plus. 😉

Linki:

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 🙂

Bądź pierwszy, który skomentuje ten wpis!

Dodaj komentarz

Twój adres email nie zostanie opublikowany.


*