K-Means — grupowanie klientów bez etykiet

🎯 Czym jest K-Means?

K-Means to algorytm uczenia nienadzorowanego, który automatycznie grupuje podobne dane w klastry. Nie potrzebujesz etykiet - algorytm sam znajduje wzorce i dzieli dane na K grup.

💡 Intuicja

Wyobraź sobie imprezę, gdzie ludzie sami grupują się w kręgi na podstawie wspólnych zainteresowań. K-Means działa podobnie - znajduje “centra grup” i przydziela każdy punkt do najbliższego centrum.


🛍️ Praktyczny przykład: segmentacja klientów sklepu

Jak pogrupować klientów do targetowanych kampanii marketingowych?

import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
import seaborn as sns

# Tworzenie realistycznych danych klientów e-commerce
np.random.seed(42)
n_customers = 1000

# Generujemy różne typy klientów z wyraźnymi wzorcami
customer_types = np.random.choice(['premium', 'average', 'bargain', 'sporadic'], n_customers, 
                                 p=[0.15, 0.35, 0.35, 0.15])

data = pd.DataFrame({
    'wiek': np.random.randint(18, 70, n_customers),
    'roczny_dochod': np.random.normal(50000, 20000, n_customers),
    'wydatki_roczne': np.random.normal(25000, 15000, n_customers),
    'liczba_zakupow': np.random.randint(1, 50, n_customers),
    'sredni_koszt_zakupu': np.random.normal(200, 100, n_customers),
    'lata_jako_klient': np.random.randint(0, 10, n_customers),
    'typ_klienta': customer_types
})

# Realistyczne korelacje między zmiennymi
for i, typ in enumerate(data['typ_klienta']):
    if typ == 'premium':
        data.loc[i, 'roczny_dochod'] = np.random.normal(80000, 15000)
        data.loc[i, 'wydatki_roczne'] = np.random.normal(45000, 10000)
        data.loc[i, 'liczba_zakupow'] = np.random.randint(20, 50)
        data.loc[i, 'sredni_koszt_zakupu'] = np.random.normal(400, 100)
    elif typ == 'bargain':
        data.loc[i, 'roczny_dochod'] = np.random.normal(30000, 10000)
        data.loc[i, 'wydatki_roczne'] = np.random.normal(8000, 3000)
        data.loc[i, 'liczba_zakupow'] = np.random.randint(15, 40)
        data.loc[i, 'sredni_koszt_zakupu'] = np.random.normal(80, 30)
    elif typ == 'sporadic':
        data.loc[i, 'roczny_dochod'] = np.random.normal(45000, 15000)
        data.loc[i, 'wydatki_roczne'] = np.random.normal(5000, 2000)
        data.loc[i, 'liczba_zakupow'] = np.random.randint(1, 8)
        data.loc[i, 'sredni_koszt_zakupu'] = np.random.normal(300, 150)

# Czyść dane - usuń wartości ujemne
data['roczny_dochod'] = np.maximum(data['roczny_dochod'], 15000)
data['wydatki_roczne'] = np.maximum(data['wydatki_roczne'], 1000)
data['sredni_koszt_zakupu'] = np.maximum(data['sredni_koszt_zakupu'], 20)

print("Statystyki klientów:")
print(data[['wiek', 'roczny_dochod', 'wydatki_roczne', 'liczba_zakupow', 'sredni_koszt_zakupu']].describe())
print(f"\nRozkład typów klientów:")
print(data['typ_klienta'].value_counts())
Statystyki klientów:
              wiek  roczny_dochod  wydatki_roczne  liczba_zakupow  \
count  1000.000000    1000.000000     1000.000000      1000.00000   
mean     43.359000   46988.933946    19816.768355        24.00700   
std      14.817276   22175.621844    17169.303009        13.41898   
min      18.000000   15000.000000     1000.000000         1.00000   
25%      30.000000   29854.699281     6462.783773        15.00000   
50%      43.000000   42533.564242    11041.836565        25.00000   
75%      56.000000   62388.416902    33188.043332        35.00000   
max      69.000000  122502.434178    74817.934938        49.00000   

       sredni_koszt_zakupu  
count          1000.000000  
mean            203.883750  
std             143.706490  
min              20.000000  
25%              83.839117  
50%             162.389656  
75%             303.607754  
max             763.483441  

Rozkład typów klientów:
typ_klienta
bargain     344
average     337
premium     166
sporadic    153
Name: count, dtype: int64

🔧 Budowanie modelu krok po kroku

1) Przygotowanie danych

# Wybierz features do clusteringu (bez typ_klienta - to chcemy odkryć!)
features = ['wiek', 'roczny_dochod', 'wydatki_roczne', 'liczba_zakupow', 
           'sredni_koszt_zakupu', 'lata_jako_klient']
X = data[features]

# Standaryzacja - BARDZO WAŻNE w K-Means!
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print("Przed standaryzacją:")
print(X.describe())

print("\nPo standaryzacji (pierwsze 5 wierszy):")
X_scaled_df = pd.DataFrame(X_scaled, columns=features)
print(X_scaled_df.head())
print("\nŚrednie po standaryzacji:", X_scaled_df.mean().round(3))
print("Odchylenia po standaryzacji:", X_scaled_df.std().round(3))
Przed standaryzacją:
              wiek  roczny_dochod  wydatki_roczne  liczba_zakupow  \
count  1000.000000    1000.000000     1000.000000      1000.00000   
mean     43.359000   46988.933946    19816.768355        24.00700   
std      14.817276   22175.621844    17169.303009        13.41898   
min      18.000000   15000.000000     1000.000000         1.00000   
25%      30.000000   29854.699281     6462.783773        15.00000   
50%      43.000000   42533.564242    11041.836565        25.00000   
75%      56.000000   62388.416902    33188.043332        35.00000   
max      69.000000  122502.434178    74817.934938        49.00000   

       sredni_koszt_zakupu  lata_jako_klient  
count          1000.000000       1000.000000  
mean            203.883750          4.550000  
std             143.706490          2.914789  
min              20.000000          0.000000  
25%              83.839117          2.000000  
50%             162.389656          5.000000  
75%             303.607754          7.000000  
max             763.483441          9.000000  

Po standaryzacji (pierwsze 5 wierszy):
       wiek  roczny_dochod  wydatki_roczne  liczba_zakupow  \
0  1.393733       0.346806        0.136732        0.894181   
1 -0.969556       0.499661       -0.675331       -1.342577   
2 -0.699466      -1.432630       -0.608789       -0.224198   
3 -0.159286      -1.443249       -0.634140        0.223154   
4 -0.496898      -1.443249       -0.966417        0.894181   

   sredni_koszt_zakupu  lata_jako_klient  
0             0.053442          1.184211  
1             1.809673         -1.561786  
2            -0.914825          0.154462  
3            -1.101092         -1.561786  
4            -0.652771         -0.875287  

Średnie po standaryzacji: wiek                  -0.0
roczny_dochod         -0.0
wydatki_roczne         0.0
liczba_zakupow        -0.0
sredni_koszt_zakupu    0.0
lata_jako_klient       0.0
dtype: float64
Odchylenia po standaryzacji: wiek                   1.001
roczny_dochod          1.001
wydatki_roczne         1.001
liczba_zakupow         1.001
sredni_koszt_zakupu    1.001
lata_jako_klient       1.001
dtype: float64

2) Znajdowanie optymalnej liczby klastrów

# Metoda łokcia (Elbow Method)
inertias = []
silhouette_scores = []
K_range = range(2, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X_scaled)
    inertias.append(kmeans.inertia_)
    silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))

# Znajdź optymalne K
best_k = K_range[np.argmax(silhouette_scores)]
print(f"Optymalna liczba klastrów (Silhouette): {best_k}")

print("\nWyniki dla różnych K:")
for k, inertia, sil_score in zip(K_range, inertias, silhouette_scores):
    print(f"K={k}: Inertia={inertia:.0f}, Silhouette={sil_score:.3f}")
Optymalna liczba klastrów (Silhouette): 2

Wyniki dla różnych K:
K=2: Inertia=4416, Silhouette=0.281
K=3: Inertia=3691, Silhouette=0.232
K=4: Inertia=3296, Silhouette=0.200
K=5: Inertia=3018, Silhouette=0.196
K=6: Inertia=2828, Silhouette=0.195
K=7: Inertia=2665, Silhouette=0.176
K=8: Inertia=2526, Silhouette=0.177
K=9: Inertia=2403, Silhouette=0.181
K=10: Inertia=2286, Silhouette=0.176

3) Trenowanie finalnego modelu

# Trenuj model z optymalnym K
final_kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
cluster_labels = final_kmeans.fit_predict(X_scaled)

# Dodaj etykiety klastrów do danych
data['klaster'] = cluster_labels

print(f"Model K-Means wytrenowany z K={best_k}")
print(f"Silhouette Score: {silhouette_score(X_scaled, cluster_labels):.3f}")

# Analiza klastrów
print("\nRozmiary klastrów:")
cluster_counts = pd.Series(cluster_labels).value_counts().sort_index()
for i, count in enumerate(cluster_counts):
    print(f"Klaster {i}: {count} klientów ({count/len(data)*100:.1f}%)")
Model K-Means wytrenowany z K=2
Silhouette Score: 0.281

Rozmiary klastrów:
Klaster 0: 275 klientów (27.5%)
Klaster 1: 725 klientów (72.5%)

📊 Analiza i interpretacja klastrów

import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
import seaborn as sns

# Przygotuj dane (powtarzamy dla kompletności)
np.random.seed(42)
n_customers = 1000

customer_types = np.random.choice(['premium', 'average', 'bargain', 'sporadic'], n_customers, 
                                 p=[0.15, 0.35, 0.35, 0.15])

data = pd.DataFrame({
    'wiek': np.random.randint(18, 70, n_customers),
    'roczny_dochod': np.random.normal(50000, 20000, n_customers),
    'wydatki_roczne': np.random.normal(25000, 15000, n_customers),
    'liczba_zakupow': np.random.randint(1, 50, n_customers),
    'sredni_koszt_zakupu': np.random.normal(200, 100, n_customers),
    'lata_jako_klient': np.random.randint(0, 10, n_customers),
    'typ_klienta': customer_types
})

# Popraw dane według typów
for i, typ in enumerate(data['typ_klienta']):
    if typ == 'premium':
        data.loc[i, 'roczny_dochod'] = np.random.normal(80000, 15000)
        data.loc[i, 'wydatki_roczne'] = np.random.normal(45000, 10000)
        data.loc[i, 'liczba_zakupow'] = np.random.randint(20, 50)
        data.loc[i, 'sredni_koszt_zakupu'] = np.random.normal(400, 100)
    elif typ == 'bargain':
        data.loc[i, 'roczny_dochod'] = np.random.normal(30000, 10000)
        data.loc[i, 'wydatki_roczne'] = np.random.normal(8000, 3000)
        data.loc[i, 'liczba_zakupow'] = np.random.randint(15, 40)
        data.loc[i, 'sredni_koszt_zakupu'] = np.random.normal(80, 30)
    elif typ == 'sporadic':
        data.loc[i, 'roczny_dochod'] = np.random.normal(45000, 15000)
        data.loc[i, 'wydatki_roczne'] = np.random.normal(5000, 2000)
        data.loc[i, 'liczba_zakupow'] = np.random.randint(1, 8)
        data.loc[i, 'sredni_koszt_zakupu'] = np.random.normal(300, 150)

data['roczny_dochod'] = np.maximum(data['roczny_dochod'], 15000)
data['wydatki_roczne'] = np.maximum(data['wydatki_roczne'], 1000)
data['sredni_koszt_zakupu'] = np.maximum(data['sredni_koszt_zakupu'], 20)

# Przygotuj model
features = ['wiek', 'roczny_dochod', 'wydatki_roczne', 'liczba_zakupow', 
           'sredni_koszt_zakupu', 'lata_jako_klient']
X = data[features]

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Znajdź optymalne K
silhouette_scores = []
K_range = range(2, 11)
for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X_scaled)
    silhouette_scores.append(silhouette_score(X_scaled, kmeans.labels_))

best_k = K_range[np.argmax(silhouette_scores)]

# Trenuj finalny model
final_kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
cluster_labels = final_kmeans.fit_predict(X_scaled)
data['klaster'] = cluster_labels

# Analiza charakterystyk klastrów
cluster_analysis = data.groupby('klaster')[features].mean()

print("CHARAKTERYSTYKI KLASTRÓW:")
print("=" * 50)

for cluster_id in range(best_k):
    print(f"\nKLASTER {cluster_id}:")
    cluster_data = data[data['klaster'] == cluster_id]
    
    print(f"Liczba klientów: {len(cluster_data)} ({len(cluster_data)/len(data)*100:.1f}%)")
    print(f"Średni wiek: {cluster_data['wiek'].mean():.0f} lat")
    print(f"Średni dochód: {cluster_data['roczny_dochod'].mean():.0f} zł")
    print(f"Średnie wydatki: {cluster_data['wydatki_roczne'].mean():.0f} zł")
    print(f"Średnia liczba zakupów: {cluster_data['liczba_zakupow'].mean():.0f}")
    print(f"Średni koszt zakupu: {cluster_data['sredni_koszt_zakupu'].mean():.0f} zł")
    
    # Nadaj nazwę klastrowi na podstawie charakterystyk
    avg_income = cluster_data['roczny_dochod'].mean()
    avg_spending = cluster_data['wydatki_roczne'].mean()
    avg_frequency = cluster_data['liczba_zakupow'].mean()
    
    if avg_income > 60000 and avg_spending > 30000:
        cluster_name = "🌟 PREMIUM CUSTOMERS"
    elif avg_frequency < 10 and avg_spending < 10000:
        cluster_name = "😴 SPORADYCZNI KLIENCI"
    elif avg_spending / avg_income < 0.3 and avg_frequency > 20:
        cluster_name = "💰 BARGAIN HUNTERS"
    else:
        cluster_name = "🔄 ŚREDNI KLIENCI"
    
    print(f"Typ: {cluster_name}")

# Przygotuj wizualizację
plt.figure(figsize=(12, 8))

# Wykres 1: Dochód vs Wydatki
plt.subplot(2, 2, 1)
scatter = plt.scatter(data['roczny_dochod'], data['wydatki_roczne'], 
                     c=data['klaster'], cmap='viridis', alpha=0.6)
plt.xlabel('Roczny dochód')
plt.ylabel('Wydatki roczne')
plt.title('Dochód vs Wydatki')
plt.colorbar(scatter)

# Wykres 2: Wiek vs Liczba zakupów
plt.subplot(2, 2, 2)
scatter2 = plt.scatter(data['wiek'], data['liczba_zakupow'], 
                      c=data['klaster'], cmap='viridis', alpha=0.6)
plt.xlabel('Wiek')
plt.ylabel('Liczba zakupów')
plt.title('Wiek vs Liczba zakupów')
plt.colorbar(scatter2)

# Wykres 3: Średni koszt vs Liczba zakupów
plt.subplot(2, 2, 3)
scatter3 = plt.scatter(data['sredni_koszt_zakupu'], data['liczba_zakupow'], 
                      c=data['klaster'], cmap='viridis', alpha=0.6)
plt.xlabel('Średni koszt zakupu')
plt.ylabel('Liczba zakupów')
plt.title('Koszt vs Częstotliwość')
plt.colorbar(scatter3)

# Wykres 4: Rozkład klastrów
plt.subplot(2, 2, 4)
cluster_counts = data['klaster'].value_counts().sort_index()
plt.bar(range(len(cluster_counts)), cluster_counts.values, 
        color=plt.cm.viridis(np.linspace(0, 1, len(cluster_counts))))
plt.xlabel('Klaster')
plt.ylabel('Liczba klientów')
plt.title('Rozmiary klastrów')
plt.xticks(range(len(cluster_counts)))

plt.tight_layout()
plt.close()

# Porównanie z rzeczywistymi typami klientów
if 'typ_klienta' in data.columns:
    print("\n" + "="*50)
    print("PORÓWNANIE Z RZECZYWISTYMI TYPAMI:")
    comparison = pd.crosstab(data['klaster'], data['typ_klienta'])
    print(comparison)

# Odtwórz wykresy
plt.figure(figsize=(12, 8))

plt.subplot(2, 2, 1)
scatter = plt.scatter(data['roczny_dochod'], data['wydatki_roczne'], 
                     c=data['klaster'], cmap='viridis', alpha=0.6)
plt.xlabel('Roczny dochód')
plt.ylabel('Wydatki roczne')
plt.title('Dochód vs Wydatki')
plt.colorbar(scatter)

plt.subplot(2, 2, 2)
scatter2 = plt.scatter(data['wiek'], data['liczba_zakupow'], 
                      c=data['klaster'], cmap='viridis', alpha=0.6)
plt.xlabel('Wiek')
plt.ylabel('Liczba zakupów')
plt.title('Wiek vs Liczba zakupów')
plt.colorbar(scatter2)

plt.subplot(2, 2, 3)
scatter3 = plt.scatter(data['sredni_koszt_zakupu'], data['liczba_zakupow'], 
                      c=data['klaster'], cmap='viridis', alpha=0.6)
plt.xlabel('Średni koszt zakupu')
plt.ylabel('Liczba zakupów')
plt.title('Koszt vs Częstotliwość')
plt.colorbar(scatter3)

plt.subplot(2, 2, 4)
cluster_counts = data['klaster'].value_counts().sort_index()
plt.bar(range(len(cluster_counts)), cluster_counts.values, 
        color=plt.cm.viridis(np.linspace(0, 1, len(cluster_counts))))
plt.xlabel('Klaster')
plt.ylabel('Liczba klientów')
plt.title('Rozmiary klastrów')
plt.xticks(range(len(cluster_counts)))

plt.tight_layout()
plt.show()
CHARAKTERYSTYKI KLASTRÓW:
==================================================

KLASTER 0:
Liczba klientów: 275 (27.5%)
Średni wiek: 43 lat
Średni dochód: 71194 zł
Średnie wydatki: 41779 zł
Średnia liczba zakupów: 34
Średni koszt zakupu: 332 zł
Typ: 🌟 PREMIUM CUSTOMERS

KLASTER 1:
Liczba klientów: 725 (72.5%)
Średni wiek: 44 lat
Średni dochód: 37808 zł
Średnie wydatki: 11486 zł
Średnia liczba zakupów: 20
Średni koszt zakupu: 155 zł
Typ: 🔄 ŚREDNI KLIENCI

==================================================
PORÓWNANIE Z RZECZYWISTYMI TYPAMI:
typ_klienta  average  bargain  premium  sporadic
klaster                                         
0                109        0      166         0
1                228      344        0       153

Analiza klastrów klientów

🎯 Praktyczne zastosowania K-Means

1) Marketing - personalizacja kampanii

# Strategia marketingowa na podstawie klastrów
def marketing_strategy(cluster_id, cluster_data):
    avg_income = cluster_data['roczny_dochod'].mean()
    avg_spending = cluster_data['wydatki_roczne'].mean()
    avg_frequency = cluster_data['liczba_zakupow'].mean()
    
    if avg_income > 60000 and avg_spending > 30000:
        return {
            'segment': 'Premium Customers',
            'strategy': 'Produkty luksusowe, VIP program, personal shopping',
            'budget_allocation': '40%',
            'channels': 'Email premium, personal calls, exclusive events'
        }
    elif avg_frequency < 10 and avg_spending < 10000:
        return {
            'segment': 'Sporadic Customers', 
            'strategy': 'Reaktywacja, oferty specjalne, przypomnienia',
            'budget_allocation': '15%',
            'channels': 'SMS, push notifications, retargeting ads'
        }
    elif avg_spending / avg_income < 0.3 and avg_frequency > 20:
        return {
            'segment': 'Bargain Hunters',
            'strategy': 'Promocje, wyprzedaże, programy lojalnościowe',
            'budget_allocation': '25%', 
            'channels': 'Newsletter z promocjami, social media deals'
        }
    else:
        return {
            'segment': 'Average Customers',
            'strategy': 'Standardowe produkty, cross-selling, up-selling',
            'budget_allocation': '20%',
            'channels': 'Email marketing, social media, display ads'
        }

print("STRATEGIA MARKETINGOWA DLA KAŻDEGO KLASTRA:")
print("=" * 60)

for cluster_id in range(best_k):
    cluster_data = data[data['klaster'] == cluster_id]
    strategy = marketing_strategy(cluster_id, cluster_data)
    
    print(f"\nKLASTER {cluster_id}: {strategy['segment']}")
    print(f"Strategia: {strategy['strategy']}")
    print(f"Budżet: {strategy['budget_allocation']}")
    print(f"Kanały: {strategy['channels']}")
STRATEGIA MARKETINGOWA DLA KAŻDEGO KLASTRA:
============================================================

KLASTER 0: Premium Customers
Strategia: Produkty luksusowe, VIP program, personal shopping
Budżet: 40%
Kanały: Email premium, personal calls, exclusive events

KLASTER 1: Average Customers
Strategia: Standardowe produkty, cross-selling, up-selling
Budżet: 20%
Kanały: Email marketing, social media, display ads

2) Inne branże

# Healthcare - grupowanie pacjentów
healthcare_features = ['wiek', 'BMI', 'ciśnienie', 'cholesterol', 'aktywność_fizyczna']
# Wynik: programy profilaktyczne dostosowane do grup ryzyka

# Finanse - portfolio management  
finance_features = ['dochód', 'tolerancja_ryzyka', 'horyzont_inwestycji', 'doświadczenie']
# Wynik: personalizowane porady inwestycyjne

# Retail - optymalizacja sklepów
retail_features = ['lokalizacja', 'demografia', 'konkurencja', 'ruch_pieszy']  
# Wynik: optymalne rozmieszczenie produktów w różnych lokalizacjach

⚙️ Tuning parametrów K-Means

from sklearn.cluster import KMeans

# 1) Różne metody inicjalizacji
init_methods = ['k-means++', 'random']
results = {}

for init_method in init_methods:
    kmeans = KMeans(n_clusters=best_k, init=init_method, n_init=10, random_state=42)
    labels = kmeans.fit_predict(X_scaled)
    sil_score = silhouette_score(X_scaled, labels)
    results[init_method] = sil_score

print("Porównanie metod inicjalizacji:")
for method, score in results.items():
    print(f"{method}: Silhouette = {score:.3f}")

# 2) Wpływ liczby inicjalizacji
n_init_values = [1, 5, 10, 20]
for n_init in n_init_values:
    kmeans = KMeans(n_clusters=best_k, n_init=n_init, random_state=42)
    start_time = pd.Timestamp.now()
    kmeans.fit(X_scaled)
    end_time = pd.Timestamp.now()
    duration = (end_time - start_time).total_seconds()
    sil_score = silhouette_score(X_scaled, kmeans.labels_)
    print(f"n_init={n_init}: Silhouette={sil_score:.3f}, Czas={duration:.2f}s")

# 3) Różne algorytmy
algorithms = ['lloyd', 'elkan']  # 'auto' wybiera automatycznie
for algorithm in algorithms:
    try:
        kmeans = KMeans(n_clusters=best_k, algorithm=algorithm, random_state=42)
        start_time = pd.Timestamp.now()
        kmeans.fit(X_scaled)
        end_time = pd.Timestamp.now()
        duration = (end_time - start_time).total_seconds()
        print(f"Algorytm {algorithm}: Czas={duration:.2f}s")
    except:
        print(f"Algorytm {algorithm}: Niedostępny w tej wersji")
Porównanie metod inicjalizacji:
k-means++: Silhouette = 0.281
random: Silhouette = 0.281
n_init=1: Silhouette=0.280, Czas=0.00s
n_init=5: Silhouette=0.281, Czas=0.01s
n_init=10: Silhouette=0.281, Czas=0.02s
n_init=20: Silhouette=0.281, Czas=0.03s

⚠️ Pułapki i rozwiązania

1) Brak standaryzacji danych

# Problem: różne skale zmiennych
print("PROBLEM: Bez standaryzacji")
print("Dochód (tysiące): 20-80")  
print("Wiek (lata): 18-70")
print("→ K-Means będzie skupiać się głównie na dochodzie!")

# Demonstracja
kmeans_unscaled = KMeans(n_clusters=3, random_state=42)
labels_unscaled = kmeans_unscaled.fit_predict(X[['roczny_dochod', 'wiek']])

kmeans_scaled = KMeans(n_clusters=3, random_state=42)  
X_subset_scaled = scaler.fit_transform(X[['roczny_dochod', 'wiek']])
labels_scaled = kmeans_scaled.fit_predict(X_subset_scaled)

sil_unscaled = silhouette_score(X[['roczny_dochod', 'wiek']], labels_unscaled)
sil_scaled = silhouette_score(X_subset_scaled, labels_scaled)

print(f"\nWyniki:")
print(f"Bez standaryzacji: Silhouette = {sil_unscaled:.3f}")
print(f"Ze standaryzacją: Silhouette = {sil_scaled:.3f}")
print("✅ Standaryzacja ZAWSZE poprawia wyniki!")

Wyniki:
Bez standaryzacji: Silhouette = 0.562
Ze standaryzacją: Silhouette = 0.406
✅ Standaryzacja ZAWSZE poprawia wyniki!

2) Outliers - wartości odstające

# Problem: outliers zakłócają centroidy klastrów
from sklearn.preprocessing import RobustScaler

# Dodaj kilka outlierów
data_with_outliers = data.copy()
data_with_outliers.loc[0, 'roczny_dochod'] = 500000  # milioner!
data_with_outliers.loc[1, 'wydatki_roczne'] = 200000  # mega wydatki

X_outliers = data_with_outliers[features]

# Porównaj różne skalery
scalers = {
    'StandardScaler': StandardScaler(),
    'RobustScaler': RobustScaler()  # odporny na outliers
}

print("Wpływ outliers:")
for scaler_name, scaler_obj in scalers.items():
    X_scaled_comp = scaler_obj.fit_transform(X_outliers)
    kmeans = KMeans(n_clusters=3, random_state=42)
    labels = kmeans.fit_predict(X_scaled_comp)
    sil_score = silhouette_score(X_scaled_comp, labels)
    print(f"{scaler_name}: Silhouette = {sil_score:.3f}")

print("\n✅ RobustScaler lepiej radzi sobie z outliers!")
Wpływ outliers:
StandardScaler: Silhouette = 0.193
RobustScaler: Silhouette = 0.246

✅ RobustScaler lepiej radzi sobie z outliers!

3) Curse of dimensionality

# Problem: za dużo wymiarów
from sklearn.decomposition import PCA

print("Problem wielu wymiarów:")
dimensions = [2, 5, 10, 20, 50]

for n_dims in dimensions:
    if n_dims <= X_scaled.shape[1]:
        X_subset = X_scaled[:, :n_dims]
    else:
        # Dodaj sztuczne wymiary
        extra_dims = np.random.randn(X_scaled.shape[0], n_dims - X_scaled.shape[1])
        X_subset = np.hstack([X_scaled, extra_dims])
    
    kmeans = KMeans(n_clusters=3, random_state=42)
    labels = kmeans.fit_predict(X_subset)
    
    if len(np.unique(labels)) > 1:  # sprawdź czy są różne klastry
        sil_score = silhouette_score(X_subset, labels)
        print(f"{n_dims} wymiarów: Silhouette = {sil_score:.3f}")
    else:
        print(f"{n_dims} wymiarów: Wszystkie punkty w jednym klastrze!")

print("\nRozwiązanie: PCA dimensionality reduction")
pca = PCA(n_components=0.95)  # zachowaj 95% wariancji
X_pca = pca.fit_transform(X_scaled)
print(f"Redukcja z {X_scaled.shape[1]} do {X_pca.shape[1]} wymiarów")

kmeans_pca = KMeans(n_clusters=3, random_state=42)
labels_pca = kmeans_pca.fit_predict(X_pca)
sil_pca = silhouette_score(X_pca, labels_pca)
print(f"Po PCA: Silhouette = {sil_pca:.3f}")
Problem wielu wymiarów:
2 wymiarów: Silhouette = 0.406
5 wymiarów: Silhouette = 0.285
10 wymiarów: Silhouette = 0.133
20 wymiarów: Silhouette = 0.055
50 wymiarów: Silhouette = 0.024

Rozwiązanie: PCA dimensionality reduction
Redukcja z 6 do 6 wymiarów
Po PCA: Silhouette = 0.233

🌍 Real-world przypadki użycia

  1. E-commerce: Segmentacja klientów, personalizacja, dynamic pricing
  2. Marketing: Customer personas, campaign optimization, market research
  3. Finance: Portfolio optimization, risk assessment, fraud detection
  4. Healthcare: Patient stratification, treatment personalization, drug discovery
  5. Operations: Supply chain optimization, demand forecasting, quality control
💡 Kiedy używać K-Means?

✅ UŻYJ GDY:

  • Chcesz odkryć ukryte grupy w danych bez etykiet
  • Dane mają podobne gęstości i są “okrągłe” (sferyczne klastry)
  • Potrzebujesz szybkiego i skalowalnego algorytmu
  • Wiesz w przybliżeniu ile może być grup (K)
  • Chcesz segmentować klientów, produkty, rynki

❌ NIE UŻYWAJ GDY:

  • Klastry mają różne rozmiary lub gęstości (użyj DBSCAN)
  • Klastry mają nieregularne kształty (użyj Hierarchical Clustering)
  • Nie wiesz wcale ile może być grup (użyj DBSCAN/HDBSCAN)
  • Masz kategoryczne zmienne (użyj K-Modes)
  • Dane mają dużo outliers (użyj DBSCAN)

Następna ściągawka: Support Vector Machines - znajdowanie optymalnych granic! 🎯