Postanowiłem zrobić mały eksperyment i porównać działanie dwóch algorytmów na tym samym zbiorze. Czy k-średnich poradzi sobie lepiej ze zmiennymi ciągłymi, niz k-modes z kategoryzowanymi zmiennymi ciągłymi?
Oba algorytmy były już dosyć szczegółowo opisywane na blogu. (k-średnich, k-modes). W tym wpisie sprawdzę ich działanie na tym samym zbiorze składającym się z kilku zmiennych ciągłych. Zaczynamy!
Główne założenia eksperymentu:
- Oba algorytmy przetestuję na tym samym zbiorze, na tym samym zestawie zmiennych.
- Na podstawie zbioru "Credit Approval - UCI" przygotuję dwa zbiory dostosowane do wymagań obu algorytmów.
- Użyję sześciu wybranych zmiennych: 'Wiek', 'Zadłużenie', 'Lata_pracy', 'Score_kredytowy', 'Saldo_konta', 'Przychody'.
- Przetestuję jeden zbiór w 6 wariantach:
- surowe zmienne ciągłe,
- zmienne ciągłe poddane procesom standaryzacji i usuwania skośności,
- surowe zmienne ciągłe poddane procesowi dyskretyzacji decylowej,
- surowe zmienne ciągłe poddane procesowi dyskretyzacji kwartylowej,
- zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji decylowej,
- zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji kwartylowej.
- Potencjał zbiorów przed wykonaniem grupowania sprawdzę za pomocą statystyki Hopkinsa, która sprawdza się zarówno dla zmiennych ciągłych, jak i dyskretnych.
- Liczbę grup wybiorę z użyciem wykresu łokcia (6 wykresów i wybranie średniej liczby segmentów).
- Jakoś grupowania zweryfikuję za pomocą kilku statystyk:
- Silhouette Score.
- Calinski-Harabasz Score.
- Davies Bouldin Score.
- O finalnej ocenie zadecyduje suma głosów poszczególnych statystyk:
- na etapie oceny potencjału zbioru,
- po wykonaniu grupowania.
Główne informacje o zbiorze użytym w przykładzie:
- Credit Approval - UCI.
- Submited by: John Ross Quinlan
- 16 zmiennych, 690 obserwacji.
Po zamianie nazwy zmiennych, wraz z ich typami wyglądają następująco:
- Płeć – binarna,
- Wiek – ciągła,
- Zadłużenie – ciągła,
- Stan_cywilny – nominalna,
- Bank – nominalna,
- Wykształcenie – nominalna,
- Pochodzenie – nominalna,
- Lata_pracy – ciągła,
- Poz_hist_kred – binarna,
- Umowa_o_pracę – binarna,
- Score_kredytowy – ciągła,
- Prawo_jazdy – binarna,
- Obywatelstwo – nominalna,
- Saldo_konta – ciągła,
- Przychody – ciągła,
- Wynik – binarna.
1. Wczytuję kilka niezbędnych bibliotek.¶
import numpy as np
import pandas as pd
import seaborn as sns
import sklearn.metrics
import matplotlib.pyplot as plt
from kmodes.kmodes import KModes
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler, Normalizer
pd.options.display.max_columns = 50
2. Wczytuję zbiór.¶
df = pd.read_csv('data/crx.data', header=None, names=['Płeć','Wiek','Zadłużenie','Stan_cywilny','Bank','Wykształcenie','Pochodzenie','Lata_pracy','Poz_hist_kred','Umowa_o_pracę','Score_kredytowy','Prawo_jazdy','Obywatelstwo','Saldo_konta','Przychody','Wynik'], decimal='.', na_values='?', dtype = {'Płeć':'category','Stan_cywilny':'category','Bank':'category','Wykształcenie':'category','Pochodzenie':'category','Poz_hist_kred':'category','Umowa_o_pracę':'category','Prawo_jazy':'category','Obywatelstwo':'category','Wynik':'category'})
3. Ograczam zbiór do czterech zmiennych, którymi się posłużę.¶
df = df[['Wiek', 'Zadłużenie', 'Lata_pracy', 'Score_kredytowy', 'Saldo_konta', 'Przychody']]
4. Usuwam obserwacje zawierające brakujące wartości.¶
df.dropna(inplace=True)
5. Podgląd podstawowych statystyk zbioru.¶
df.head()
df.describe()
Zbiór zawiera wartości zerowe, co nieco utrudni proces przygotowania zmiennych.
6. Przygotowanie zbiorów.¶
6.1. Zbiór 1 - surowe zmienne ciągłe.¶
df_1 = df.copy()
6.2. Zbiór 2 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności.¶
df_2 = np.log(df+0.01)
scaler = StandardScaler()
scaler.fit(df_2)
df_2 = pd.DataFrame(scaler.transform(df_2.values), columns = df_2.columns)
6.3. Zbiór 3 - surowe zmienne ciągłe poddane procesowi dyskretyzacji decylowej.¶
df_3 = df.copy()
for col in df.columns:
df_3[col] = pd.qcut(df_3[col], 10, duplicates = "drop")
df_3[col].replace(df_3[col].unique(), np.arange(1, df_3[col].unique().shape[0]+1), inplace = True)
df_3[col] = df_3[col].astype(str)
6.4. Zbiór 4 - surowe zmienne ciągłe poddane procesowi dyskretyzacji kwartylowej.¶
df_4 = df.copy()
for col in df.columns:
df_4[col] = pd.qcut(df_4[col], 4, duplicates = "drop")
df_4[col].replace(df_4[col].unique(), np.arange(1, df_4[col].unique().shape[0]+1), inplace = True)
df_4[col] = df_4[col].astype(str)
6.5. Zbiór 5 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji decylowej.¶
df_5 = df_2.copy()
for col in df.columns:
df_5[col] = pd.qcut(df_5[col], 10, duplicates = "drop")
df_5[col].replace(df_5[col].unique(), np.arange(1, df_5[col].unique().shape[0]+1), inplace = True)
df_5[col] = df_5[col].astype(str)
6.6. Zbiór 6 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji kwartylowej.¶
df_6 = df_2.copy()
for col in df.columns:
df_6[col] = pd.qcut(df_6[col], 4, duplicates = "drop")
df_6[col].replace(df_6[col].unique(), np.arange(1, df_6[col].unique().shape[0]+1), inplace = True)
df_6[col] = df_6[col].astype(str)
7. Ocena potencjału poszczególnych zbiorów - statystyka Hopkinsa.¶
Do oceny potencjały zbiorów użyje statystyki Hopkinsa, której opis znajduje się tutaj. Jej implementacja nie jest dostępna w żadnej znanej mi bibliotece Python-a, lecz na szczęście ktoś udostępnił jej implementację na GitHub, a kolejna osoba udoskonaliła ją i opublikowała na swoim blogu.
To, co nas będzie interesować, to końcowy wynik. Statystyka przyjmuje wartości z przedziału [0, 1]. Im bliżej 1, tym większy potencjał drzemie w danych pod kątem segmentacji.
Eksperyment ten pozwoli ocenić wpływ przygotowania danych na potencjał zbioru.
from sklearn.neighbors import NearestNeighbors
from random import sample
from numpy.random import uniform
import numpy as np
from math import isnan
def hopkins(X):
d = X.shape[1]
n = len(X)
m = int(0.1 * n)
nbrs = NearestNeighbors(n_neighbors=1).fit(X.values)
rand_X = sample(range(0, n, 1), m)
ujd = []
wjd = []
for j in range(0, m):
u_dist, _ = nbrs.kneighbors(uniform(np.amin(X,axis=0),np.amax(X,axis=0),d).reshape(1, -1), 2, return_distance=True)
ujd.append(u_dist[0][1])
w_dist, _ = nbrs.kneighbors(X.iloc[rand_X[j]].values.reshape(1, -1), 2, return_distance=True)
wjd.append(w_dist[0][1])
H = sum(ujd) / (sum(ujd) + sum(wjd))
if isnan(H):
print(ujd, wjd)
H = 0
return H
datasets = [df_1, df_2, df_3, df_4, df_5, df_6]
for i in range(0,6):
print("Zbiór numer {}. Osiągnięty wynik: {}.".format(i+1, hopkins(datasets[i]).round(3)))
Dwa najlepsze wyniki, to zbiory zawierające zmienne numeryczne. Najlepszy wynik osiągnął zbiór numer 1, czyli "surowe" zmienne numeryczne, przed standaryzacją i usuwaniem skośności. Drugi najlepszy rezultat, to zmienne surowe poddane procesowi standaryzacji i usuwania skośności.
Dalsze miejsca zajmują zbiory składające się ze zmiennych kategorycznych. Da się zauważyć ciekawą zależność: kategoryzacja kwartylowa > kategoryzacja decylowa. Dodatkowo najgorszy wynik osiągnął zbiór składający się ze zmiennych kategorycznych zbudowanych na podstawie surowych zmiennych ciągłych.
Jest to dosyć mała próba, by wyciągać dalekosiężne wnioski, dlatego powyższe wyniki nie muszą być aplikowalne dla wszystkich przypadków. Ja pozwolę sobie na kilka punktów podsumowania wpływu przygotowania danych na potencjał zbioru przed segmentacją:
- zmienne numeryczne > zmienne kategoryczne (zbudowane na podstawie zmiennych numerycznych w procesie kategoryzacji),
- proces standaryzacji i usuwania skośności negatywnie wpływa na potencjał zbioru pod kątem segmentacji (uwaga, korzystając z większości algorytmów i tak powinniśmy wykonywać te procesy - dzięki nim dostosowujemy zbiór do wymagań i założeń algorytmów grupujących),
- kategoryzacja kwartylowa > kategoryzacja decylowa,
- poddanie zmiennych ciągłych procesom standaryzacji i usuwania skośności bez wykonania kategoryzacji negatywnie wpływa na wynik.
8. Sprawdzam na ile grup podzielić zbiór.¶
for i in range(0,2):
res = []
for n in range(1, 21):
# clustering
kmeans = KMeans(n_clusters=n)
kmeans.fit(datasets[i])
res.append([n, kmeans.inertia_])
res = pd.DataFrame(res, columns=[0, 'wspolcz_odm']).set_index(0)
# plotting results
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 - zbiór {}.".format(i+1))
plt.show()
Dla pierwszego zbioru "łokieć" wskazuje 4 segmenty. Dla drugiego chyba tyle samo.
for i in range(2,6):
res = []
for n in range(1, 21):
# clustering
km = KModes(n_clusters=n, init='Huang', n_init=20, n_jobs=4)
km.fit_predict(df)
res.append([n, km.cost_])
res = pd.DataFrame(res, columns=[0, 'wspolcz_odm']).set_index(0)
# plotting results
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 - zbiór {}.".format(i+1))
plt.show()
Dla czterech powyższych zbiorów bardzo ciężko wskazać "łokieć", lecz jest to z pewnością wartość bliższa 10, niż 4. Uśrednijmy zatem do 7. 🙂
ps. ciekawe, że dla zmiennych kategoryzowanych na podstawie zmiennych standaryzowanych wyniki są nieco bardziej "wygładzone" i stabilniejsze.
9. Grupowanie.¶
for i in range(0,2):
kmeans = KMeans(n_clusters=7)
kmeans.fit(datasets[i])
df['cluster_{}'.format(i+1)] = kmeans.labels_
for i in range(2,6):
km = KModes(n_clusters=7, init='Huang', n_init=10, n_jobs=4)
df['cluster_{}'.format(i+1)] = km.fit_predict(datasets[i])
10. Weryfikacja jakości grupowania.¶
res = []
for i in range(1,7):
cluster_metrics = [metrics.calinski_harabaz_score(df[['Wiek', 'Zadłużenie', 'Lata_pracy', 'Score_kredytowy', 'Saldo_konta', 'Przychody']], df['cluster_{}'.format(i)]),
metrics.silhouette_score(df[['Wiek', 'Zadłużenie', 'Lata_pracy', 'Score_kredytowy', 'Saldo_konta', 'Przychody']], df['cluster_{}'.format(i)]),
metrics.davies_bouldin_score(df[['Wiek', 'Zadłużenie', 'Lata_pracy', 'Score_kredytowy', 'Saldo_konta', 'Przychody']], df['cluster_{}'.format(i)])]
res.append(cluster_metrics)
results = pd.DataFrame(res, columns = ['Calinski_Harabasz', 'Silhouette_Score', 'Davies_Bouldin'])
ranking = results.copy()
ranking['Calinski_Harabasz'] = ranking['Calinski_Harabasz'].rank(ascending = False)
ranking['Silhouette_Score'] = ranking['Silhouette_Score'].rank(ascending = False)
ranking['Davies_Bouldin'] = ranking['Davies_Bouldin'].rank(ascending = True)
ranking['score'] = ranking.sum(axis = 1)
ranking['place'] = ranking.score.rank(ascending = True)
ranking
Są dwa trzecie miejsca. Wykonam zaten normalizację, zsumuję wyniki i ponownie wykonam ranking.
ranking = results.copy()
normalizer = Normalizer()
ranking = pd.DataFrame(normalizer.fit_transform(results), columns = results.columns)
ranking['Davies_Bouldin'] = -ranking['Davies_Bouldin']
ranking['score'] = ranking.sum(axis = 1)
ranking['place'] = ranking.score.rank(ascending = False)
ranking
Finalne wyniki wyglądają następująco:
- Miejsce 1 - zbiór numer 1 - surowe zmienne ciągłe.
- Miejsce 2 - zbiór numer 4 - surowe zmienne ciągłe poddane procesowi dyskretyzacji kwartylowej.
- Miejsce 3 - zbiór numer 2 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności.
- Miejsce 4 - zbiór numer 6 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji kwartylowej.
- Miejsce 5 - zbiór numer 5 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji decylowej.
- Miejsce 6 - zbiór numer 3 - surowe zmienne ciągłe poddane procesowi dyskretyzacji decylowej.
Dla przypomnienia ocena potencjału zbiorów wyglądała następująco:
- Miejsce 1 - zbiór numer 1 - surowe zmienne ciągłe.
- Miejsce 2 - zbiór numer 2 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności.
- Miejsce 3 - zbiór numer 4 - surowe zmienne ciągłe poddane procesowi dyskretyzacji kwartylowej.
- Miejsce 4 - zbiór numer 6 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji kwartylowej.
- Miejsce 5 - zbiór numer 5 - zmienne ciągłe poddane procesom standaryzacji i usuwania skośności, a następnie dyskretyzacji decylowej.
- Miejsce 6 - zbiór numer 3 - surowe zmienne ciągłe poddane procesowi dyskretyzacji decylowej.
W obu rankingach jedyna różnica, to zamiana miejsc pomiędzy zbiorami 2 i 4.
Kilka dodatkowych wniosków na podstawie powyższych wyników:
- kategoryzacja kwartylowa > kategoryzacja decylowa - kategoryzacja decylowa nie jest najlepszym pomysłem,
- poddanie zmiennych ciągłych procesom standaryzacji i usuwania skośności negatywnie wpływa na wynik.
Podsumowanie¶
Mam nadzieję, że powyższy eksperyment przypadł Ci do gustu i być może okaże się pomocny w Twojej pracy.
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 :-)
Dodaj komentarz