Lompat ke konten
nalar

Stream & Graph Mining

Mengolah aliran data real-time dengan sliding/decaying window dan reservoir sampling, mencari dokumen mirip dengan MinHash & LSH, serta meranking simpul graf dengan PageRank.

Notebook lab (pratinjau)

Unduh lalu jalankan di Jupyter, VS Code, atau Google Colab.

Unduh lab.ipynb

Lab Minggu 15 — Stream & Graph Mining

Tujuan lab:

  1. Mensimulasikan data stream dan menghitung sliding window, decaying window, reservoir sampling.
  2. Mencari dokumen mirip dengan Jaccard → MinHash → LSH.
  3. Menghitung PageRank dengan power iteration dan membandingkannya dengan NetworkX.

Baca lesson.md dulu. Jalankan sel satu per satu (Shift+Enter).

[1]
import sys
sys.path.insert(0, "..")

import random
from collections import deque, defaultdict
from itertools import combinations

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import networkx as nx

from common.datasets import dokumen_teks, jaringan_web, log_transaksi

1. Simulasi data stream

Kita perlakukan log_transaksi() sebagai stream: setiap baris datang satu per satu. Generator Python cocok untuk meniru stream — data tidak dimuat sekaligus.

[2]
def stream_nilai_transaksi():
    for baris in log_transaksi():
        tgl, kota, produk, jumlah, harga = baris.split(",")
        yield int(jumlah) * int(harga)

s = stream_nilai_transaksi()
print([next(s) for _ in range(5)])

2. Sliding window dengan jumlah berjalan

[3]
def sliding_mean(stream, w):
    jendela, total = deque(maxlen=w), 0.0
    for x in stream:
        if len(jendela) == w:
            total -= jendela[0]
        jendela.append(x)
        total += x
        if len(jendela) == w:
            yield total / w

nilai = list(stream_nilai_transaksi())[:1000]
sw = list(sliding_mean(nilai, 50))

3. Exponentially decaying window

Untuk membandingkan dengan rata-rata, kita pakai rata-rata berbobot S_t · c (karena jumlah bobot ≈ 1/c).

[4]
def decaying(stream, c):
    s = 0.0
    for x in stream:
        s = (1 - c) * s + x
        yield s

c = 1 / 50
dw = [v * c for v in decaying(nilai, c)]

plt.figure(figsize=(12, 3.5))
plt.plot(nilai, alpha=0.25, label="nilai transaksi")
plt.plot(range(49, 1000), sw, label="sliding window (w=50)")
plt.plot(dw, label="decaying window (c=1/50)")
plt.legend(); plt.title("Ringkasan stream"); plt.show()

Decaying window bereaksi halus dan tidak butuh menyimpan 50 elemen — cukup satu angka.

Setiap produk punya skor sendiri; skor semua produk meluruh tiap ada transaksi baru.

[5]
def trending(stream_baris, c=0.01):
    skor = defaultdict(float)
    for baris in stream_baris:
        produk = baris.split(",")[2]
        for p in skor:
            skor[p] *= (1 - c)
        skor[produk] += 1
    return pd.Series(skor).sort_values(ascending=False)

trending(log_transaksi()).head(5)

4. Reservoir sampling

Ambil 5 transaksi acak seragam tanpa tahu panjang stream.

[6]
def reservoir(stream, k, seed=0):
    rng = random.Random(seed)
    res = []
    for i, x in enumerate(stream):
        if i < k:
            res.append(x)
        else:
            j = rng.randint(0, i)
            if j < k:
                res[j] = x
    return res

reservoir(log_transaksi(), 5, seed=42)

Bukti empiris keseragaman: ulangi 3000 kali pada stream 0..19, setiap angka mestinya terpilih ≈ k/n = 25%.

[7]
hitung = np.zeros(20)
for s_ in range(3000):
    for x in reservoir(range(20), 5, seed=s_):
        hitung[x] += 1
plt.bar(range(20), hitung / 3000); plt.axhline(0.25, color="r", ls="--")
plt.title("Peluang terpilih tiap elemen"); plt.show()

5. Kemiripan dokumen: Jaccard

[8]
docs = dokumen_teks()
himp = [set(t.lower().split()) for t in docs]

def jaccard(a, b):
    return len(a & b) / len(a | b)

n = len(docs)
J = np.array([[jaccard(himp[i], himp[j]) for j in range(n)] for i in range(n)])
pd.DataFrame(J).round(2)

6. MinHash signature

[9]
P = 2_147_483_647
vocab = sorted(set().union(*himp))
idx = {v: i for i, v in enumerate(vocab)}

def minhash(himpunan, n_hash=100, seed=0):
    rng = np.random.default_rng(seed)
    a = rng.integers(1, P, n_hash, dtype=np.int64)
    b = rng.integers(0, P, n_hash, dtype=np.int64)
    sig = np.zeros((n_hash, len(himpunan)), dtype=np.int64)
    for d, h in enumerate(himpunan):
        ids = np.array([idx[x] for x in h], dtype=np.int64)
        sig[:, d] = ((a[:, None] * ids[None, :] + b[:, None]) % P).min(axis=1)
    return sig

sig = minhash(himp, 200, seed=1)
print("ukuran signature:", sig.shape)

est = np.array([[np.mean(sig[:, i] == sig[:, j]) for j in range(n)] for i in range(n)])
iu = np.triu_indices(n, 1)
plt.scatter(J[iu], est[iu]); plt.plot([0, 1], [0, 1], "r--")
plt.xlabel("Jaccard asli"); plt.ylabel("Estimasi MinHash"); plt.show()

7. LSH banding

[10]
def lsh(sig, bands):
    r = sig.shape[0] // bands
    kandidat = set()
    for b in range(bands):
        bucket = defaultdict(list)
        for d in range(sig.shape[1]):
            bucket[tuple(sig[b * r:(b + 1) * r, d])].append(d)
        for anggota in bucket.values():
            kandidat.update(combinations(anggota, 2))
    return kandidat

kand = lsh(sig, bands=50)   # r = 4
print(f"Kandidat: {len(kand)} dari {n*(n-1)//2} pasangan")
for i, j in sorted(kand):
    print(f"({i},{j}) J={J[i,j]:.2f}  |  {docs[i][:40]}…  ~  {docs[j][:40]}…")

Kurva-S LSH

[11]
s_ = np.linspace(0, 1, 200)
for b, r in [(50, 4), (20, 10), (100, 2)]:
    plt.plot(s_, 1 - (1 - s_ ** r) ** b, label=f"b={b}, r={r}, ambang≈{(1/b)**(1/r):.2f}")
plt.xlabel("kemiripan s"); plt.ylabel("P(kandidat)"); plt.legend(); plt.show()

8. PageRank

[12]
edges = jaringan_web()
G = nx.DiGraph(edges)
nx.draw_networkx(G, pos=nx.spring_layout(G, seed=1), node_color="#9ecae1", arrows=True)
plt.axis("off"); plt.show()
[13]
def pagerank(edges, d=0.85, tol=1e-10, max_iter=200):
    simpul = sorted({u for e in edges for u in e})
    N = len(simpul)
    keluar = defaultdict(list)
    for u, v in edges:
        keluar[u].append(v)
    r = {u: 1 / N for u in simpul}
    for it in range(max_iter):
        bocor = sum(r[u] for u in simpul if u not in keluar)
        baru = {u: (1 - d) / N + d * bocor / N for u in simpul}
        for u, tujuan in keluar.items():
            for v in tujuan:
                baru[v] += d * r[u] / len(tujuan)
        selisih = sum(abs(baru[u] - r[u]) for u in simpul)
        r = baru
        if selisih < tol:
            break
    print("konvergen setelah", it + 1, "iterasi")
    return r

pr = pagerank(edges)
pd.DataFrame({"manual": pr, "networkx": nx.pagerank(G, alpha=0.85)}).sort_values("manual", ascending=False).round(4)

Bentuk matriks: r = (1-d)/N + d·M·r. Matriks transisi M (kolom = sumber):

[14]
nodes = sorted(G.nodes)
M = nx.to_numpy_array(G, nodelist=nodes).T
M = M / M.sum(axis=0, keepdims=True)
pd.DataFrame(M, index=nodes, columns=nodes).round(2)

✍️ Coba sendiri

  1. Ubah w sliding window menjadi 10 dan 200. Apa efeknya pada kehalusan kurva?
  2. Tambahkan dokumen baru yang hampir sama dengan dokumen ke-3, lalu jalankan MinHash + LSH. Apakah pasangan barunya tertangkap?
  3. Coba bands=10 (r = 20). Berapa kandidat yang tersisa? Jelaskan dengan kurva-S.
  4. Tambahkan simpul dangling H (C→H) ke graf. Bagaimana PageRank berubah? Bandingkan dengan NetworkX.
  5. Ubah damping d = 0.5 dan d = 0.99. Simpul mana yang paling terpengaruh?
[15]
# 1.
[16]
# 2.
[17]
# 3.
[18]
# 4.

Jawaban no. 5:

  • ...

Selanjutnya

Kerjakan kuis dan exercise.py, lalu jalankan python check.py 15 dari folder utama.