| """ |
| =============================================================================== |
| SIZINTI HARİTASI (LEAKAGE MAP): Sızıntının Ağ Üzerinde Nasıl Aktığını Göster |
| =============================================================================== |
| |
| HİÇ YAPILMAMIŞ ÖZGÜN KATKI: |
| |
| Random split yaptığınızda test düğümlerinin komşuları eğitim setinde olabiliyor. |
| GraphSAGE gibi GNN'ler komşuluktan bilgi topladığı için, bu komşuluk üzerinden |
| geleceğin bilgisi geçmişe SIZIYOR. |
| |
| Biz bunu ilk kez ölçüyor ve haritasını çıkarıyoruz: |
| |
| 1. KOMŞULUK SIZINTI ORANI: Her test düğümü için komşularının yüzde kaçı |
| eğitim setinde? (Yüksek = çok sızıntı) |
| |
| 2. SIZINTI-BAŞARI KORELASYONU: Komşu sızıntısı yüksek olan düğümler daha mı |
| doğru tahmin ediliyor? (Korelasyon varsa = sızıntı kanıtı) |
| |
| 3. TEMPORAL SIZINTI: Test düğümünün komşuları farklı bir zaman diliminden mi? |
| (Farklı zaman = temporal bilgi sızıntısı) |
| |
| 4. YAPI DUYARLILIĞI: Komşu sızıntısı kaldırıldığında (test düğümlerinin |
| eğitim komşularıyla bağları kesildiğinde) performans ne kadar düşüyor? |
| (Düşüş = model sızıntıya bağımlıydı) |
| =============================================================================== |
| """ |
|
|
| import os, json, warnings |
| import numpy as np |
| import pandas as pd |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| import seaborn as sns |
| from sklearn.preprocessing import StandardScaler |
| from sklearn.metrics import f1_score, roc_auc_score |
| from sklearn.model_selection import train_test_split |
| import lightgbm as lgb |
| warnings.filterwarnings('ignore') |
| np.random.seed(42) |
|
|
| FIGDIR = '/app/figures_proof' |
| OUTDIR = '/app/results_proof' |
|
|
| |
| feat_df = pd.read_csv('/app/data/elliptic_txs_features.csv', header=None) |
| class_df = pd.read_csv('/app/data/elliptic_txs_classes.csv') |
| edge_df = pd.read_csv('/app/data/elliptic_txs_edgelist.csv') |
|
|
| txids = feat_df.iloc[:, 0].values |
| timesteps = feat_df.iloc[:, 1].values.astype(int) |
| features = feat_df.iloc[:, 2:].values.astype(np.float32) |
| N = len(txids) |
|
|
| id_map = {tid: i for i, tid in enumerate(txids)} |
| label_map = {'1': 1, '2': 0, 'unknown': -1} |
| labels = np.array([label_map[str(c)] for c in class_df['class'].values]) |
|
|
| |
| print("Komşuluk sözlüğü oluşturuluyor...") |
| neighbors = {i: set() for i in range(N)} |
| for _, row in edge_df.iterrows(): |
| s = id_map.get(row['txId1']) |
| d = id_map.get(row['txId2']) |
| if s is not None and d is not None: |
| neighbors[s].add(d) |
| neighbors[d].add(s) |
| print(f" Toplam düğüm: {N}, ortalama komşu: {np.mean([len(v) for v in neighbors.values()]):.1f}") |
|
|
| labeled_mask = labels >= 0 |
| X = features[labeled_mask] |
| y = labels[labeled_mask] |
| ts = timesteps[labeled_mask] |
| labeled_indices = np.where(labeled_mask)[0] |
|
|
| |
| idx_all = np.arange(len(y)) |
| train_idx, test_idx = train_test_split(idx_all, test_size=0.2, random_state=42, stratify=y) |
| train_global = set(labeled_indices[train_idx]) |
| test_global = labeled_indices[test_idx] |
|
|
| |
| scaler = StandardScaler() |
| X_tr = scaler.fit_transform(X[train_idx]) |
| X_te = scaler.transform(X[test_idx]) |
| model = lgb.LGBMClassifier(n_estimators=300, max_depth=10, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1) |
| model.fit(X_tr, y[train_idx]) |
| pred = model.predict(X_te) |
| proba = model.predict_proba(X_te)[:, 1] |
| correct = (pred == y[test_idx]) |
|
|
| print(f"\nRandom split F1: {f1_score(y[test_idx], pred):.4f}") |
|
|
| |
| |
| |
| print("\n" + "=" * 70) |
| print("ÖLÇÜM 1: KOMŞULUK SIZINTI ORANI") |
| print("Her test düğümünün komşularının yüzde kaçı eğitim setinde?") |
| print("=" * 70) |
|
|
| leakage_ratios = [] |
| neighbor_counts = [] |
| train_neighbor_ts_diffs = [] |
|
|
| for i, global_idx in enumerate(test_global): |
| nbrs = neighbors[global_idx] |
| if len(nbrs) == 0: |
| leakage_ratios.append(0.0) |
| neighbor_counts.append(0) |
| train_neighbor_ts_diffs.append(0.0) |
| continue |
| |
| train_nbrs = nbrs & train_global |
| ratio = len(train_nbrs) / len(nbrs) |
| leakage_ratios.append(ratio) |
| neighbor_counts.append(len(nbrs)) |
| |
| |
| test_ts = timesteps[global_idx] |
| if len(train_nbrs) > 0: |
| nbr_ts = [timesteps[n] for n in train_nbrs] |
| avg_diff = np.mean(np.abs(np.array(nbr_ts) - test_ts)) |
| train_neighbor_ts_diffs.append(avg_diff) |
| else: |
| train_neighbor_ts_diffs.append(0.0) |
|
|
| leakage_ratios = np.array(leakage_ratios) |
| neighbor_counts = np.array(neighbor_counts) |
| train_neighbor_ts_diffs = np.array(train_neighbor_ts_diffs) |
|
|
| print(f" Ortalama sızıntı oranı: {leakage_ratios.mean():.3f}") |
| print(f" Median sızıntı oranı: {np.median(leakage_ratios):.3f}") |
| print(f" Sızıntısız düğüm (0.0): {(leakage_ratios == 0).sum()} ({(leakage_ratios == 0).mean()*100:.1f}%)") |
| print(f" Tam sızıntı (1.0): {(leakage_ratios == 1).sum()} ({(leakage_ratios == 1).mean()*100:.1f}%)") |
|
|
| |
| |
| |
| print("\n" + "=" * 70) |
| print("ÖLÇÜM 2: SIZINTI-BAŞARI KORELASYONU") |
| print("Komşu sızıntısı yüksek düğümler daha mı kolay tahmin ediliyor?") |
| print("=" * 70) |
|
|
| |
| bins = [0.0, 0.2, 0.4, 0.6, 0.8, 1.01] |
| bin_labels = ['0-20%', '20-40%', '40-60%', '60-80%', '80-100%'] |
| leak_bins = pd.cut(leakage_ratios, bins=bins, labels=bin_labels, include_lowest=True) |
|
|
| print(f"\n Sızıntı grubuna göre doğruluk:") |
| bin_results = [] |
| for bl in bin_labels: |
| mask = (leak_bins == bl) |
| if mask.sum() < 10: |
| continue |
| acc = correct[mask].mean() |
| n = mask.sum() |
| n_ill = y[test_idx][mask].sum() |
| f1_bin = f1_score(y[test_idx][mask], pred[mask], zero_division=0) if len(np.unique(y[test_idx][mask])) > 1 else 0 |
| bin_results.append({'bin': bl, 'accuracy': acc, 'f1': f1_bin, 'n': n, 'n_illicit': n_ill}) |
| print(f" Sızıntı {bl:>8s}: doğruluk={acc:.4f}, F1={f1_bin:.4f} (n={n}, illicit={n_ill})") |
|
|
| |
| corr = np.corrcoef(leakage_ratios[neighbor_counts > 0], |
| correct[neighbor_counts > 0].astype(float))[0, 1] |
| print(f"\n Sızıntı oranı ↔ doğru tahmin korelasyonu: r = {corr:.4f}") |
| if abs(corr) > 0.05: |
| print(f" ⚠️ Pozitif korelasyon: Sızıntı yüksek → tahmin daha kolay") |
|
|
| |
| |
| |
| print("\n" + "=" * 70) |
| print("ÖLÇÜM 3: TEMPORAL SIZINTI") |
| print("Test düğümünün eğitim komşuları farklı zaman diliminden mi?") |
| print("=" * 70) |
|
|
| |
| has_train_nbrs = train_neighbor_ts_diffs > 0 |
| print(f" Ortalama zamansal mesafe (test ↔ eğitim komşu): {train_neighbor_ts_diffs[has_train_nbrs].mean():.2f} timestep") |
| print(f" Aynı timestep'ten komşu oranı: {(train_neighbor_ts_diffs == 0).sum() / max(has_train_nbrs.sum(), 1) * 100:.1f}%") |
|
|
| |
| print("\n Temporal split'te sızıntı oranı:") |
| cutoff = 39 |
| temp_train_global = set(np.where((labels >= 0) & (timesteps <= cutoff))[0]) |
| temp_test_global = np.where((labels >= 0) & (timesteps > cutoff))[0] |
|
|
| temp_leak = [] |
| for global_idx in temp_test_global: |
| nbrs = neighbors[global_idx] |
| if len(nbrs) == 0: |
| temp_leak.append(0.0) |
| continue |
| temp_leak.append(len(nbrs & temp_train_global) / len(nbrs)) |
|
|
| temp_leak = np.array(temp_leak) |
| print(f" Random split ortalama sızıntı oranı: {leakage_ratios.mean():.3f}") |
| print(f" Temporal split ortalama sızıntı oranı: {temp_leak.mean():.3f}") |
| print(f" FARK: Random split {leakage_ratios.mean() / max(temp_leak.mean(), 0.001):.1f}x daha fazla sızıntı!") |
|
|
| |
| |
| |
| print("\n" + "=" * 70) |
| print("ÖLÇÜM 4: YAPI DUYARLILIĞI (Sızıntıya Bağımlılık)") |
| print("Aggregated özellikler (komşuluk bilgisi) çıkarılınca ne oluyor?") |
| print("=" * 70) |
|
|
| |
| |
| X_local = features[labeled_mask][:, :93] |
| X_agg = features[labeled_mask][:, 93:] |
|
|
| |
| scaler_l = StandardScaler() |
| X_tr_l = scaler_l.fit_transform(X_local[train_idx]) |
| X_te_l = scaler_l.transform(X_local[test_idx]) |
| model_l = lgb.LGBMClassifier(n_estimators=300, max_depth=10, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1) |
| model_l.fit(X_tr_l, y[train_idx]) |
| pred_l = model_l.predict(X_te_l) |
| f1_local = f1_score(y[test_idx], pred_l) |
|
|
| |
| scaler_a = StandardScaler() |
| X_tr_a = scaler_a.fit_transform(X_agg[train_idx]) |
| X_te_a = scaler_a.transform(X_agg[test_idx]) |
| model_a = lgb.LGBMClassifier(n_estimators=300, max_depth=10, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1) |
| model_a.fit(X_tr_a, y[train_idx]) |
| pred_a = model_a.predict(X_te_a) |
| f1_agg = f1_score(y[test_idx], pred_a) |
|
|
| |
| f1_all = f1_score(y[test_idx], pred) |
|
|
| |
| tr_t = np.where((labels[labeled_mask] >= 0) & (ts <= cutoff))[0] |
| te_t = np.where((labels[labeled_mask] >= 0) & (ts > cutoff))[0] |
|
|
| if len(tr_t) > 0 and len(te_t) > 0 and len(np.unique(y[te_t])) > 1: |
| |
| scaler_lt = StandardScaler() |
| model_lt = lgb.LGBMClassifier(n_estimators=300, max_depth=10, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1) |
| model_lt.fit(scaler_lt.fit_transform(X_local[tr_t]), y[tr_t]) |
| f1_local_temp = f1_score(y[te_t], model_lt.predict(scaler_lt.transform(X_local[te_t]))) |
| |
| |
| scaler_at = StandardScaler() |
| model_at = lgb.LGBMClassifier(n_estimators=300, max_depth=10, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1) |
| model_at.fit(scaler_at.fit_transform(X_agg[tr_t]), y[tr_t]) |
| f1_agg_temp = f1_score(y[te_t], model_at.predict(scaler_at.transform(X_agg[te_t]))) |
| |
| |
| scaler_allt = StandardScaler() |
| model_allt = lgb.LGBMClassifier(n_estimators=300, max_depth=10, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1) |
| model_allt.fit(scaler_allt.fit_transform(X[tr_t]), y[tr_t]) |
| f1_all_temp = f1_score(y[te_t], model_allt.predict(scaler_allt.transform(X[te_t]))) |
|
|
| print(f"\n RANDOM SPLIT:") |
| print(f" Tüm özellikler: F1 = {f1_all:.4f}") |
| print(f" Sadece local (93 feat): F1 = {f1_local:.4f}") |
| print(f" Sadece aggregated (72): F1 = {f1_agg:.4f}") |
| print(f" Aggregated'ın katkısı: +{f1_all - f1_local:.4f}") |
|
|
| print(f"\n TEMPORAL SPLIT:") |
| print(f" Tüm özellikler: F1 = {f1_all_temp:.4f}") |
| print(f" Sadece local (93 feat): F1 = {f1_local_temp:.4f}") |
| print(f" Sadece aggregated (72): F1 = {f1_agg_temp:.4f}") |
| print(f" Aggregated'ın katkısı: +{f1_all_temp - f1_local_temp:.4f}") |
|
|
| agg_boost_random = f1_all - f1_local |
| agg_boost_temporal = f1_all_temp - f1_local_temp |
| print(f"\n KILIT BULGU:") |
| print(f" Aggregated özelliklerin random split'te ekstra katkısı: +{agg_boost_random:.4f}") |
| print(f" Aggregated özelliklerin temporal split'te ekstra katkısı: +{agg_boost_temporal:.4f}") |
| if agg_boost_random > agg_boost_temporal + 0.01: |
| print(f" ⚠️ Aggregated özellikler random split'te {agg_boost_random/max(agg_boost_temporal, 0.001):.1f}x daha fazla katkı sağlıyor!") |
| print(f" Bu, komşuluk bilgisinin random split'te sızıntı kanalı olduğunun kanıtıdır.") |
|
|
| |
| |
| |
| print("\nFİGÜRLER OLUŞTURULUYOR...") |
| sns.set_theme(style='whitegrid', font_scale=1.1) |
|
|
| |
| fig, axes = plt.subplots(2, 2, figsize=(18, 14)) |
|
|
| |
| axes[0, 0].hist(leakage_ratios, bins=30, color='#FF6B6B', edgecolor='white', alpha=0.8) |
| axes[0, 0].set_xlabel('Komşuluk Sızıntı Oranı', fontsize=12) |
| axes[0, 0].set_ylabel('Düğüm Sayısı', fontsize=12) |
| axes[0, 0].set_title('Her Test Düğümünün Komşuluk Sızıntı Oranı\n(Random Split)', fontsize=13, fontweight='bold') |
| axes[0, 0].axvline(x=leakage_ratios.mean(), color='black', linewidth=2, linestyle='--', |
| label=f'Ortalama: {leakage_ratios.mean():.2f}') |
| axes[0, 0].legend(fontsize=11) |
|
|
| |
| if len(bin_results) > 0: |
| br_df = pd.DataFrame(bin_results) |
| axes[0, 1].bar(br_df['bin'], br_df['f1'], color='#4ECDC4', edgecolor='black') |
| axes[0, 1].set_xlabel('Komşuluk Sızıntı Oranı', fontsize=12) |
| axes[0, 1].set_ylabel('Illicit F1 Score', fontsize=12) |
| axes[0, 1].set_title('Sızıntı ↑ = Tahmin Doğruluğu ↑ ?\n(Korelasyon varsa sızıntı kanıtı)', fontsize=13, fontweight='bold') |
|
|
| |
| data_compare = [ |
| ('Random Split', leakage_ratios.mean()), |
| ('Temporal Split', temp_leak.mean()), |
| ] |
| axes[1, 0].bar([d[0] for d in data_compare], [d[1] for d in data_compare], |
| color=['#FF6B6B', '#4ECDC4'], edgecolor='black') |
| axes[1, 0].set_ylabel('Ortalama Komşuluk Sızıntı Oranı', fontsize=12) |
| axes[1, 0].set_title('Random vs Temporal: Komşuluk Sızıntısı', fontsize=13, fontweight='bold') |
| for i, (name, val) in enumerate(data_compare): |
| axes[1, 0].text(i, val + 0.01, f'{val:.3f}', ha='center', fontsize=13, fontweight='bold') |
|
|
| |
| feat_types = ['Sadece Local\n(93 özellik)', 'Sadece Aggregated\n(72 özellik)', 'Tümü\n(165 özellik)'] |
| random_vals = [f1_local, f1_agg, f1_all] |
| temporal_vals = [f1_local_temp, f1_agg_temp, f1_all_temp] |
| x = np.arange(len(feat_types)) |
| axes[1, 1].bar(x - 0.18, random_vals, 0.32, label='Random Split', color='#FF6B6B') |
| axes[1, 1].bar(x + 0.18, temporal_vals, 0.32, label='Temporal Split', color='#4ECDC4') |
| axes[1, 1].set_xticks(x) |
| axes[1, 1].set_xticklabels(feat_types, fontsize=10) |
| axes[1, 1].set_ylabel('Illicit F1 Score', fontsize=12) |
| axes[1, 1].set_title('Yapı Duyarlılığı: Aggregated Özellikler\nRandom Split\'te Daha mı Çok Katkı Sağlıyor?', fontsize=13, fontweight='bold') |
| axes[1, 1].legend(fontsize=11) |
|
|
| plt.suptitle('SIZINTI HARİTASI: Bilgi Ağ Üzerinden Nasıl Sızıyor?', fontsize=16, fontweight='bold', y=1.02) |
| plt.tight_layout() |
| plt.savefig(f'{FIGDIR}/fig7_leakage_map.png', dpi=150, bbox_inches='tight') |
| plt.close() |
| print(" ✓ Figür 7: Sızıntı Haritası") |
|
|
| |
| results = { |
| 'avg_leakage_ratio_random': float(leakage_ratios.mean()), |
| 'avg_leakage_ratio_temporal': float(temp_leak.mean()), |
| 'leakage_ratio_multiplier': float(leakage_ratios.mean() / max(temp_leak.mean(), 0.001)), |
| 'correlation_leakage_accuracy': float(corr), |
| 'f1_local_random': float(f1_local), |
| 'f1_agg_random': float(f1_agg), |
| 'f1_all_random': float(f1_all), |
| 'f1_local_temporal': float(f1_local_temp), |
| 'f1_agg_temporal': float(f1_agg_temp), |
| 'f1_all_temporal': float(f1_all_temp), |
| 'agg_boost_random': float(agg_boost_random), |
| 'agg_boost_temporal': float(agg_boost_temporal), |
| } |
| with open(f'{OUTDIR}/leakage_map_results.json', 'w') as f: |
| json.dump(results, f, indent=2) |
|
|
| print("\n✓ Tamamlandı!") |
|
|