11  Unsupervised Learning: Menemukan Struktur Tersembunyi

Tujuan pembelajaran. Setelah menyelesaikan bab ini, kita akan mampu: 1. Membedakan supervised dari unsupervised learning, dan memahami kapan masing-masing tepat digunakan. 2. Menerapkan K-means clustering dan memilih jumlah cluster secara sistematis. 3. Menerapkan hierarchical clustering dan membaca dendrogram. 4. Menggunakan PCA untuk mereduksi dimensi dan memvisualisasikan data berdimensi tinggi. 5. Menemukan association rules dalam data transaksi (market basket analysis). 6. Menginterpretasikan segmen pelanggan secara bisnis dan menghubungkannya ke keputusan retensi. 7. Memahami mengapa segmentasi adalah input penting untuk model optimasi di Bagian III.

11.1 Mengapa Unsupervised Learning?

Sampai Bab 10, semua model kita adalah supervised—kita memiliki label (churn/tidak, unit terjual) dan model belajar memprediksi label tersebut. Tetapi tidak semua pertanyaan analytics memiliki label:

  • “Apakah ada kelompok pelanggan alami yang tidak kita ketahui?”
  • “Fitur mana yang paling penting dalam menjelaskan variasi data?”
  • “Produk apa yang sering dibeli bersama?”

Pertanyaan-pertanyaan ini tidak memiliki “jawaban benar” yang dapat dilatih. Kita tidak tahu berapa banyak segmen yang ada, atau segmen mana yang “benar”—kita ingin menemukan struktur dari data itu sendiri.

Inilah domain unsupervised learning: menemukan pola dalam data tanpa label.

Important

Tiga kegunaan utama unsupervised learning dalam decision analytics:

  1. Segmentasi—mengelompokkan pelanggan/produk ke dalam segmen yang bermakna secara bisnis. Ini adalah input langsung untuk personalisasi dan alokasi anggaran di Bagian III.

  2. Reduksi dimensi—menyederhanakan data berdimensi tinggi agar dapat divisualisasikan dan dimodelkan. PCA adalah alat utama untuk ini.

  3. Penemuan pola—menemukan asosiasi antar item (association rules) yang dapat digunakan untuk cross-selling, bundling, dan rekomendasi.

11.2 Menyiapkan Data

Kita kembali ke dataset Telco, tetapi kali ini tanpa label churn—kita ingin menemukan segmen pelanggan berdasarkan profil mereka.

Code
import sys
sys.path.insert(0, ".")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")

from decanal.utils import load_churn_clean, load_retail
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.cluster import KMeans, AgglomerativeClustering
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score, silhouette_samples

RANDOM_STATE = 42
df = load_churn_clean()

print(f"Dataset: {df.shape[0]:,} pelanggan × {df.shape[1]} kolom")
Dataset: 7,043 pelanggan × 23 kolom

Untuk clustering, kita fokus pada fitur numerik yang bermakna secara bisnis—tenure, tagihan bulanan, total tagihan, dan jumlah layanan. Fitur kategorikal akan kita tangani terpisah atau melalui encoding.

Code
# Fitur numerik untuk clustering
cluster_features = ["tenure", "MonthlyCharges", "TotalCharges", "num_services"]

# Periksa apakah kolom tersedia
available = [c for c in cluster_features if c in df.columns]
print(f"Fitur yang tersedia: {available}")

X_cluster = df[available].copy()
X_cluster.describe().round(2)
Fitur yang tersedia: ['tenure', 'MonthlyCharges', 'TotalCharges', 'num_services']
tenure MonthlyCharges TotalCharges num_services
count 7043.00 7043.00 7043.00 7043.00
mean 32.37 64.76 2279.73 3.36
std 24.56 30.09 2266.79 2.06
min 0.00 18.25 0.00 0.00
25% 9.00 35.50 398.55 1.00
50% 29.00 70.35 1394.55 3.00
75% 55.00 89.85 3786.60 5.00
max 72.00 118.75 8684.80 8.00
Code
# Standardisasi wajib untuk clustering berbasis jarak (K-means, hierarchical)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_cluster)

# Simpan sebagai DataFrame untuk kenyamanan
X_scaled_df = pd.DataFrame(X_scaled, columns=available)
X_scaled_df.head()
tenure MonthlyCharges TotalCharges num_services
0 -1.277445 -1.160323 -0.992611 -1.145997
1 0.066327 -0.259629 -0.172165 -0.176011
2 -1.236724 -0.362660 -0.958066 -0.176011
3 0.514251 -0.746535 -0.193672 -0.176011
4 -1.236724 0.197365 -0.938874 -1.145997
Warning

Standardisasi adalah wajib untuk clustering. Tanpa standardisasi, fitur dengan skala besar (TotalCharges dalam ribuan) akan mendominasi perhitungan jarak, membuat fitur dengan skala kecil (num_services, 0–8) hampir tidak berpengaruh. Ini adalah kesalahan paling umum dalam clustering—dan konsekuensinya adalah segmen yang tidak bermakna.

11.3 K-Means Clustering

K-means adalah algoritma clustering paling populer. Ia bekerja dengan: 1. Memilih K titik pusat (centroid) secara acak. 2. Menetapkan setiap titik ke centroid terdekat. 3. Memperbarui centroid sebagai rata-rata titik dalam cluster. 4. Mengulangi langkah 2–3 sampai konvergen.

11.3.1 Memilih Jumlah Cluster (K)

Pertanyaan tersulit dalam K-means adalah: berapa K yang tepat? Ada dua metode yang paling umum:

Elbow Method: plot inertia (jumlah kuadrat jarak ke centroid) vs K. Titik di mana penurunan mulai “melandai” adalah K yang disarankan.

Code
inertias = []
K_range = range(1, 11)

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

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(list(K_range), inertias, marker="o", color="#4C72B0", linewidth=2)
ax.set_xlabel("Jumlah cluster (K)")
ax.set_ylabel("Inertia")
ax.set_title("Elbow Method — Memilih K Optimal")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Silhouette Score: mengukur seberapa baik setiap titik cocok dengan clusternya sendiri dibanding cluster lain. Nilai berkisar -1 hingga 1; semakin tinggi semakin baik.

Code
silhouettes = []
K_range_sil = range(2, 11)

for k in K_range_sil:
    kmeans = KMeans(n_clusters=k, random_state=RANDOM_STATE, n_init=10)
    labels = kmeans.fit_predict(X_scaled)
    silhouettes.append(silhouette_score(X_scaled, labels))

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(list(K_range_sil), silhouettes, marker="s", color="#DD8452", linewidth=2)
best_k = list(K_range_sil)[np.argmax(silhouettes)]
ax.axvline(best_k, color="red", linestyle="--", alpha=0.7,
           label=f"Silhouette tertinggi (K={best_k})")
ax.set_xlabel("Jumlah cluster (K)")
ax.set_ylabel("Silhouette Score")
ax.set_title("Silhouette Score per K")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print(f"Silhouette tertinggi: K = {best_k} ({max(silhouettes):.3f})")

Silhouette tertinggi: K = 2 (0.467)
Tip

Elbow vs Silhouette. Keduanya adalah panduan, bukan aturan mutlak. Dalam praktik: - Elbow lebih mudah diinterpretasikan tetapi subjektif (di mana “siku”-nya?). - Silhouette memberi angka objektif, tetapi bisa memilih K yang secara bisnis tidak bermakna.

Yang terpenting: pilih K yang dapat diinterpretasikan secara bisnis. Jika K=3 memberi segmen “baru, menengah, lama” yang jelas, itu lebih berguna daripada K=7 dengan segmen yang sulit dijelaskan—bahkan jika K=7 memiliki silhouette sedikit lebih tinggi.

11.3.2 Melatih K-Means dengan K Optimal

Berdasarkan kedua metode, kita pilih K=3 (atau sesuaikan dengan hasil render kita):

Code
K_OPT = 3  # sesuaikan setelah melihat elbow & silhouette

kmeans = KMeans(n_clusters=K_OPT, random_state=RANDOM_STATE, n_init=10)
df["segment"] = kmeans.fit_predict(X_scaled)

# Profil setiap segmen
profil = df.groupby("segment")[available + ["Churn"]].agg(
    {**{c: "mean" for c in available}, "Churn": lambda s: (s == "Yes").mean() * 100}
).round(2)
profil["jumlah"] = df.groupby("segment").size()
profil = profil[["jumlah"] + available + ["Churn"]]
profil.columns = ["jumlah", "tenure", "MonthlyCharges", "TotalCharges", "num_services", "churn_rate (%)"]
profil.sort_values("churn_rate (%)", ascending=False)
jumlah tenure MonthlyCharges TotalCharges num_services churn_rate (%)
segment
2 2622 15.00 75.83 1137.24 3.24 46.15
1 2163 58.37 90.52 5270.62 5.63 15.03
0 2258 27.64 27.24 741.35 1.33 14.79

Cara membaca profil segmen:

  • Segmen 1 mungkin adalah pelanggan baru dengan tagihan tinggi dan churn rate tinggi—prioritas retensi.
  • Segmen 2 mungkin adalah pelanggan setia dengan tenure panjang, tagihan moderat, dan churn rate rendah—target upsell.
  • Segmen 3 mungkin adalah pelanggan berisiko menengah—target program engagement.

11.3.3 Visualisasi Segmen

Code
fig, ax = plt.subplots(figsize=(9, 5))
palette = ["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3"]

for seg in sorted(df["segment"].unique()):
    subset = df[df["segment"] == seg]
    ax.scatter(subset["tenure"], subset["MonthlyCharges"],
               c=palette[seg], label=f"Segmen {seg}",
               alpha=0.5, s=subset["num_services"] * 5)

# Plot centroid
centroids = scaler.inverse_transform(kmeans.cluster_centers_)
for i, c in enumerate(centroids):
    ax.scatter(c[0], c[1], c="black", marker="X", s=200,
               edgecolors="white", linewidths=2)
    ax.annotate(f"C{i}", (c[0], c[1]), fontsize=12, fontweight="bold",
                ha="center", va="center", color="white")

ax.set_xlabel("Tenure (bulan)")
ax.set_ylabel("Tagihan bulanan")
ax.set_title("Segmentasi Pelanggan (K-Means, K=3)")
ax.legend(title="Segmen")
plt.tight_layout()
plt.show()
Figure 11.1: Segmentasi pelanggan berdasarkan tenure dan MonthlyCharges. Warna menunjukkan segmen; ukuran titik proporsional terhadap jumlah layanan.
Important

Interpretasi bisnis adalah kunci. Cluster yang dihasilkan K-means tidak memiliki makna inheren—“Segmen 0” bukanlah label yang berguna. Tugas kita adalah memberi nama segmen berdasarkan profilnya:

Segmen Profil Nama bisnis Strategi
0 Tenure rendah, tagihan tinggi, banyak layanan “Premium Baru” Onboarding intensif, diskon perpanjangan
1 Tenure tinggi, tagihan rendah, sedikit layanan “Loyal Hemat” Upsell layanan tambahan
2 Tenure menengah, tagihan menengah “Mainstream” Program engagement rutin

Nama-nama ini akan digunakan di seluruh Bagian III untuk merujuk segmen dalam model optimasi.

11.4 Hierarchical Clustering

Hierarchical clustering membangun pohon (dendrogram) yang menunjukkan bagaimana cluster bergabung pada berbagai tingkat jarak. Berbeda dari K-means, ia tidak memerlukan K di awal—K dapat ditentukan setelah melihat dendrogram.

11.4.1 Membangun Dendrogram

Code
from scipy.cluster.hierarchy import dendrogram, linkage

# Ambil sampel 500 pelanggan agar dendrogram terbaca
sample_idx = np.random.RandomState(RANDOM_STATE).choice(len(X_scaled), 500, replace=False)
X_sample = X_scaled[sample_idx]

Z = linkage(X_sample, method="ward")

fig, ax = plt.subplots(figsize=(10, 5))
dendrogram(Z, ax=ax, truncate_mode="lastp", p=20,
           leaf_rotation=90, leaf_font_size=8, show_contracted=True)
ax.set_xlabel("Indeks sampel atau ukuran cluster")
ax.set_ylabel("Jarak penggabungan (Ward)")
ax.set_title("Dendrogram — Hierarchical Clustering (sampel 500 pelanggan)")
plt.tight_layout()
plt.show()
Figure 11.2: Dendrogram hierarchical clustering. Garis horizontal menunjukkan jarak penggabungan; semakin tinggi, semakin berbeda cluster yang digabung.

Cara membaca dendrogram:

  • Setiap daun (di bawah) adalah satu pelanggan atau cluster kecil.
  • Garis horizontal menunjukkan pada jarak berapa dua cluster digabung.
  • Semakin tinggi garis horizontal, semakin berbeda cluster yang digabung.
  • Untuk memilih K: gambar garis horizontal imajiner melintasi dendrogram. Jumlah garis vertikal yang terpotong = jumlah cluster.

11.4.2 Membandingkan dengan K-Means

Code
from sklearn.metrics import adjusted_rand_score

# Hierarchical dengan K yang sama
hc = AgglomerativeClustering(n_clusters=K_OPT, linkage="ward")
df["segment_hc"] = hc.fit_predict(X_scaled)

# Bandingkan dengan K-means
ari = adjusted_rand_score(df["segment"], df["segment_hc"])
print(f"Adjusted Rand Index (K-means vs Hierarchical): {ari:.3f}")
print(f"  (1.0 = identik, 0.0 = acak)")

# Profil segmen hierarchical
profil_hc = df.groupby("segment_hc")[available + ["Churn"]].agg(
    {**{c: "mean" for c in available}, "Churn": lambda s: (s == "Yes").mean() * 100}
).round(2)
profil_hc["jumlah"] = df.groupby("segment_hc").size()
profil_hc
Adjusted Rand Index (K-means vs Hierarchical): 0.436
  (1.0 = identik, 0.0 = acak)
tenure MonthlyCharges TotalCharges num_services Churn jumlah
segment_hc
0 20.22 38.37 586.19 1.50 26.15 3097
1 29.87 79.42 2235.12 3.93 34.94 2430
2 61.21 95.19 5810.94 6.26 13.85 1516
Tip

Kapan memakai hierarchical vs K-means?

Aspek K-Means Hierarchical
K Harus ditentukan di awal Dapat ditentukan setelah melihat dendrogram
Skalabilitas Cepat untuk data besar (O(n·k·d)) Lambat untuk data besar (O(n²))
Bentuk cluster Cenderung bulat Dapat menangkap bentuk kompleks
Interpretabilitas Centroid mudah diinterpretasikan Dendrogram memberi gambaran hierarkis
Penggunaan Segmentasi operasional Eksplorasi struktur data

Untuk dataset besar (>10.000 baris), K-means biasanya lebih praktis. Untuk eksplorasi awal atau dataset kecil, hierarchical memberi gambaran yang lebih kaya.

11.5 PCA: Mereduksi Dimensi

Principal Component Analysis (PCA) adalah teknik reduksi dimensi—ia mengubah banyak fitur yang berkorelasi menjadi komponen utama yang saling independen, diurutkan berdasarkan varians yang dijelaskan.

11.5.1 Mengapa PCA?

  • Visualisasi: data dengan 20 fitur tidak dapat divisualisasikan; data dengan 2 komponen utama bisa.
  • Reduksi noise: komponen dengan varians rendah sering kali hanya noise.
  • Multikolinearitas: PCA menghasilkan fitur yang saling independen—berguna untuk regresi.

11.5.2 Menerapkan PCA

Code
pca = PCA(random_state=RANDOM_STATE)
X_pca = pca.fit_transform(X_scaled)

# Varians yang dijelaskan
explained = pca.explained_variance_ratio_ * 100
cumulative = np.cumsum(explained)

pca_df = pd.DataFrame({
    "Komponen": [f"PC{i+1}" for i in range(len(explained))],
    "Varians (%)": explained.round(2),
    "Kumulatif (%)": cumulative.round(2),
})
pca_df
Komponen Varians (%) Kumulatif (%)
0 PC1 73.79 73.79
1 PC2 20.93 94.71
2 PC3 3.80 98.52
3 PC4 1.48 100.00
Code
fig, axes = plt.subplots(1, 2, figsize=(11, 3.5))

# Scree plot
axes[0].bar(range(1, len(explained)+1), explained, color="#4C72B0")
axes[0].set_xlabel("Komponen utama")
axes[0].set_ylabel("Varians dijelaskan (%)")
axes[0].set_title("Scree Plot")

# Kumulatif
axes[1].plot(range(1, len(cumulative)+1), cumulative, marker="o", color="#DD8452")
axes[1].axhline(80, color="gray", linestyle="--", alpha=0.5, label="80%")
axes[1].axhline(90, color="red", linestyle=":", alpha=0.5, label="90%")
axes[1].set_xlabel("Jumlah komponen")
axes[1].set_ylabel("Varians kumulatif (%)")
axes[1].set_title("Varians Kumulatif")
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Figure 11.3: Scree plot: varians yang dijelaskan per komponen. Titik ‘siku’ menunjukkan jumlah komponen yang perlu dipertahankan.

Cara membaca:

  • Scree plot menunjukkan kontribusi setiap komponen. “Siku” (perubahan tajam) menunjukkan jumlah komponen yang wajar.
  • Varians kumulatif menunjukkan berapa banyak informasi yang dipertahankan. Aturan umum: pertahankan komponen yang menjelaskan 80–90% varians.
  • Untuk dataset kita (4 fitur numerik), 2 komponen biasanya menjelaskan >85% varians—cukup untuk visualisasi.

11.5.3 Visualisasi 2D dengan PCA

Code
fig, ax = plt.subplots(figsize=(9, 5))

for seg in sorted(df["segment"].unique()):
    mask = df["segment"] == seg
    ax.scatter(X_pca[mask, 0], X_pca[mask, 1],
               c=palette[seg], label=f"Segmen {seg}",
               alpha=0.5, s=10)

ax.set_xlabel(f"PC1 ({explained[0]:.1f}% varians)")
ax.set_ylabel(f"PC2 ({explained[1]:.1f}% varians)")
ax.set_title("Visualisasi PCA 2D — Segmen Pelanggan")
ax.legend(title="Segmen")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Figure 11.4: Visualisasi data dalam 2 komponen utama, diwarnai berdasarkan segmen K-means. PCA memungkinkan kita melihat struktur cluster dalam 2D.
Important

PCA dan interpretasi. Komponen utama tidak memiliki makna langsung—mereka adalah kombinasi linear dari fitur asli. Namun, kita dapat melihat loading (bobot fitur dalam setiap komponen) untuk memahami apa yang diwakili komponen tersebut:

loadings = pd.DataFrame(
    pca.components_[:2].T,
    index=available,
    columns=["PC1", "PC2"]
)

Jika PC1 memiliki loading tinggi pada tenure dan TotalCharges, kita bisa menyebutnya “komponen loyalitas”. Jika PC2 memiliki loading tinggi pada MonthlyCharges, itu mungkin “komponen biaya”.

Code
loadings = pd.DataFrame(
    pca.components_[:2].T,
    index=available,
    columns=["PC1", "PC2"]
).round(3)
loadings
PC1 PC2
tenure 0.438 0.704
MonthlyCharges 0.461 -0.621
TotalCharges 0.557 0.217
num_services 0.534 -0.268

11.6 Association Rules: Market Basket Analysis

Sekarang kita beralih ke dataset telco—dan ke jenis analisis yang sama sekali berbeda: menemukan asosiasi antar item dalam transaksi.

Association rules menjawab pertanyaan: “Jika pelanggan membeli produk A, seberapa besar kemungkinan mereka juga membeli produk B?”

11.6.1 Konsep Dasar

  • Itemset: kumpulan item (mis. {Roti, Susu}).
  • Support: proporsi transaksi yang mengandung itemset.
  • Confidence: probabilitas membeli B jika membeli A.
  • Lift: seberapa besar asosiasi A→B dibandingkan jika keduanya independen.

\[\text{Support}(A \rightarrow B) = P(A \cap B)\] \[\text{Confidence}(A \rightarrow B) = P(B|A)\] \[\text{Lift}(A \rightarrow B) = \frac{P(B|A)}{P(B)}\]

  • Lift > 1: A dan B lebih sering muncul bersama daripada yang diharapkan secara kebetulan.
  • Lift = 1: A dan B independen.
  • Lift < 1: A dan B cenderung tidak muncul bersama.

11.6.2 Menyiapkan Data Transaksi

Dataset retail tidak memiliki struktur transaksi (satu baris per pembelian). Tetapi kita dapat mensimulasikan transaksi dengan mengelompokkan produk yang terjual pada tanggal dan toko yang sama:

Code
df_retail = load_retail()
df_retail["Date"] = pd.to_datetime(df_retail["Date"])

# Anggap setiap (Date, Store ID) adalah satu "transaksi"
# Item = Product ID yang terjual (Units Sold > 0)
transactions = (df_retail[df_retail["Units Sold"] > 0]
                .groupby(["Date", "Store ID"])["Product ID"]
                .apply(lambda x: sorted(set(x)))
                .reset_index())
transactions.columns = ["Date", "Store ID", "items"]

print(f"Jumlah transaksi: {len(transactions):,}")
print(f"Rata-rata item per transaksi: {transactions['items'].apply(len).mean():.1f}")
transactions.head()
Jumlah transaksi: 3,655
Rata-rata item per transaksi: 19.9
Date Store ID items
0 2022-01-01 S001 [P0001, P0002, P0003, P0004, P0005, P0006, P00...
1 2022-01-01 S002 [P0001, P0002, P0003, P0004, P0005, P0006, P00...
2 2022-01-01 S003 [P0001, P0002, P0003, P0004, P0005, P0006, P00...
3 2022-01-01 S004 [P0001, P0002, P0003, P0005, P0006, P0007, P00...
4 2022-01-01 S005 [P0001, P0002, P0003, P0004, P0005, P0006, P00...

11.6.3 Menerapkan Apriori

Library mlxtend menyediakan implementasi Apriori yang efisien:

Code
# from mlxtend.preprocessing import TransactionEncoder
# from mlxtend.frequent_patterns import fpgrowth, association_rules

# # Encode transaksi ke format one-hot
# te = TransactionEncoder()
# te_array = te.fit(transactions["items"]).transform(transactions["items"])
# basket_df = pd.DataFrame(te_array, columns=te.columns_)

# # Cari frequent itemsets dengan support minimal 10%
# frequent = fpgrowth(basket_df, min_support=0.1, max_len=3, use_colnames=True)
# print(f"Jumlah frequent itemsets: {len(frequent)}")

# # Bangkitkan association rules
# rules = association_rules(frequent, metric="lift", min_threshold=1.2)
# rules = rules.sort_values("lift", ascending=False)

# print(f"Jumlah rules (lift > 1.2): {len(rules)}")
# rules[["antecedents", "consequents", "support", "confidence", "lift"]].head(10).round(3)

from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import fpgrowth, association_rules
from decanal.utils import load_churn_clean

# --- Bangun keranjang dari layanan Telco ---
df_telco = load_churn_clean()

service_cols = [
    "PhoneService", "MultipleLines", "OnlineSecurity", "OnlineBackup",
    "DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies",
]

def get_services(row):
    """Kembalikan daftar layanan yang dilanggani pelanggan."""
    services = [col for col in service_cols if row[col] == "Yes"]
    if row["InternetService"] != "No":
        services.append(f"Internet_{row['InternetService']}")
    return services

df_telco["services"] = df_telco.apply(get_services, axis=1)

# Hanya pelanggan dengan >= 2 layanan (keranjang bermakna)
baskets = df_telco[df_telco["services"].apply(len) >= 2]["services"].tolist()

print(f"Jumlah keranjang (pelanggan): {len(baskets):,}")
print(f"Rata-rata layanan per pelanggan: {sum(len(b) for b in baskets)/len(baskets):.1f}")
print(f"Rentang: {min(len(b) for b in baskets)}–{max(len(b) for b in baskets)} layanan")

# --- Encoding ---
te = TransactionEncoder()
te_array = te.fit(baskets).transform(baskets)
basket_df = pd.DataFrame(te_array, columns=te.columns_)

# --- FP-Growth ---
frequent = fpgrowth(basket_df, min_support=0.05, use_colnames=True, max_len=3)
print(f"\nJumlah frequent itemsets: {len(frequent)}")

# --- Association rules ---
rules = association_rules(frequent, metric="lift", min_threshold=1.2)
rules = rules.sort_values("lift", ascending=False)

print(f"Jumlah rules (lift > 1.2): {len(rules)}")
rules[["antecedents", "consequents", "support", "confidence", "lift"]].head(10).round(3)
Jumlah keranjang (pelanggan): 5,779
Rata-rata layanan per pelanggan: 4.8
Rentang: 2–9 layanan

Jumlah frequent itemsets: 166
Jumlah rules (lift > 1.2): 440
antecedents consequents support confidence lift
187 (TechSupport) (OnlineSecurity, DeviceProtection) 0.123 0.349 1.814
182 (OnlineSecurity, DeviceProtection) (TechSupport) 0.123 0.642 1.814
204 (OnlineSecurity, StreamingMovies) (TechSupport) 0.116 0.626 1.769
209 (TechSupport) (OnlineSecurity, StreamingMovies) 0.116 0.329 1.769
178 (Internet_DSL, TechSupport) (OnlineSecurity) 0.125 0.615 1.762
179 (OnlineSecurity) (Internet_DSL, TechSupport) 0.125 0.359 1.762
305 (Internet_DSL, DeviceProtection) (TechSupport) 0.115 0.623 1.760
308 (TechSupport) (Internet_DSL, DeviceProtection) 0.115 0.324 1.760
318 (Internet_DSL, StreamingTV) (TechSupport) 0.103 0.621 1.755
321 (TechSupport) (Internet_DSL, StreamingTV) 0.103 0.291 1.755

Ilustrasi Cara membaca rules:

  • Antecedents → Consequents: “Jika pelanggan membeli [antecedents], mereka juga membeli [consequents]”.
  • Support: proporsi transaksi yang mengandung kedua itemset.
  • Confidence: probabilitas membeli consequent jika membeli antecedent.
  • Lift: seberapa besar asosiasi dibandingkan independen. Lift = 2 berarti pelanggan dua kali lebih mungkin membeli consequent jika mereka membeli antecedent.
Tip

Interpretasi bisnis dari association rules:

  • Cross-selling: Jika {Produk A} → {Produk B} dengan confidence tinggi, tawarkan B kepada pelanggan yang membeli A.
  • Bundling: Jika {A, B} → {C} dengan lift tinggi, buat paket A+B+C dengan harga khusus.
  • Layout toko: Tempatkan A dan B berdekatan untuk meningkatkan pembelian impulsif.

Peringatan: association rules menemukan korelasi, bukan kausalitas. {A} → {B} tidak berarti A menyebabkan pembelian B—mungkin ada faktor ketiga (mis. promosi) yang menyebabkan keduanya.

Latihan 11.5 — Association rules per segmen. Jalankan association rules secara terpisah untuk pelanggan DSL dan Fiber optic. Apakah rules yang muncul berbeda? Jika ya, mengapa? Apakah strategi bundling sebaiknya dibedakan per tipe internet?

11.6.4 📊 Interpretasi Hasil Komputasi Analitik

Temuan utama: 440 rules dengan lift > 1.2, dan semua rule teratas melibatkan tiga layanan “proteksi”: TechSupport, OnlineSecurity, DeviceProtection—dengan lift ~1,76–1,81.

Rule Lift Interpretasi bisnis
{TechSupport} → {DeviceProtection, OnlineSecurity} 1,81 Pelanggan dengan dukungan teknis 81% lebih mungkin memiliki proteksi perangkat + keamanan online
{DeviceProtection, OnlineSecurity} → {TechSupport} 1,81 Sebaliknya—konfirmasi asosiasi simetris
{StreamingMovies, OnlineSecurity} → {TechSupport} 1,77 Bahkan pelanggan hiburan yang punya keamanan cenderung punya dukungan teknis
{TechSupport, Internet_DSL} → {OnlineSecurity} 1,76 DSL + dukungan teknis → keamanan online

Insight bisnis yang muncul:

  1. Ada “Paket Proteksi” alami — tiga layanan (TechSupport, OnlineSecurity, DeviceProtection) cenderung dibeli bersama. Ini kandidat kuat untuk bundling resmi dengan harga khusus.

  2. Pelanggan DSL lebih cenderung bundling — Internet_DSL muncul di banyak rule. Ini mungkin karena pelanggan fiber optic sudah mendapat layanan proteksi bawaan, sementara pelanggan DSL harus membelinya terpisah.

  3. Streaming dan proteksi tidak saling eksklusif — pelanggan yang membeli hiburan (StreamingMovies) juga cenderung membeli proteksi. Ini peluang untuk cross-sell proteksi ke pelanggan hiburan.

Yang tidak muncul di top rules: PhoneService, MultipleLines, StreamingTV — layanan ini tampaknya lebih independen, tidak membentuk pola bundling yang kuat. Ini juga insight: tidak semua layanan cocok untuk bundling.

Important

Temuan utama. Tiga layanan—TechSupport, OnlineSecurity, dan DeviceProtection—membentuk klaster bundling alami dengan lift ~1,8. Pelanggan yang memiliki salah satunya 81% lebih mungkin memiliki dua lainnya. Ini adalah kandidat langsung untuk paket proteksi dengan harga khusus.

Sebaliknya, layanan seperti PhoneService dan MultipleLines tidak muncul di rules teratas—artinya mereka tidak membentuk pola bundling yang kuat. Ini juga insight: tidak semua layanan cocok untuk strategi bundling.

11.6.5 📌 Catatan tentang Support 0,123

Support 0,123 pada rule teratas berarti 12,3% pelanggan memiliki kombinasi {TechSupport, DeviceProtection, OnlineSecurity}. Ini adalah basis pelanggan yang cukup besar (~710 pelanggan dari 5.779)—cukup untuk dijadikan target program bundling yang bermakna secara finansial. Ini poin yang bagus untuk ditambahkan ke interpretasi:

“Dengan support 12,3%, ini berarti sekitar 710 pelanggan saat ini sudah memiliki ketiga layanan—dan ribuan lainnya memiliki subset. Program bundling dapat menargetkan mereka yang memiliki 1–2 dari 3 layanan untuk upsell ke paket lengkap.”

11.6.6 Visualisasi Rules

Code
fig, ax = plt.subplots(figsize=(9, 5))
scatter = ax.scatter(rules["support"], rules["confidence"],
                     c=rules["lift"], cmap="YlOrRd",
                     s=50, alpha=0.7, edgecolors="gray")
plt.colorbar(scatter, ax=ax, label="Lift")
ax.set_xlabel("Support")
ax.set_ylabel("Confidence")
ax.set_title("Association Rules — Support vs Confidence (warna = Lift)")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Figure 11.5: Scatter plot association rules layanan Telco: support vs confidence, diwarnai berdasarkan lift. Rules di kanan atas dengan warna gelap adalah kandidat terkuat untuk bundling layanan.

11.7 Menghubungkan Segmen ke Keputusan

Sekarang kita memiliki segmen pelanggan (dari K-means) dan association rules (dari retail). Bagaimana ini terhubung ke keputusan?

11.7.1 Dari Segmentasi ke Alokasi Anggaran

Setiap segmen memiliki churn rate dan nilai yang berbeda. Ini adalah input langsung untuk alokasi anggaran retensi:

Code
# Estimasi nilai per segmen
segment_summary = df.groupby("segment").agg(
    n=("Churn", "size"),
    churn_rate=("Churn", lambda s: (s == "Yes").mean()),
    avg_charges=("MonthlyCharges", "mean"),
    avg_tenure=("tenure", "mean"),
).round(2)

segment_summary["revenue_at_risk"] = (
    segment_summary["n"] * segment_summary["churn_rate"]
    * segment_summary["avg_charges"] * 12  # annualized
).round(0)

segment_summary["priority"] = segment_summary["revenue_at_risk"].rank(ascending=False).astype(int)
segment_summary.sort_values("priority")
n churn_rate avg_charges avg_tenure revenue_at_risk priority
segment
2 2622 0.46 75.83 15.00 1097521.0 1
1 2163 0.15 90.52 58.37 352431.0 2
0 2258 0.15 27.24 27.64 110714.0 3

Interpretasi: Segmen dengan revenue_at_risk tertinggi adalah prioritas utama untuk intervensi retensi. Tetapi ini belum cukup—kita juga perlu mempertimbangkan efektivitas intervensi (seberapa besar churn dapat dikurangi) dan biaya intervensi. Ini akan menjadi topik optimasi di Bab 14–15.

11.7.2 Dari Association Rules ke Strategi Bundling

Rules dengan lift tinggi dan confidence tinggi adalah kandidat untuk strategi bundling. Tetapi tidak semua rules layak secara bisnis:

Rule Lift Confidence Kandidat bundling
{TechSupport} → {DeviceProtection, OnlineSecurity} 1,81 0,35 ✅ Paket Proteksi
{StreamingMovies, OnlineSecurity} → {TechSupport} 1,77 0,33 ⚠️ Cross-sell, bukan bundling penuh
{TechSupport, Internet_DSL} → {OnlineSecurity} 1,76 0,36 ✅ Paket Proteksi DSL
Important

Segmentasi dan association rules adalah input, bukan keputusan. Mereka memberi kita struktur—kelompok pelanggan dan asosiasi produk. Tetapi keputusan apa yang harus dilakukan dengan struktur itu membutuhkan model optimasi yang mempertimbangkan:

  • Anggaran yang tersedia.
  • Biaya setiap intervensi.
  • Efektivitas intervensi per segmen.
  • Kendala operasional (mis. kapasitas tim retensi).

Ini adalah domain Bagian III—dan segmentasi dari bab ini akan menjadi parameter dalam model optimasi tersebut.

11.8 Latihan Terbimbing

Latihan 11.1 — Memilih K. Jalankan elbow method dan silhouette untuk K=2 hingga K=8. Berdasarkan kedua plot, berapa K yang kita pilih? Apakah kedua metode memberi jawaban yang sama? Jika tidak, mana yang kita prioritaskan, dan mengapa?

Latihan 11.2 — Interpretasi segmen. Berdasarkan profil segmen dari K-means, beri nama setiap segmen (mis. “Premium Baru”, “Loyal Hemat”) dan tuliskan satu paragraf yang menjelaskan profil dan strategi yang sesuai untuk masing-masing.

Latihan 11.3 — Hierarchical clustering. Jalankan hierarchical clustering dengan linkage="complete" (bukan ward). Bandingkan dendrogram dan segmen yang dihasilkan dengan ward. Metode linkage mana yang memberi segmen lebih seimbang?

Latihan 11.4 — PCA untuk clustering. Lakukan PCA terlebih dahulu (pertahankan 2 komponen), lalu jalankan K-means pada komponen PCA tersebut. Bandingkan segmen yang dihasilkan dengan K-means pada data asli. Apakah hasilnya lebih baik atau lebih buruk? Mengapa?

Latihan 11.5 — Association rules di Telco. Alih-alih retail, coba temukan association rules di Telco: anggap setiap pelanggan sebagai “transaksi” yang berisi layanan yang mereka berlangganan (PhoneService, OnlineSecurity, dll.). Rules apa yang muncul? Apakah masuk akal secara bisnis?

Latihan 11.6 — Segmentasi dan churn. Bandingkan churn rate antar segmen. Apakah segmen yang dihasilkan K-means memiliki churn rate yang berbeda secara signifikan? Gunakan uji chi-square untuk menguji. Jika ya, segmen ini berguna untuk retensi; jika tidak, mungkin K-means tidak menangkap pola yang relevan dengan churn.

Petunjuk Latihan 11.4. PCA sebelum clustering dapat membantu jika data memiliki banyak fitur berkorelasi (mengurangi noise dan multikolinearitas). Tetapi PCA juga kehilangan informasi—komponen dengan varians rendah mungkin mengandung sinyal yang penting untuk segmentasi. Untuk dataset kecil dengan 4 fitur (seperti kita), PCA mungkin tidak banyak membantu. Untuk dataset dengan 20+ fitur, PCA bisa sangat berguna.

11.9 Kotak “Jembatan”: Dari Struktur ke Optimasi

Important

Jembatan. Bab ini memberi kita struktur yang sebelumnya tidak terlihat:

  1. Segmen pelanggan—kelompok dengan profil dan churn rate berbeda. Ini mengubah cara kita memandang basis pelanggan: bukan sebagai massa homogen, tetapi sebagai portofolio segmen dengan prioritas berbeda.

  2. Komponen utama—reduksi dimensi yang memungkinkan visualisasi dan pemodelan yang lebih efisien. PCA juga berguna untuk deteksi anomali: pelanggan yang jauh dari komponen utama mungkin adalah outlier yang perlu perhatian khusus.

  3. Association rules—pola pembelian bersama yang dapat digunakan untuk cross-selling dan bundling. Ini adalah input untuk strategi pricing dan promosi yang akan dioptimalkan di Bagian III.

Tetapi semua ini masih deskriptif. Kita tahu segmen mana yang berisiko, produk mana yang sering dibeli bersama—tetapi kita belum tahu tindakan optimal:

  • Berapa anggaran yang harus dialokasikan ke setiap segmen?
  • Intervensi apa yang paling efektif per segmen?
  • Paket bundling mana yang memaksimalkan revenue?

Inilah tujuan Bagian III. Segmentasi dari bab ini akan menjadi parameter dalam model optimasi—setiap segmen memiliki churn rate, nilai, dan respons terhadap intervensi yang berbeda. Model optimasi akan memilih kombinasi intervensi yang memaksimalkan dampak di bawah kendala anggaran.

Bab 12—Simulasi Monte Carlo—akan menjadi jembatan terakhir: ia mengajarkan kita untuk memodelkan ketidakpastian yang melekat dalam setiap keputusan, sehingga model optimasi di Bagian III dapat mempertimbangkan risiko, bukan hanya ekspektasi.

11.10 Rangkuman

Dalam bab ini kita telah:

  • Membedakan supervised dari unsupervised learning, memahami bahwa unsupervised learning menemukan struktur tanpa label—berguna untuk segmentasi, reduksi dimensi, dan penemuan pola.
  • Menerapkan K-means clustering dan memilih K secara sistematis dengan elbow method dan silhouette score.
  • Menerapkan hierarchical clustering dan membaca dendrogram untuk memahami struktur hierarkis data.
  • Menginterpretasikan segmen secara bisnis—memberi nama dan strategi untuk setiap segmen berdasarkan profilnya.
  • Menggunakan PCA untuk mereduksi dimensi, memvisualisasikan data berdimensi tinggi, dan memahami loading komponen.
  • Menemukan association rules dengan Apriori pada data retail, memahami support, confidence, dan lift.
  • Menghubungkan segmentasi ke keputusan: segmen dengan revenue-at-risk tertinggi adalah prioritas retensi; rules dengan lift tinggi adalah kandidat bundling.
  • Menyadari batas unsupervised learning: ia menemukan struktur, tetapi tidak memberi tahu apa yang harus dilakukan—keputusan membutuhkan model optimasi.

11.11 Jembatan ke Bab 12

Kita telah membangun model prediktif (Bab 7–9), model waktu (Bab 10), dan segmentasi (Bab 11). Tetapi semua model ini menghasilkan satu titik prediksi—satu probabilitas, satu forecast, satu segmen.

Keputusan nyata selalu menghadapi ketidakpastian: - Permintaan bisa lebih tinggi atau lebih rendah dari forecast. - Churn bisa terjadi lebih cepat atau lebih lambat. - Efektivitas intervensi bisa bervariasi.

Simulasi Monte Carlo—topik Bab 12—akan mengajarkan kita untuk membangkitkan ribuan skenario berdasarkan distribusi probabilitas dari data historis. Ini memungkinkan kita untuk:

  • Menghitung distribusi hasil, bukan hanya nilai tunggal.
  • Mengukur risiko (mis. probabilitas kerugian melebihi ambang tertentu).
  • Mengevaluasi keputusan berdasarkan expected value dan risk profile.

Bab 12 adalah jembatan terakhir sebelum Bagian III: setelah kita dapat memprediksi, mengelompokkan, dan mensimulasikan, kita akan mengoptimalkan.