""" =============================================================================== EVENT-AWARE DATA SPLITTING FOR BITCOIN ANTI-MONEY LAUNDERING =============================================================================== Elliptic Bitcoin Dataset üzerinde "Olay-Farkında Veri Bölme" çalışması. BAĞLAM: Kripto para dünyası devasa bir sosyal ağdır. Cüzdanlar insanları, para transferleri aralarındaki ilişkileri temsil eder. Bu ağ statik değildir; büyür, küçülür ve değişir. Dark market kapanmaları, piyasa çöküşleri ve düzenleyici operasyonlar ağın yapısını kökten değiştirir. TEZ: Araştırmacılar karmaşık GNN/DL modelleri geliştirmek yerine, önce veriyi doğru ve "olay-farkında" bir şekilde bölmeye odaklanmalıdır. Aksi halde, kara para aklama tespit sistemleri kritik durumlarda çöker. LİTERATÜRDEKİ MEVCUT YAKLAŞIM: - Kronolojik bölme: "İlk 34 timestep eğitim, son 15 test" - Kayan pencere: Sabit pencere ile ileri kaydırma - Rastgele bölme: Her timestep'ten rastgele örnekleme BİZİM KATKIMIZ — OLAY-FARKINDA BÖLME: Bitcoin ağındaki "olayları" tanımlayıp, veriyi olay sınırlarına göre bölüyoruz: OLAY TİPLERİ (Elliptic dataset'teki timestep analizi ile tespit edilen): 1. DARK_MARKET_ACTIVE: TS 9,13,15,16,20,25,26,28,29,32 (illicit rate >18%) 2. DARK_MARKET_SHUTDOWN: TS 43 (Bilinen dark market kapanması) 3. POST_SHUTDOWN_QUIET: TS 44,45,46 (Kapanma sonrası sessiz dönem) 4. RECOVERY_PHASE: TS 47,48,49 (Yeniden yükseliş dönemi) 5. NORMAL_ACTIVITY: Geri kalan timestep'ler (düşük illicit oranlar) 5 BÖLME STRATEJİSİ: 1. Rastgele (Naive): Standart train_test_split 2. Kronolojik (Mevcut yaklaşım): İlk %80 eğitim, son %20 test 3. Olay-Farkında (Bizim katkımız): Olay pencerelerini bütün tutarak böl 4. Tabakalı-Kronolojik: Her olay tipinden kronolojik bölme 5. Düşmanca-Olay (Stres testi): Sadece normal dönemde eğit, sadece olay dönemlerinde test et 4 MODEL (Kasıtlı olarak basit — mesele model değil, bölme stratejisi): - Logistic Regression - Random Forest - XGBoost - LightGBM =============================================================================== """ import os import json import warnings import numpy as np import pandas as pd from collections import defaultdict import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as mpatches import seaborn as sns from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import ( roc_auc_score, average_precision_score, f1_score, precision_score, recall_score, confusion_matrix ) import xgboost as xgb import lightgbm as lgb warnings.filterwarnings('ignore') np.random.seed(42) os.makedirs('/app/results_elliptic', exist_ok=True) os.makedirs('/app/figures_elliptic', exist_ok=True) # ============================================================================ # ADIM 1: VERİ YÜKLEME VE HAZIRLAMA # ============================================================================ print("=" * 80) print("ADIM 1: ELLIPTIC BITCOIN VERİ SETİ YÜKLEME VE HAZIRLAMA") print("=" * 80) # Üç dosyayı yükle print("\nSınıflar yükleniyor...") classes_df = pd.read_csv('/app/data/elliptic_txs_classes.csv') print(f" Sınıflar: {classes_df.shape}") print("Özellikler yükleniyor...") features_df = pd.read_csv('/app/data/elliptic_txs_features.csv', header=None) features_df.columns = ['txId', 'timestep'] + [f'feat_{i}' for i in range(2, 167)] print(f" Özellikler: {features_df.shape} ({features_df.shape[1]-2} özellik)") print("Kenarlar yükleniyor...") edges_df = pd.read_csv('/app/data/elliptic_txs_edgelist.csv') print(f" Kenarlar: {edges_df.shape}") # Birleştir df = features_df.merge(classes_df, on='txId', how='left') # Sadece etiketli düğümleri al (illicit=1, licit=2) df_labeled = df[df['class'].isin(['1', '2'])].copy() df_labeled['is_illicit'] = (df_labeled['class'] == '1').astype(int) df_labeled['timestep'] = df_labeled['timestep'].astype(int) print(f"\nToplam düğüm: {len(df)}") print(f"Etiketli düğüm: {len(df_labeled)}") print(f" Illicit (kara para): {df_labeled['is_illicit'].sum()} ({df_labeled['is_illicit'].mean()*100:.1f}%)") print(f" Licit (yasal): {(1-df_labeled['is_illicit']).sum()}") print(f"Timestep aralığı: {df_labeled['timestep'].min()}-{df_labeled['timestep'].max()} (49 adım)") # ============================================================================ # ADIM 2: AĞ ÖZELLİKLERİ MÜHENDİSLİĞİ # ============================================================================ print("\n" + "=" * 80) print("ADIM 2: AĞ ÖZELLİKLERİ MÜHENDİSLİĞİ (GRAF FEATUREleri)") print("=" * 80) # Ağ topoloji özellikleri hesapla print("Ağ topoloji özellikleri hesaplanıyor...") # Derece bilgileri (in-degree, out-degree) out_degree = edges_df.groupby('txId1').size().reset_index(name='out_degree') in_degree = edges_df.groupby('txId2').size().reset_index(name='in_degree') # Toplam derece out_degree.columns = ['txId', 'out_degree'] in_degree.columns = ['txId', 'in_degree'] df_labeled = df_labeled.merge(out_degree, on='txId', how='left') df_labeled = df_labeled.merge(in_degree, on='txId', how='left') df_labeled['out_degree'] = df_labeled['out_degree'].fillna(0) df_labeled['in_degree'] = df_labeled['in_degree'].fillna(0) df_labeled['total_degree'] = df_labeled['out_degree'] + df_labeled['in_degree'] df_labeled['degree_ratio'] = df_labeled['out_degree'] / (df_labeled['in_degree'] + 1) # Her timestep'teki ağ yoğunluğu ts_edge_counts = {} for ts in df_labeled['timestep'].unique(): ts_nodes = set(df[df['timestep'] == ts]['txId'].values) ts_edges = edges_df[ (edges_df['txId1'].isin(ts_nodes)) & (edges_df['txId2'].isin(ts_nodes)) ] ts_edge_counts[ts] = len(ts_edges) df_labeled['ts_edge_count'] = df_labeled['timestep'].map(ts_edge_counts) # Timestep başına istatistikler ts_stats = df_labeled.groupby('timestep').agg( ts_node_count=('txId', 'count'), ts_illicit_ratio=('is_illicit', 'mean'), ts_mean_degree=('total_degree', 'mean'), ts_std_degree=('total_degree', 'std') ).reset_index() df_labeled = df_labeled.merge(ts_stats, on='timestep', how='left') # Özellik listesi feature_cols = [f'feat_{i}' for i in range(2, 167)] + [ 'out_degree', 'in_degree', 'total_degree', 'degree_ratio', 'ts_edge_count', 'ts_node_count', 'ts_mean_degree', 'ts_std_degree' ] print(f"Toplam özellik sayısı: {len(feature_cols)}") print(f" - Orijinal özellikler: 165") print(f" - Ağ topoloji özellikleri: 4 (in/out/total degree, degree ratio)") print(f" - Timestep ağ özellikleri: 4 (edge count, node count, mean/std degree)") # ============================================================================ # ADIM 3: OLAY TİPLERİNİ TANIMLAMA (ANA KATKI) # ============================================================================ print("\n" + "=" * 80) print("ADIM 3: BITCOIN AĞ OLAYLARINI TANIMLAMA (ANA KATKI)") print("=" * 80) print(""" Bitcoin ağındaki "olayları" illicit oranı, ağ yapısı ve bilinen gerçek-dünya olaylarına göre tanımlıyoruz: • DARK_MARKET_PEAK: İllicit oranı >%18 olan timestep'ler (Dark market operasyonlarının yoğun olduğu dönemler) • DARK_MARKET_SHUTDOWN: TS 43 (Bilinen dark market kapanması - Inspection-L makalesinde doğrulanmış) • POST_SHUTDOWN_SILENCE: TS 44-46 (Kapanma sonrası illicit aktivitenin sessizleştiği dönem) • RECOVERY_PHASE: TS 47-49 (Yeni aktörlerin sisteme girdiği toparlanma dönemi) • NORMAL_ACTIVITY: Geri kalan tüm timestep'ler """) # Olay sınıflandırması DARK_MARKET_PEAK_TS = set() DARK_MARKET_SHUTDOWN_TS = {43} POST_SHUTDOWN_TS = {44, 45, 46} RECOVERY_TS = {47, 48, 49} # Illicit oranı yüksek timestep'leri bul for ts in sorted(df_labeled['timestep'].unique()): ts_data = df_labeled[df_labeled['timestep'] == ts] illicit_rate = ts_data['is_illicit'].mean() if illicit_rate > 0.18 and ts not in DARK_MARKET_SHUTDOWN_TS | POST_SHUTDOWN_TS | RECOVERY_TS: DARK_MARKET_PEAK_TS.add(ts) def classify_event(ts): if ts in DARK_MARKET_PEAK_TS: return 'dark_market_peak' elif ts in DARK_MARKET_SHUTDOWN_TS: return 'shutdown' elif ts in POST_SHUTDOWN_TS: return 'post_shutdown' elif ts in RECOVERY_TS: return 'recovery' else: return 'normal' df_labeled['event_type'] = df_labeled['timestep'].apply(classify_event) # Olay dağılımı print("OLAY TİPİ DAĞILIMI:") print("-" * 70) event_summary = df_labeled.groupby('event_type').agg( n_transactions=('txId', 'count'), n_illicit=('is_illicit', 'sum'), illicit_rate=('is_illicit', 'mean'), timesteps=('timestep', lambda x: sorted(x.unique())), n_timesteps=('timestep', 'nunique') ).round(4) for event, row in event_summary.iterrows(): ts_list = ', '.join(map(str, row['timesteps'][:8])) if len(row['timesteps']) > 8: ts_list += f'... (+{len(row["timesteps"])-8} more)' print(f" {event:25s}: {row['n_transactions']:5d} tx, " f"{row['n_illicit']:4.0f} illicit ({row['illicit_rate']*100:.1f}%), " f"{row['n_timesteps']:2d} ts [{ts_list}]") # ============================================================================ # ADIM 4: BÖLME STRATEJİLERİ # ============================================================================ print("\n" + "=" * 80) print("ADIM 4: 5 BÖLME STRATEJİSİ TANIMLAMA") print("=" * 80) X_all = df_labeled[feature_cols].values y_all = df_labeled['is_illicit'].values timesteps_all = df_labeled['timestep'].values events_all = df_labeled['event_type'].values def split_random(X, y, timesteps, events, test_size=0.2): """Strateji 1: Rastgele bölme (çoğu makalenin kullandığı)""" idx = np.arange(len(y)) train_idx, test_idx = train_test_split(idx, test_size=test_size, random_state=42, stratify=y) return (X[train_idx], X[test_idx], y[train_idx], y[test_idx], events[test_idx], timesteps[test_idx]) def split_chronological(X, y, timesteps, events, test_ratio=0.2): """Strateji 2: Kronolojik bölme (mevcut literatür standardı)""" n_test_ts = max(1, int(49 * test_ratio)) # Son ~10 timestep test cutoff = 49 - n_test_ts train_mask = timesteps <= cutoff test_mask = timesteps > cutoff return (X[train_mask], X[test_mask], y[train_mask], y[test_mask], events[test_mask], timesteps[test_mask]) def split_event_aware(X, y, timesteps, events, test_ratio=0.2): """ Strateji 3: OLAY-FARKINDA BÖLME (BİZİM KATKIMIZ) Algoritma: 1. Her olay tipinden son oluşumları test setine ata 2. Olay pencerelerini bütün tut (asla bir olay penceresini bölme) 3. Test seti tüm olay tiplerini temsil etsin 4. Kronolojik sırayı mümkün olduğunca koru Bu sayede model, her olay tipinin GÖRÜLMEMIŞ bir örneğine karşı test edilir. """ test_timesteps = set() # Her olay tipinin son oluşumlarını test setine ata for event in df_labeled['event_type'].unique(): event_ts = sorted(df_labeled[df_labeled['event_type'] == event]['timestep'].unique()) n_test = max(1, int(len(event_ts) * test_ratio)) test_timesteps.update(event_ts[-n_test:]) train_mask = ~np.isin(timesteps, list(test_timesteps)) test_mask = np.isin(timesteps, list(test_timesteps)) return (X[train_mask], X[test_mask], y[train_mask], y[test_mask], events[test_mask], timesteps[test_mask]) def split_stratified_temporal(X, y, timesteps, events, test_ratio=0.2): """Strateji 4: Tabakalı-kronolojik (her olay tipinde kronolojik bölme)""" train_indices = [] test_indices = [] for event in np.unique(events): event_mask = events == event event_indices = np.where(event_mask)[0] event_ts = timesteps[event_mask] # Bu olay tipindeki timestep'leri sırala sorted_order = np.argsort(event_ts) sorted_indices = event_indices[sorted_order] split_point = int(len(sorted_indices) * (1 - test_ratio)) train_indices.extend(sorted_indices[:split_point]) test_indices.extend(sorted_indices[split_point:]) train_idx = np.array(train_indices) test_idx = np.array(test_indices) return (X[train_idx], X[test_idx], y[train_idx], y[test_idx], events[test_idx], timesteps[test_idx]) def split_adversarial_event(X, y, timesteps, events): """ Strateji 5: DÜŞMANCA-OLAY BÖLME (Stres Testi) Sadece normal dönemde eğit, TÜM olay dönemlerinde test et. Bu, en kötü senaryoyu simüle eder: normal koşullarda eğitilmiş bir model, birden dark market kapanması, sessiz dönem veya toparlanma ile karşılaşır. """ train_mask = events == 'normal' test_mask = events != 'normal' return (X[train_mask], X[test_mask], y[train_mask], y[test_mask], events[test_mask], timesteps[test_mask]) strategies = { 'Rastgele (Naive)': split_random, 'Kronolojik (Mevcut)': split_chronological, 'Olay-Farkında (Bizim)': split_event_aware, 'Tabakalı-Kronolojik': split_stratified_temporal, 'Düşmanca-Olay': split_adversarial_event, } for name in strategies: print(f" • {name}") # ============================================================================ # ADIM 5: MODELLER # ============================================================================ print("\n" + "=" * 80) print("ADIM 5: MODELLER (KASITLI OLARAK BASİT)") print("=" * 80) def get_models(): return { 'Logistic Regression': LogisticRegression( max_iter=2000, class_weight='balanced', random_state=42, n_jobs=-1, C=0.1 ), 'Random Forest': RandomForestClassifier( n_estimators=300, max_depth=15, class_weight='balanced', random_state=42, n_jobs=-1 ), 'XGBoost': xgb.XGBClassifier( n_estimators=300, max_depth=8, learning_rate=0.1, scale_pos_weight=10, random_state=42, n_jobs=-1, eval_metric='aucpr', verbosity=0 ), 'LightGBM': lgb.LGBMClassifier( n_estimators=300, max_depth=10, learning_rate=0.1, scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1 ), } for name in get_models(): print(f" • {name}") # ============================================================================ # ADIM 6: DENEYLER # ============================================================================ print("\n" + "=" * 80) print("ADIM 6: DENEYLER ÇALIŞTIRILIYOR") print("=" * 80) results = [] detailed_results = defaultdict(dict) timestep_results = defaultdict(dict) for split_name, split_fn in strategies.items(): print(f"\n{'─' * 60}") print(f"Bölme Stratejisi: {split_name}") print(f"{'─' * 60}") if split_name == 'Düşmanca-Olay': X_train, X_test, y_train, y_test, test_events, test_ts = split_fn( X_all, y_all, timesteps_all, events_all ) else: X_train, X_test, y_train, y_test, test_events, test_ts = split_fn( X_all, y_all, timesteps_all, events_all ) train_illicit_rate = y_train.mean() * 100 test_illicit_rate = y_test.mean() * 100 print(f" Eğitim: {len(y_train)} ({y_train.sum()} illicit, %{train_illicit_rate:.1f})") print(f" Test: {len(y_test)} ({y_test.sum()} illicit, %{test_illicit_rate:.1f})") print(f" Test olay dağılımı: {dict(zip(*np.unique(test_events, return_counts=True)))}") # Ölçeklendirme scaler = StandardScaler() X_train_sc = scaler.fit_transform(X_train) X_test_sc = scaler.transform(X_test) # NaN temizliği X_train_sc = np.nan_to_num(X_train_sc, nan=0.0) X_test_sc = np.nan_to_num(X_test_sc, nan=0.0) X_train_clean = np.nan_to_num(X_train, nan=0.0) X_test_clean = np.nan_to_num(X_test, nan=0.0) for model_name, model in get_models().items(): print(f" {model_name} eğitiliyor...", end=" ", flush=True) use_scaled = model_name == 'Logistic Regression' Xtr = X_train_sc if use_scaled else X_train_clean Xte = X_test_sc if use_scaled else X_test_clean model.fit(Xtr, y_train) y_pred_proba = model.predict_proba(Xte)[:, 1] y_pred = model.predict(Xte) # Genel metrikler if len(np.unique(y_test)) > 1: auroc = roc_auc_score(y_test, y_pred_proba) auprc = average_precision_score(y_test, y_pred_proba) else: auroc = auprc = 0.0 f1 = f1_score(y_test, y_pred, zero_division=0) prec = precision_score(y_test, y_pred, zero_division=0) rec = recall_score(y_test, y_pred, zero_division=0) results.append({ 'Bölme Stratejisi': split_name, 'Model': model_name, 'AUROC': round(auroc, 4), 'AUPRC': round(auprc, 4), 'Illicit F1': round(f1, 4), 'Precision': round(prec, 4), 'Recall': round(rec, 4), 'Eğitim Boyutu': len(y_train), 'Test Boyutu': len(y_test), 'Test Illicit Oranı': round(y_test.mean(), 4), }) print(f"AUROC={auroc:.4f}, AUPRC={auprc:.4f}, F1={f1:.4f}") # Olay tipine göre kırılım event_metrics = {} for event in np.unique(test_events): mask = test_events == event if mask.sum() < 5: continue if len(np.unique(y_test[mask])) < 2: event_metrics[event] = { 'AUROC': 0.5, 'AUPRC': y_test[mask].mean(), 'F1': f1_score(y_test[mask], y_pred[mask], zero_division=0), 'n_samples': int(mask.sum()), 'n_illicit': int(y_test[mask].sum()) } continue event_metrics[event] = { 'AUROC': round(roc_auc_score(y_test[mask], y_pred_proba[mask]), 4), 'AUPRC': round(average_precision_score(y_test[mask], y_pred_proba[mask]), 4), 'F1': round(f1_score(y_test[mask], y_pred[mask], zero_division=0), 4), 'n_samples': int(mask.sum()), 'n_illicit': int(y_test[mask].sum()), } detailed_results[f"{split_name}|{model_name}"] = event_metrics # Timestep bazında kırılım ts_metrics = {} for ts in np.unique(test_ts): mask = test_ts == ts if mask.sum() < 5: continue if len(np.unique(y_test[mask])) < 2: ts_metrics[int(ts)] = { 'AUROC': 0.5, 'F1': f1_score(y_test[mask], y_pred[mask], zero_division=0), 'n_samples': int(mask.sum()), 'n_illicit': int(y_test[mask].sum()), 'event_type': classify_event(int(ts)) } continue ts_metrics[int(ts)] = { 'AUROC': round(roc_auc_score(y_test[mask], y_pred_proba[mask]), 4), 'F1': round(f1_score(y_test[mask], y_pred[mask], zero_division=0), 4), 'n_samples': int(mask.sum()), 'n_illicit': int(y_test[mask].sum()), 'event_type': classify_event(int(ts)) } timestep_results[f"{split_name}|{model_name}"] = ts_metrics # ============================================================================ # ADIM 7: ANALİZ VE GÖRSELLEŞTİRME # ============================================================================ print("\n" + "=" * 80) print("ADIM 7: ANALİZ VE GÖRSELLEŞTİRME") print("=" * 80) results_df = pd.DataFrame(results) results_df.to_csv('/app/results_elliptic/experiment_results.csv', index=False) # ── TABLO 1: GENEL SONUÇLAR ── print("\n" + "=" * 60) print("TABLO 1: AUROC SONUÇLARI") print("=" * 60) pivot_auroc = results_df.pivot_table(values='AUROC', index='Model', columns='Bölme Stratejisi') print(pivot_auroc.to_string()) print("\n" + "=" * 60) print("TABLO 2: ILLICIT F1 SONUÇLARI") print("=" * 60) pivot_f1 = results_df.pivot_table(values='Illicit F1', index='Model', columns='Bölme Stratejisi') print(pivot_f1.to_string()) print("\n" + "=" * 60) print("TABLO 3: AUPRC SONUÇLARI") print("=" * 60) pivot_auprc = results_df.pivot_table(values='AUPRC', index='Model', columns='Bölme Stratejisi') print(pivot_auprc.to_string()) # ── PERFORMANS DÜŞÜŞÜ ANALİZİ ── print("\n" + "=" * 60) print("KİLİT BULGU: RASTGELE BÖLMEDEN OLAY-FARKINDA BÖLMEYE PERFORMANS DEĞİŞİMİ") print("=" * 60) model_names = list(get_models().keys()) for model_name in model_names: random_auroc = results_df[(results_df['Model'] == model_name) & (results_df['Bölme Stratejisi'] == 'Rastgele (Naive)')]['AUROC'].values[0] event_auroc = results_df[(results_df['Model'] == model_name) & (results_df['Bölme Stratejisi'] == 'Olay-Farkında (Bizim)')]['AUROC'].values[0] adv_auroc = results_df[(results_df['Model'] == model_name) & (results_df['Bölme Stratejisi'] == 'Düşmanca-Olay')]['AUROC'].values[0] chrono_auroc = results_df[(results_df['Model'] == model_name) & (results_df['Bölme Stratejisi'] == 'Kronolojik (Mevcut)')]['AUROC'].values[0] drop_event = ((random_auroc - event_auroc) / random_auroc) * 100 drop_adv = ((random_auroc - adv_auroc) / random_auroc) * 100 drop_chrono = ((random_auroc - chrono_auroc) / random_auroc) * 100 print(f"\n{model_name}:") print(f" Rastgele AUROC: {random_auroc:.4f}") print(f" Kronolojik AUROC: {chrono_auroc:.4f} (Δ = {drop_chrono:+.2f}%)") print(f" Olay-Farkında AUROC: {event_auroc:.4f} (Δ = {drop_event:+.2f}%)") print(f" Düşmanca-Olay AUROC: {adv_auroc:.4f} (Δ = {drop_adv:+.2f}%)") # ── OLAY BAZLI KIRILIM ── print("\n" + "=" * 60) print("OLAY TİPİNE GÖRE PERFORMANS (Random Forest)") print("=" * 60) for split_name in ['Rastgele (Naive)', 'Olay-Farkında (Bizim)', 'Düşmanca-Olay']: key = f"{split_name}|Random Forest" if key in detailed_results: print(f"\n {split_name}:") for event, m in sorted(detailed_results[key].items()): status = "🔴 KRİTİK" if m['AUROC'] < 0.7 else "🟡 UYARI" if m['AUROC'] < 0.85 else "🟢 İYİ" print(f" {event:25s} AUROC={m['AUROC']:.4f} F1={m['F1']:.4f} " f"(n={m['n_samples']}, illicit={m['n_illicit']}) {status}") # ============================================================================ # ADIM 8: YAYIN KALİTESİNDE FİGÜRLER # ============================================================================ print("\n" + "=" * 80) print("ADIM 8: FİGÜRLER OLUŞTURULUYOR") print("=" * 80) sns.set_theme(style='whitegrid', font_scale=1.1) # ── FİGÜR 1: Bitcoin Ağında İllicit Oranı ve Olay Haritası ── fig, axes = plt.subplots(3, 1, figsize=(18, 14), gridspec_kw={'height_ratios': [2, 1, 1]}) # 1a: Timestep başına illicit oranı ts_analysis = df_labeled.groupby('timestep').agg( illicit_rate=('is_illicit', 'mean'), n_total=('txId', 'count'), n_illicit=('is_illicit', 'sum') ).reset_index() colors_ts = [] for ts in ts_analysis['timestep']: evt = classify_event(ts) if evt == 'dark_market_peak': colors_ts.append('#FF4444') elif evt == 'shutdown': colors_ts.append('#000000') elif evt == 'post_shutdown': colors_ts.append('#4444FF') elif evt == 'recovery': colors_ts.append('#FFA500') else: colors_ts.append('#44BB44') axes[0].bar(ts_analysis['timestep'], ts_analysis['illicit_rate'] * 100, color=colors_ts, edgecolor='white', linewidth=0.5) axes[0].set_ylabel('İllicit Oranı (%)', fontsize=12) axes[0].set_title('Bitcoin Ağında Zamana Göre İllicit Oranı ve Olay Haritası', fontsize=14, fontweight='bold') # Olay bölgeleri işaretle axes[0].axvline(x=43, color='black', linewidth=2, linestyle='--', alpha=0.8) axes[0].annotate('Dark Market\nKapanması', xy=(43, 35), fontsize=10, fontweight='bold', ha='center', color='black') legend_elements = [ mpatches.Patch(facecolor='#FF4444', label='Dark Market Zirvesi'), mpatches.Patch(facecolor='#000000', label='Kapanma (TS 43)'), mpatches.Patch(facecolor='#4444FF', label='Kapanma Sonrası Sessizlik'), mpatches.Patch(facecolor='#FFA500', label='Toparlanma Dönemi'), mpatches.Patch(facecolor='#44BB44', label='Normal Aktivite'), ] axes[0].legend(handles=legend_elements, loc='upper right', fontsize=9) # 1b: İşlem hacmi axes[1].bar(ts_analysis['timestep'], ts_analysis['n_total'], color='steelblue', alpha=0.7) axes[1].set_ylabel('İşlem Sayısı', fontsize=12) axes[1].set_title('Timestep Başına İşlem Hacmi', fontsize=12) axes[1].axvline(x=43, color='black', linewidth=2, linestyle='--', alpha=0.8) # 1c: İllicit sayısı axes[2].bar(ts_analysis['timestep'], ts_analysis['n_illicit'], color='red', alpha=0.7) axes[2].set_ylabel('İllicit Sayısı', fontsize=12) axes[2].set_xlabel('Timestep', fontsize=12) axes[2].set_title('Timestep Başına İllicit İşlem Sayısı', fontsize=12) axes[2].axvline(x=43, color='black', linewidth=2, linestyle='--', alpha=0.8) plt.tight_layout() plt.savefig('/app/figures_elliptic/fig1_bitcoin_event_map.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 1: Bitcoin Olay Haritası") # ── FİGÜR 2: Performans Isı Haritası ── fig, axes = plt.subplots(1, 3, figsize=(24, 6)) for idx, (metric, title) in enumerate([ ('AUROC', 'AUROC'), ('AUPRC', 'AUPRC'), ('Illicit F1', 'Illicit F1') ]): pivot = results_df.pivot_table(values=metric, index='Model', columns='Bölme Stratejisi') sns.heatmap(pivot, annot=True, fmt='.3f', cmap='RdYlGn', ax=axes[idx], linewidths=0.5, vmin=max(0, pivot.min().min() - 0.05)) axes[idx].set_title(title, fontsize=14, fontweight='bold') axes[idx].set_ylabel('') plt.suptitle('Olay-Farkında Veri Bölme: Bölme Stratejisinin Performans Üzerindeki Etkisi', fontsize=15, fontweight='bold', y=1.02) plt.tight_layout() plt.savefig('/app/figures_elliptic/fig2_performance_heatmap.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 2: Performans Isı Haritası") # ── FİGÜR 3: Bölme Stratejilerine Göre AUROC Karşılaştırması ── fig, ax = plt.subplots(figsize=(14, 7)) strategy_names = list(strategies.keys()) colors5 = sns.color_palette('Set2', 5) x = np.arange(len(model_names)) width = 0.15 for i, strategy in enumerate(strategy_names): aurocs = [results_df[(results_df['Model'] == m) & (results_df['Bölme Stratejisi'] == strategy)]['AUROC'].values[0] for m in model_names] ax.bar(x + i * width, aurocs, width, label=strategy, color=colors5[i]) ax.set_xlabel('Model', fontsize=13) ax.set_ylabel('AUROC', fontsize=13) ax.set_title('Bölme Stratejisine Göre AUROC Karşılaştırması', fontsize=14, fontweight='bold') ax.set_xticks(x + width * 2) ax.set_xticklabels(model_names, rotation=15) ax.legend(loc='lower left', fontsize=9) ax.set_ylim(bottom=0.3) ax.grid(axis='y', alpha=0.3) plt.tight_layout() plt.savefig('/app/figures_elliptic/fig3_auroc_comparison.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 3: AUROC Karşılaştırması") # ── FİGÜR 4: Timestep Bazında Performans (ÇÖKÜŞÜ GÖSTEREN ANA FİGÜR) ── fig, axes = plt.subplots(2, 2, figsize=(20, 14)) axes = axes.flatten() for idx, model_name in enumerate(model_names): ax = axes[idx] for split_name in ['Rastgele (Naive)', 'Kronolojik (Mevcut)', 'Olay-Farkında (Bizim)']: key = f"{split_name}|{model_name}" if key in timestep_results and len(timestep_results[key]) > 0: ts_data = timestep_results[key] ts_sorted = sorted(ts_data.keys()) aurocs = [ts_data[ts]['AUROC'] for ts in ts_sorted] ax.plot(ts_sorted, aurocs, 'o-', label=split_name, linewidth=2, markersize=5) ax.axvline(x=43, color='black', linewidth=2, linestyle='--', alpha=0.5) ax.annotate('Kapanma', xy=(43, 0.45), fontsize=9, ha='center') ax.axhline(y=0.5, color='red', linestyle=':', alpha=0.3) ax.set_ylabel('AUROC', fontsize=11) ax.set_xlabel('Timestep', fontsize=11) ax.set_title(f'{model_name}', fontsize=13, fontweight='bold') ax.legend(fontsize=9) ax.set_ylim(0.3, 1.05) ax.grid(alpha=0.3) # Olay bölgelerini renklendir for ts in ts_sorted if key in timestep_results else []: evt = classify_event(ts) if evt == 'dark_market_peak': ax.axvspan(ts-0.4, ts+0.4, alpha=0.1, color='red') elif evt == 'shutdown': ax.axvspan(ts-0.4, ts+0.4, alpha=0.2, color='black') elif evt == 'post_shutdown': ax.axvspan(ts-0.4, ts+0.4, alpha=0.1, color='blue') plt.suptitle('Timestep Bazında AUROC: Dark Market Kapanması Sonrası Çöküş', fontsize=15, fontweight='bold') plt.tight_layout() plt.savefig('/app/figures_elliptic/fig4_timestep_performance.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 4: Timestep Bazında Performans Çöküşü") # ── FİGÜR 5: Olay Tipine Göre Performans Karşılaştırması ── fig, axes = plt.subplots(1, 2, figsize=(18, 7)) # RF için olay bazlı karşılaştırma for ax_idx, (split_name, ax) in enumerate([ ('Rastgele (Naive)', axes[0]), ('Düşmanca-Olay', axes[1]) ]): key = f"{split_name}|Random Forest" if key in detailed_results: events_list = sorted(detailed_results[key].keys()) aurocs = [detailed_results[key][e]['AUROC'] for e in events_list] colors_evt = [] for e in events_list: if 'dark_market' in e: colors_evt.append('#FF4444') elif 'shutdown' in e: colors_evt.append('#000000') elif 'post_shutdown' in e: colors_evt.append('#4444FF') elif 'recovery' in e: colors_evt.append('#FFA500') else: colors_evt.append('#44BB44') bars = ax.barh(events_list, aurocs, color=colors_evt, edgecolor='white') ax.set_xlabel('AUROC', fontsize=12) ax.set_title(f'{split_name} (Random Forest)', fontsize=13, fontweight='bold') ax.axvline(x=0.5, color='red', linestyle='--', alpha=0.5) ax.set_xlim(0, 1.05) for bar, auroc in zip(bars, aurocs): ax.text(auroc + 0.01, bar.get_y() + bar.get_height()/2, f'{auroc:.3f}', va='center', fontsize=10) plt.suptitle('Olay Tipine Göre Performans: Rastgele vs Düşmanca Bölme (Random Forest)', fontsize=14, fontweight='bold', y=1.02) plt.tight_layout() plt.savefig('/app/figures_elliptic/fig5_event_performance.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 5: Olay Tipine Göre Performans") # ── FİGÜR 6: Degradasyon Isı Haritası ── fig, ax = plt.subplots(figsize=(12, 6)) deg_data = [] for model_name in model_names: random_auroc = results_df[(results_df['Model'] == model_name) & (results_df['Bölme Stratejisi'] == 'Rastgele (Naive)')]['AUROC'].values[0] for split_name in ['Kronolojik (Mevcut)', 'Olay-Farkında (Bizim)', 'Tabakalı-Kronolojik', 'Düşmanca-Olay']: other = results_df[(results_df['Model'] == model_name) & (results_df['Bölme Stratejisi'] == split_name)]['AUROC'].values[0] deg_data.append({ 'Model': model_name, 'Strateji': split_name, 'AUROC Düşüşü (%)': ((random_auroc - other) / random_auroc) * 100 }) deg_df = pd.DataFrame(deg_data) deg_pivot = deg_df.pivot_table(values='AUROC Düşüşü (%)', index='Model', columns='Strateji') sns.heatmap(deg_pivot, annot=True, fmt='.1f', cmap='Reds', ax=ax, center=0, linewidths=0.5) ax.set_title('Rastgele Bölmeye Göre AUROC Düşüşü (%)\n' 'Yüksek değer = Rastgele bölme o kadar fazla şişirilmiş', fontsize=13, fontweight='bold') plt.tight_layout() plt.savefig('/app/figures_elliptic/fig6_degradation_heatmap.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 6: Degradasyon Isı Haritası") # ── FİGÜR 7: ANA İÇGÖRÜ — Model Karmaşıklığı vs Bölme Stratejisi ── fig, ax = plt.subplots(figsize=(14, 8)) model_complexity = {'Logistic Regression': 1, 'Random Forest': 2, 'XGBoost': 3, 'LightGBM': 4} markers = {'Rastgele (Naive)': 'o', 'Kronolojik (Mevcut)': 's', 'Olay-Farkında (Bizim)': '^', 'Tabakalı-Kronolojik': 'D', 'Düşmanca-Olay': 'v'} linewidths = {'Olay-Farkında (Bizim)': 3.5, 'Düşmanca-Olay': 3.5} for i, split_name in enumerate(strategy_names): complexities = [model_complexity[m] for m in model_names] aurocs = [results_df[(results_df['Model'] == m) & (results_df['Bölme Stratejisi'] == split_name)]['AUROC'].values[0] for m in model_names] lw = linewidths.get(split_name, 2) ax.plot(complexities, aurocs, f'-{markers[split_name]}', label=split_name, linewidth=lw, markersize=10, color=colors5[i]) ax.set_xticks(list(model_complexity.values())) ax.set_xticklabels(list(model_complexity.keys()), rotation=15, fontsize=11) ax.set_xlabel('Model Karmaşıklığı →', fontsize=13) ax.set_ylabel('AUROC', fontsize=13) ax.set_title('ANA İÇGÖRÜ: Bölme Stratejisi Model Seçiminden Daha Önemli\n' 'Çizgiler arası fark (bölme etkisi) > Noktalar arası fark (model etkisi)', fontsize=13, fontweight='bold') ax.legend(fontsize=10) ax.grid(True, alpha=0.3) # Aradaki farkı gösteren oklar random_avg = np.mean([results_df[(results_df['Model'] == m) & (results_df['Bölme Stratejisi'] == 'Rastgele (Naive)')]['AUROC'].values[0] for m in model_names]) adv_avg = np.mean([results_df[(results_df['Model'] == m) & (results_df['Bölme Stratejisi'] == 'Düşmanca-Olay')]['AUROC'].values[0] for m in model_names]) ax.annotate('', xy=(4.5, adv_avg), xytext=(4.5, random_avg), arrowprops=dict(arrowstyle='<->', color='red', lw=2)) ax.text(4.6, (random_avg + adv_avg)/2, f'Bölme\netkisi', fontsize=10, color='red', fontweight='bold', va='center') plt.tight_layout() plt.savefig('/app/figures_elliptic/fig7_complexity_vs_splitting.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 7: Model Karmaşıklığı vs Bölme Stratejisi") # ── FİGÜR 8: Ağ Yapısı ve Olay İlişkisi ── fig, axes = plt.subplots(1, 2, figsize=(16, 6)) # 8a: Timestep başına ortalama derece vs illicit oranı ts_degree = df_labeled.groupby('timestep').agg( mean_degree=('total_degree', 'mean'), illicit_rate=('is_illicit', 'mean') ).reset_index() colors_scatter = [colors_ts[i] for i in range(len(ts_degree))] axes[0].scatter(ts_degree['mean_degree'], ts_degree['illicit_rate'] * 100, c=colors_scatter, s=100, edgecolors='black', linewidth=0.5) axes[0].set_xlabel('Ortalama Düğüm Derecesi', fontsize=12) axes[0].set_ylabel('İllicit Oranı (%)', fontsize=12) axes[0].set_title('Ağ Yoğunluğu vs İllicit Oranı', fontsize=13, fontweight='bold') # 8b: Event tipine göre ağ istatistikleri event_network = df_labeled.groupby('event_type').agg( mean_degree=('total_degree', 'mean'), mean_in=('in_degree', 'mean'), mean_out=('out_degree', 'mean') ).reset_index() x_pos = np.arange(len(event_network)) axes[1].bar(x_pos - 0.15, event_network['mean_in'], 0.3, label='In-Degree', color='steelblue') axes[1].bar(x_pos + 0.15, event_network['mean_out'], 0.3, label='Out-Degree', color='coral') axes[1].set_xticks(x_pos) axes[1].set_xticklabels(event_network['event_type'], rotation=30, ha='right') axes[1].set_ylabel('Ortalama Derece', fontsize=12) axes[1].set_title('Olay Tipine Göre Ağ Topolojisi', fontsize=13, fontweight='bold') axes[1].legend() plt.suptitle('Bitcoin Ağ Yapısı: Olaylar Ağ Topolojisini Değiştirir', fontsize=14, fontweight='bold') plt.tight_layout() plt.savefig('/app/figures_elliptic/fig8_network_topology.png', dpi=150, bbox_inches='tight') plt.close() print(" ✓ Figür 8: Ağ Topolojisi ve Olaylar") # ============================================================================ # ADIM 9: İSTATİSTİKSEL ÖZET # ============================================================================ print("\n" + "=" * 80) print("ADIM 9: İSTATİSTİKSEL ÖZET VE SONUÇLAR") print("=" * 80) print("\n── Ana Bulgular ──") # Bulgu 1: Ortalama performans düşüşü print("\n1. RASTGELE BÖLMEYE GÖRE ORTALAMA AUROC DÜŞÜŞÜ:") avg_drops = {} for split_name in ['Kronolojik (Mevcut)', 'Olay-Farkında (Bizim)', 'Düşmanca-Olay']: drops = [] for m in model_names: r = results_df[(results_df['Model'] == m) & (results_df['Bölme Stratejisi'] == 'Rastgele (Naive)')]['AUROC'].values[0] o = results_df[(results_df['Model'] == m) & (results_df['Bölme Stratejisi'] == split_name)]['AUROC'].values[0] drops.append(((r - o) / r) * 100) avg_drops[split_name] = np.mean(drops) print(f" {split_name:30s}: {np.mean(drops):+.2f}% (min={min(drops):+.2f}%, max={max(drops):+.2f}%)") # Bulgu 2: Etki büyüklüğü karşılaştırması print("\n2. ETKİ BÜYÜKLÜĞÜ KARŞILAŞTIRMASI:") for split_name in strategy_names: aurocs = results_df[results_df['Bölme Stratejisi'] == split_name]['AUROC'].values print(f" {split_name:30s}: aralık={aurocs.max()-aurocs.min():.4f} " f"(min={aurocs.min():.4f}, max={aurocs.max():.4f})") # Bölme stratejisi etkisi vs model etkisi split_effect = results_df.groupby('Model')['AUROC'].apply(lambda x: x.max() - x.min()).mean() model_effect = results_df.groupby('Bölme Stratejisi')['AUROC'].apply(lambda x: x.max() - x.min()).mean() print(f"\n Bölme stratejisi etkisi (model içi varyans): {split_effect:.4f}") print(f" Model seçimi etkisi (strateji içi varyans): {model_effect:.4f}") ratio = split_effect / model_effect if model_effect > 0 else float('inf') print(f" → Bölme stratejisi modelden {ratio:.1f}x DAHA ÖNEMLİ!") # Bulgu 3: Dark market kapanma etkisi print("\n3. DARK MARKET KAPANMASI ETKİSİ (TS 43):") for split_name in ['Kronolojik (Mevcut)', 'Olay-Farkında (Bizim)']: key = f"{split_name}|Random Forest" if key in timestep_results: ts_data = timestep_results[key] if 43 in ts_data: print(f" {split_name}: TS43 AUROC = {ts_data[43]['AUROC']:.4f} " f"(n={ts_data[43]['n_samples']}, illicit={ts_data[43]['n_illicit']})") # ============================================================================ # SONUÇLARI KAYDET # ============================================================================ print("\n" + "=" * 80) print("SONUÇLAR KAYDEDİLİYOR") print("=" * 80) with open('/app/results_elliptic/detailed_event_results.json', 'w') as f: json.dump(dict(detailed_results), f, indent=2) with open('/app/results_elliptic/timestep_results.json', 'w') as f: json.dump(dict(timestep_results), f, indent=2) summary = { 'experiment': 'Event-Aware Data Splitting for Bitcoin AML (Elliptic Dataset)', 'dataset': 'Elliptic Bitcoin Dataset', 'n_total_nodes': len(df), 'n_labeled_nodes': len(df_labeled), 'n_illicit': int(df_labeled['is_illicit'].sum()), 'n_licit': int((1-df_labeled['is_illicit']).sum()), 'illicit_rate': float(df_labeled['is_illicit'].mean()), 'n_timesteps': 49, 'n_edges': len(edges_df), 'n_features': len(feature_cols), 'n_strategies': len(strategies), 'n_models': len(get_models()), 'event_types': { 'dark_market_peak': [int(x) for x in sorted(DARK_MARKET_PEAK_TS)], 'shutdown': [int(x) for x in sorted(DARK_MARKET_SHUTDOWN_TS)], 'post_shutdown': [int(x) for x in sorted(POST_SHUTDOWN_TS)], 'recovery': [int(x) for x in sorted(RECOVERY_TS)], }, 'avg_auroc_drops': {k: float(v) for k, v in avg_drops.items()}, 'split_vs_model_effect_ratio': float(ratio), 'key_finding': f'Bölme stratejisi model seçiminden {ratio:.1f}x daha önemli', } with open('/app/results_elliptic/experiment_summary.json', 'w') as f: json.dump(summary, f, indent=2, ensure_ascii=False) results_df.to_csv('/app/results_elliptic/experiment_results.csv', index=False) print("\n✓ Tüm sonuçlar /app/results_elliptic/ klasörüne kaydedildi") print("✓ Tüm figürler /app/figures_elliptic/ klasörüne kaydedildi") print(f"\nFigürler:") for f in sorted(os.listdir('/app/figures_elliptic')): print(f" • {f}") print("\n" + "=" * 80) print("DENEY TAMAMLANDI!") print("=" * 80)