Gdy zbiór zawiera zmienne numeryczne mamy do wyboru całą gamę algorytmów grupujących. Całość się nieco komplikuje, gdy w grę wchodzą zmienne kategoryczne. W tej sytuacji rozwiązaniem może być algorytm k-modes.
Podobnie jak w przypadku algorytmu k-median postanowiłem nie rozdzielać części teoretycznej od praktycznej. Przekładam praktykę ponad teorię, dlatego dosłownie w dwóch akapitach przedstawię najważniejsze informacje, o których należy wiedzieć przed użyciem k-modes w praktyce. Po nich przejdę do części praktycznej z użyciem przykładowego zbioru i biblioteki KModes. Zaczynamy! 🙂
Opis algorytmu¶
K-modes - najważniejsze informacje:
- Autor: Joshua Zhexue Huang.
- Rok publikacji: 1997.
- Mało popularny, niewiele informacji dostępnych w sieci.
- Jest on rozszerzeniem k-średnich dla zmiennych kategorycznych.
- Ang. mode (moda/dominanta/wartość modalna/wartość najczęstsza) - użyta jest jako reprezentant tendencji centralnej danej grupy. Jest to obiekt będący reprezentantem danej grupy obserwacji. Jest odpowiednikiem centroidu z algorytmu k-średnich.
- Zamiast dystansu (jak w k-średnich) używa on miary odmienności. Im mniejsza jej wartość, tym większe podobieństwo pomiędzy obserwacjami. Miara odmienności jest przedstawiona jako suma niedopasowań poszczególnych zmiennych kategorycznych pomiędzy obserwacjami.
- Implementacja algorytmu w Python jest dostępna w bibliotece KModes.
- Istnieją również inne algorytmy grupujące, które wspierają zmienne kategoryczne, lecz nie są one powszechnie używane.
- Rozwinięciem algorytmu dla zmiennych kategorycznych jest fuzzy k-modes. Zamiast przypisywać obserwację do jednej grupy, obliczane są wartości poziomów przynależności (patrz moje wpisy dotyczące zbiorów rozmytych) danej obserwacji do wszystkich grup.
- Rozwinięciem algorytmu dla mieszanych zmiennych kategorycznych i numerycznych jest fuzzy k-prototypes.
Być może zadajesz sobie pytanie: czy możemy wykonać grupowanie zmiennych dyskretnych z użyciem k-średnich? W ogólnym przypadku powinno się tego unikać. Transformacje zmiennych kategorycznych poprzez label encoding i użycie k-średnich jest bardzo złym pomysłem. Algorytm k-średnich używa odległości wyrażonych za pomocą wartości numerycznych, a dystans pomiędzy kat_a i kat_b wcale nie musi być równy dystansowi dzielącemu kat_b i kat_c.
K-średnich w przeszłości był używany do segmentacji zmiennych kategorycznych. Często proponowano by zmienne kategoryczne transformować na zmienne dummy (0,1). Jest to "mniejsze zło" niż użycie label encodingu. Największym problemem jest tu jednak wielowymiarowość danych.
Schemat działania algorytmu k-modes:
- Losowe wybranie k unikalnych obserwacji ze zbioru jako inicjalnych k-dominant.
- Przeliczenie miary odmienności pomiędzy dominantami a wszystkimi obserwacjami.
- Przypisanie obserwacji do najbardziej podobnej dominanty.
- Wybranie nowych k-dominant dla każdej grupy obserwacji. Jeżeli dominanty się nie zmieniły, to algorytm kończy działanie. W przeciwnym wypadku powtarzane są kroki 2, 3 i 4.
Warunki stopu:
- Osiągnięcie minimum lokalnego, a więc momentu, w którym przydział obserwacji do grup się nie zmienia.
- Osiągnięcie zakładanej liczby iteracji.
Kiedy go stosować?
Sprawdź również projekt, w którym wykonuję segmentację ze zbiorem zawierającym zmienne dyskretne.
Wady i zalety algorytmu¶
Stosując algorytm k-median warto mieć również na uwadze zarówno wszystkie jego ograniczenia, jak i mocne strony.
Wady:
- Wymaga ustalenia liczby grup – zanim uruchomimy algorytm, musimy a priori podać liczbę grup, które mają zostać wyznaczone. Bez uprzedniego wizualizowania zbioru lub wykonania dodatkowych analiz jest to dosyć trudne.
- Wrażliwy na dobór punktów startowych – w pierwszej iteracji swojego działania algorytm losowo dobiera punkty startowe (ew. możesz je ręcznie zdefiniować). To jak dobre wyniki uzyska, zależy zatem w pewnym stopniu od czynnika losowego.
- Używa jedynie zmiennych kategorycznych – algorytm nie wspiera zmiennych numerycznych. Istnieje oczywiście możliwość przeprowadzenia kategoryzacji zmiennych ciągłych (co wcale nie jest takim złym pomysłem i potrafi dać całkiem ciekawe rezultaty - będę o tym pisać w jednym z kolejnych wpisów), lub użyć algorytmu k-prototypes.
Zalety:
- Dosyć szybki – wynika to bezpośrednio ze sposobu jego działania. Niższa złożoność obliczeniowa sprawia, że w porównaniu np. z grupowaniem aglomeracyjnym, algorytm k-modes działa błyskawicznie. Wielkość zbioru przestaje więc być tak dużym problemem.
- Wspiera zmienne kategoryczne - jakie pierwszy z opisywanych przeze mnie na blogu algorytmów grupujących wspiera zmienne kategoryczne bez konieczności jakichkolwiek transformacji.
- Działa pomimo brakujących wartości - braki są uzupełniane automatycznie i tworzona jest z nich odrębna kategoria.
Wprowadzenie teoretyczne do algorytmów iteracyjno-optymalizacyjnych.
Przykład użycia¶
By pokazać działanie algorytmu, posłużę się biblioteką KModes. Poniżej kilka istotnych informacj na jej temat:
- Posiada ona ten sam styl budowania modeli, co scikit-learn. Jest zatem intuicyjna dla wszystkich użytkowników sklearna.
- Brakujące dane są uzupełniane automatycznie i traktowane jako odrębna kategoria (braki powinny być ujęte jako np.NaN) - jest to ułatwienie, choć nawet sam autor zaleca, by w większości przypadków lepiej zdecydować się na ręczne uzupełnianie braków zgodne z np. wiedzą biznesową.
Główne informacje o zbiorze użytym w przykładzie:
- Default of Credit Card Clients Data Set – UCI.
- Autor: I-Cheng Yeh.
- Dodano w 2016.01.26.
- 24 zmienne, 30 000 obserwacji.
Opis zmiennych,których użyję:
- X2: Plec (1 = mężczyzna; 2 = kobieta).
- X3: Edukacja (1 = wyzsze_pelne, link_2; 2 = wyzsze; 3 = srednie; 4 = inne).
- X4: Stan_cywilny (1 = w_zwiazku; 2 = kawaler_panna; 3 = inny).
1. Wczytuję kilka niezbędnych bibliotek.¶
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from kmodes.kmodes import KModes
2. Wczytuję zbiór.¶
ef = pd.ExcelFile('data/credit_card_default.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)
3. Zamieniam wartości jakie przyjmują poszczególne zmienne.¶
df.plec.replace([1,2], ['kobieta', 'mezczyzna'], inplace = True)
df.wyksztalcenie.replace([0, 1, 2, 3, 4, 5, 6], ['nieznane', 'wyzsze_pelne', 'wyzsze', 'srednie', 'inne', 'nieznane', 'nieznane'], inplace = True)
df.stan_cywilny.replace([0, 1, 2, 3], ['nieznany', 'w_zwiazku', 'kawaler_panna', 'inny'], inplace = True)
4. Ograczam zbiór do trzech zmiennych, którymi się posłużę.¶
df = df[['plec', 'wyksztalcenie', 'stan_cywilny']]
df.head()
5. Sprawdzam na ile grup podzielić zbiór.¶
res = []
for n in range(1, 20):
km = KModes(n_clusters=n, init='Huang', n_init=30, n_jobs=4)
km.fit_predict(df)
res.append([n, km.cost_])
res = pd.DataFrame(res, columns=[0, 'wspolcz_odm']).set_index(0)
plt.figure(figsize=(10,7))
sns.set(font_scale=1.4, style="whitegrid")
sns.lineplot(data = res, palette = ['#eb6c6a']).set(title = "Miara odmienności grup vs liczba grup")
plt.show()
Na podstawie wykresu i powyższej tabeli decyduję się na podział zbioru na 7 segmentów.
6. Przeprowadzam grupowanie.¶
km = KModes(n_clusters=7, init='Huang', n_init=30, n_jobs=4)
clusters = km.fit_predict(df)
7. Przygotowuję dane do końcowej analizy.¶
df = df.assign(segment = clusters)
df.segment = df.segment.astype(str)
8. Analiza wyników grupowania.¶
Analiza całego zbioru.
Prześledzę rozkłady całego zbioru, tak by mieć punkt odniesienia dla poszczególnych grup.
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((df[column].value_counts(normalize = True) * 100).round(2))
print('')
Segment 0.
segment_0 = df[df.segment == "0"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_0[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - mieszanka kobiet i mężczyzn. Brak wyraźnej dyskryminacji wg tej zmiennej.
- Zmienna "wyksztalcenie" - zdecydowana przewaga osób o średnim wykształceniu (najniższe z deklarowanych).
- Zmienna "stan_cywilny" - przewaga osób będących w związku.
Segment 0 to klienci banku będący w stałych związkach, o średnim wykształceniu.
Segment 1.
segment_1 = df[df.segment == "1"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_1[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - mieszanka kobiet i mężczyzn. Brak wyraźnej dyskryminacji wg tej zmiennej.
- Zmienna "wyksztalcenie" - wyłącznie osoby o wykształceniu wyższym, pełnym.
- Zmienna "stan_cywilny" - przewaga osób będących w związku.
Segment 1 to klienci banku będący w stałych związkach, o wyższym, pełnym wykształceniu.
Segment 2.
segment_2 = df[df.segment == "2"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_2[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - sami mężczyźni.
- Zmienna "wyksztalcenie" - zdecydowana przewaga osób o wyższym wykształceniu.
- Zmienna "stan_cywilny" - przewaga kawalerów/panien.
Segment 2 to klienci banku będący kawalerami o wyższym wykształceniu.
Segment 3.
segment_3 = df[df.segment == "3"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_3[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - mieszanka kobiet i mężczyzn. Brak wyraźnej dyskryminacji wg tej zmiennej.
- Zmienna "wyksztalcenie" - tylko osoby o wyższym, pełnym wykształceniu.
- Zmienna "stan_cywilny" - tylko osoby nie będące w stałym związku.
Segment 3 to klienci banku nie będący w stałych związkach, o wykształceniu wyższym, pełnym. (Od segmentu 1 różni ich stan cywilny).
Segment 4.
segment_4 = df[df.segment == "4"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_4[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - same kobiety.
- Zmienna "wyksztalcenie" - zdecydowana przewaga osób o wykształceniu wyższym.
- Zmienna "stan_cywilny" - przewaga osób będących w związku.
Segment 4 to zamężne klientki banku, o wyższym wykształceniu.
Segment 5.
segment_5 = df[df.segment == "5"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_5[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - same kobiety.
- Zmienna "wyksztalcenie" - przewaga osób o wyższym wykształceniu.
- Zmienna "stan_cywilny" - przewaga osób będących w związku.
Segment 5 to niezamężne klientki banku o średnim wykształceniu. (Od segmentu 4 odróżnia je stan cywilny).
Segment 6.
segment_6 = df[df.segment == "6"]
for column in ['plec', 'wyksztalcenie', 'stan_cywilny']:
print((segment_6[column].value_counts(normalize = True) * 100).round(2))
print('')
Charakterystyka segmentu:
- Zmienna "plec" - sami mężczyźni.
- Zmienna "wyksztalcenie" - tylko wyższe wykształcenie.
- Zmienna "stan_cywilny" - sami żonaci.
Segment 5 to żonaci klienci banku o wyższym wykształceniu.
9. Pogrupowane wyniki segmentacji.¶
Zmienna "plec".
((df.groupby(['plec', 'segment'])['segment'].count().unstack().fillna(0)/df['segment'].value_counts())*100).round(2)
Zmienna "wyksztalcenie".
((df.groupby(['wyksztalcenie', 'segment'])['segment'].count().unstack().fillna(0)/df['segment'].value_counts())*100).round(2)
Zmienna "stan_cywilny".
((df.groupby(['stan_cywilny', 'segment'])['segment'].count().unstack().fillna(0)/df['segment'].value_counts())*100).round(2)
Podsumowanie¶
W kolejnych wpisach będę kontynuować temat grupowania i pokażę już ostatni algorytm z rodziny algorytmów iteracyjno-optymalizacyjnych. Będzie to algorytm hybrydowy, będący połączeniem k-średnich i opisywanego w tym wpisie k-modes: k-prototypes. 🙂
Jeśli chcesz zaczerpnąć wiedzy na temat k-modes "u źródła", to zerknij proszę poniżej do sekcji "Linki". Zawarłem w niej odnośniki do źródeł, które kiedyś pomogły mi dogłębnie zrozumieć działanie k-modes i przygotować się do napisania tego wpisu.
Jeśli masz jakieś pytania, to proszę podziel się nimi w komentarzu pod wpisem. Na wszystkie postaram się odpowiedzieć. Zapraszam do dyskusji. 🙂
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 :-)
W jaki sposób został obliczony współczynnik odmienności? Czy to jest współczynnik SW/SB, czy jakaś inna metryka?