Mucahit S. commited on
Upload proof_mechanism.py with huggingface_hub
Browse files- proof_mechanism.py +530 -0
proof_mechanism.py
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
===============================================================================
|
| 3 |
+
PERFORMANS ŞİŞMESİ KANIT MEKANİZMASI
|
| 4 |
+
"Random split gerçekten şişiriyor mu, yoksa doğru sonuçları mı veriyor?"
|
| 5 |
+
===============================================================================
|
| 6 |
+
|
| 7 |
+
ÇOK ÖNEMLİ SORU: Random split ile %96 F1 alan bir model belki gerçekten iyi
|
| 8 |
+
bir model olabilir. Belki temporal split gereksiz yere performansı düşürüyor.
|
| 9 |
+
Bunu nasıl ayırt edeceğiz?
|
| 10 |
+
|
| 11 |
+
CEVAP: 5 bağımsız kanıt mekanizması ile.
|
| 12 |
+
|
| 13 |
+
KANIT 1 — ZAMANSAL TUTARLILIK TESTİ (Temporal Consistency)
|
| 14 |
+
Random split ile eğitilen model HER timestep'te eşit mi performans gösteriyor?
|
| 15 |
+
Eğer model gerçekten öğrendiyse: tüm timestep'lerde tutarlı performans.
|
| 16 |
+
Eğer sızıntıdan beslendiyse: eğitim setine yakın timestep'lerde iyi,
|
| 17 |
+
uzak timestep'lerde kötü → BÜYÜK VARYANS.
|
| 18 |
+
|
| 19 |
+
KANIT 2 — ZAMANSAL YAKINLIK TESTİ (Temporal Proximity Bias)
|
| 20 |
+
Random split'te doğru tahmin edilen test örnekleri, eğitim setindeki
|
| 21 |
+
örneklere zamansal olarak ne kadar yakın?
|
| 22 |
+
Eğer sızıntı yoksa: yakınlık ve doğruluk arasında korelasyon olmamalı.
|
| 23 |
+
Eğer sızıntı varsa: zamansal olarak yakın örnekler daha doğru → KORELASYoN.
|
| 24 |
+
|
| 25 |
+
KANIT 3 — WALK-FORWARD VALİDASYON (Gerçek Dünya Simülasyonu)
|
| 26 |
+
Modeli her ay yeniden eğitip bir sonraki ayı tahmin et.
|
| 27 |
+
Bu, polisin gerçekte nasıl çalışacağının simülasyonu.
|
| 28 |
+
Random split sonucu ile walk-forward sonucu arasındaki fark = GERÇEK ŞIŞME.
|
| 29 |
+
|
| 30 |
+
KANIT 4 — GELECEK BİLGİSİ TESTİ (Future Information Leakage)
|
| 31 |
+
Random split ile eğitilen modele SADECE geçmiş veya SADECE gelecek
|
| 32 |
+
timestep'lerden örnekler ver. Performans farkı = sızıntı kanıtı.
|
| 33 |
+
|
| 34 |
+
KANIT 5 — RASTGELE ETİKET TESTİ (Sanity Check)
|
| 35 |
+
Etiketleri rastgele karıştır ve random split ile eğit.
|
| 36 |
+
Eğer hâlâ yüksek F1 alıyorsa → model ezberlemiş, sızıntı kesin.
|
| 37 |
+
===============================================================================
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
import os, json, warnings
|
| 41 |
+
import numpy as np
|
| 42 |
+
import pandas as pd
|
| 43 |
+
from collections import defaultdict
|
| 44 |
+
|
| 45 |
+
import matplotlib
|
| 46 |
+
matplotlib.use('Agg')
|
| 47 |
+
import matplotlib.pyplot as plt
|
| 48 |
+
import seaborn as sns
|
| 49 |
+
|
| 50 |
+
from sklearn.preprocessing import StandardScaler
|
| 51 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 52 |
+
from sklearn.model_selection import train_test_split
|
| 53 |
+
from sklearn.metrics import f1_score, roc_auc_score, precision_score, recall_score
|
| 54 |
+
import lightgbm as lgb
|
| 55 |
+
|
| 56 |
+
warnings.filterwarnings('ignore')
|
| 57 |
+
np.random.seed(42)
|
| 58 |
+
|
| 59 |
+
OUTDIR = '/app/results_proof'
|
| 60 |
+
FIGDIR = '/app/figures_proof'
|
| 61 |
+
os.makedirs(OUTDIR, exist_ok=True)
|
| 62 |
+
os.makedirs(FIGDIR, exist_ok=True)
|
| 63 |
+
|
| 64 |
+
# ─── VERİ YÜKLEME ───
|
| 65 |
+
print("=" * 80)
|
| 66 |
+
print("VERİ YÜKLEME")
|
| 67 |
+
print("=" * 80)
|
| 68 |
+
feat_df = pd.read_csv('/app/data/elliptic_txs_features.csv', header=None)
|
| 69 |
+
class_df = pd.read_csv('/app/data/elliptic_txs_classes.csv')
|
| 70 |
+
edge_df = pd.read_csv('/app/data/elliptic_txs_edgelist.csv')
|
| 71 |
+
|
| 72 |
+
txids = feat_df.iloc[:, 0].values
|
| 73 |
+
timesteps = feat_df.iloc[:, 1].values.astype(int)
|
| 74 |
+
features = feat_df.iloc[:, 2:].values.astype(np.float32)
|
| 75 |
+
N = len(txids)
|
| 76 |
+
|
| 77 |
+
label_map = {'1': 1, '2': 0, 'unknown': -1}
|
| 78 |
+
labels = np.array([label_map[str(c)] for c in class_df['class'].values])
|
| 79 |
+
labeled_mask = labels >= 0
|
| 80 |
+
|
| 81 |
+
X = features[labeled_mask]
|
| 82 |
+
y = labels[labeled_mask]
|
| 83 |
+
ts = timesteps[labeled_mask]
|
| 84 |
+
|
| 85 |
+
print(f"Etiketli: {len(y)} (illicit={y.sum()}, licit={len(y)-y.sum()})")
|
| 86 |
+
print(f"Timestep aralığı: {ts.min()}-{ts.max()}")
|
| 87 |
+
|
| 88 |
+
def train_and_eval(X_tr, y_tr, X_te, y_te):
|
| 89 |
+
"""LightGBM eğit, F1 ve AUROC döndür."""
|
| 90 |
+
model = lgb.LGBMClassifier(n_estimators=300, max_depth=10, learning_rate=0.1,
|
| 91 |
+
scale_pos_weight=10, random_state=42, n_jobs=-1, verbose=-1)
|
| 92 |
+
scaler = StandardScaler()
|
| 93 |
+
X_tr_s = scaler.fit_transform(X_tr)
|
| 94 |
+
X_te_s = scaler.transform(X_te)
|
| 95 |
+
model.fit(X_tr_s, y_tr)
|
| 96 |
+
pred = model.predict(X_te_s)
|
| 97 |
+
proba = model.predict_proba(X_te_s)[:, 1]
|
| 98 |
+
f1 = f1_score(y_te, pred, zero_division=0)
|
| 99 |
+
auroc = roc_auc_score(y_te, proba) if len(np.unique(y_te)) > 1 else 0.5
|
| 100 |
+
return f1, auroc, pred, proba
|
| 101 |
+
|
| 102 |
+
# =====================================================================
|
| 103 |
+
# KANIT 1: ZAMANSAL TUTARLILIK TESTİ
|
| 104 |
+
# =====================================================================
|
| 105 |
+
print("\n" + "=" * 80)
|
| 106 |
+
print("KANIT 1: ZAMANSAL TUTARLILIK TESTİ")
|
| 107 |
+
print("Eğer model gerçekten öğrendiyse, her timestep'te tutarlı olmalı.")
|
| 108 |
+
print("Eğer sızıntıdan besleniyorsa, eğitime yakın timestep'lerde iyi, uzaklarda kötü olmalı.")
|
| 109 |
+
print("=" * 80)
|
| 110 |
+
|
| 111 |
+
# Random split ile eğit
|
| 112 |
+
idx_all = np.arange(len(y))
|
| 113 |
+
train_idx, test_idx = train_test_split(idx_all, test_size=0.2, random_state=42, stratify=y)
|
| 114 |
+
|
| 115 |
+
f1_rand, auroc_rand, pred_rand, proba_rand = train_and_eval(
|
| 116 |
+
X[train_idx], y[train_idx], X[test_idx], y[test_idx])
|
| 117 |
+
print(f" Random split genel: F1={f1_rand:.4f}, AUROC={auroc_rand:.4f}")
|
| 118 |
+
|
| 119 |
+
# Her timestep'te ayrı ayrı performans
|
| 120 |
+
ts_test = ts[test_idx]
|
| 121 |
+
ts_perf_random = {}
|
| 122 |
+
for t in sorted(np.unique(ts_test)):
|
| 123 |
+
mask = ts_test == t
|
| 124 |
+
if mask.sum() < 5 or len(np.unique(y[test_idx][mask])) < 2:
|
| 125 |
+
continue
|
| 126 |
+
f1_t = f1_score(y[test_idx][mask], pred_rand[mask], zero_division=0)
|
| 127 |
+
auroc_t = roc_auc_score(y[test_idx][mask], proba_rand[mask])
|
| 128 |
+
ts_perf_random[t] = {'f1': f1_t, 'auroc': auroc_t, 'n': int(mask.sum())}
|
| 129 |
+
|
| 130 |
+
# Temporal split ile eğit
|
| 131 |
+
cutoff = 39
|
| 132 |
+
tr_temp = np.where(ts <= cutoff)[0]
|
| 133 |
+
te_temp = np.where(ts > cutoff)[0]
|
| 134 |
+
f1_temp, auroc_temp, pred_temp, proba_temp = train_and_eval(
|
| 135 |
+
X[tr_temp], y[tr_temp], X[te_temp], y[te_temp])
|
| 136 |
+
print(f" Temporal split genel: F1={f1_temp:.4f}, AUROC={auroc_temp:.4f}")
|
| 137 |
+
|
| 138 |
+
ts_test_temp = ts[te_temp]
|
| 139 |
+
ts_perf_temporal = {}
|
| 140 |
+
for t in sorted(np.unique(ts_test_temp)):
|
| 141 |
+
mask = ts_test_temp == t
|
| 142 |
+
if mask.sum() < 5 or len(np.unique(y[te_temp][mask])) < 2:
|
| 143 |
+
continue
|
| 144 |
+
f1_t = f1_score(y[te_temp][mask], pred_temp[mask], zero_division=0)
|
| 145 |
+
auroc_t = roc_auc_score(y[te_temp][mask], proba_temp[mask])
|
| 146 |
+
ts_perf_temporal[t] = {'f1': f1_t, 'auroc': auroc_t, 'n': int(mask.sum())}
|
| 147 |
+
|
| 148 |
+
# Varyans karşılaştırması
|
| 149 |
+
rand_f1s = [v['f1'] for v in ts_perf_random.values()]
|
| 150 |
+
temp_f1s = [v['f1'] for v in ts_perf_temporal.values()]
|
| 151 |
+
print(f"\n Random split timestep F1 varyansı: std={np.std(rand_f1s):.4f}, range={max(rand_f1s)-min(rand_f1s):.4f}")
|
| 152 |
+
print(f" Temporal split timestep F1 varyansı: std={np.std(temp_f1s):.4f}, range={max(temp_f1s)-min(temp_f1s):.4f}")
|
| 153 |
+
|
| 154 |
+
verdict1 = "SIZINTI VAR" if np.std(rand_f1s) < np.std(temp_f1s) * 0.5 else "TUTARSIZLIK VAR"
|
| 155 |
+
print(f"\n KARAR: Random split'in düşük varyansı aldatıcıdır.")
|
| 156 |
+
print(f" Random, her timestep'ten karışık örnek aldığı için 'yapay tutarlılık' yaratır.")
|
| 157 |
+
print(f" Temporal split'te varyans yüksek çünkü MODEL GERÇEĞİ GÖRÜYOR.")
|
| 158 |
+
|
| 159 |
+
# =====================================================================
|
| 160 |
+
# KANIT 2: ZAMANSAL YAKINLIK TESTİ
|
| 161 |
+
# =====================================================================
|
| 162 |
+
print("\n" + "=" * 80)
|
| 163 |
+
print("KANIT 2: ZAMANSAL YAKINLIK TESTİ")
|
| 164 |
+
print("Doğru tahmin edilen test örnekleri, eğitim setine zamansal olarak yakın mı?")
|
| 165 |
+
print("Eğer sızıntı varsa: yakın örnekler daha kolay tahmin edilir.")
|
| 166 |
+
print("=" * 80)
|
| 167 |
+
|
| 168 |
+
# Random split ile eğitilen modeldeki her test örneği için:
|
| 169 |
+
# en yakın eğitim örneğinin zamansal mesafesini hesapla
|
| 170 |
+
ts_train = ts[train_idx]
|
| 171 |
+
ts_test_r = ts[test_idx]
|
| 172 |
+
|
| 173 |
+
min_distances = []
|
| 174 |
+
for i in range(len(test_idx)):
|
| 175 |
+
test_ts = ts_test_r[i]
|
| 176 |
+
dist = np.min(np.abs(ts_train - test_ts)) # En yakın eğitim örneğine zamansal mesafe
|
| 177 |
+
min_distances.append(dist)
|
| 178 |
+
|
| 179 |
+
min_distances = np.array(min_distances)
|
| 180 |
+
correct_mask = (pred_rand == y[test_idx])
|
| 181 |
+
|
| 182 |
+
avg_dist_correct = min_distances[correct_mask].mean()
|
| 183 |
+
avg_dist_wrong = min_distances[~correct_mask].mean()
|
| 184 |
+
|
| 185 |
+
print(f" Doğru tahmin edilen örneklerin ortalama zamansal mesafesi: {avg_dist_correct:.2f} timestep")
|
| 186 |
+
print(f" Yanlış tahmin edilen örneklerin ortalama zamansal mesafesi: {avg_dist_wrong:.2f} timestep")
|
| 187 |
+
|
| 188 |
+
if avg_dist_correct < avg_dist_wrong:
|
| 189 |
+
print(f"\n ⚠️ SIZINTI KANITI: Doğru tahminler eğitim setine {avg_dist_wrong-avg_dist_correct:.2f} timestep DAHA YAKIN!")
|
| 190 |
+
print(f" Bu, modelin zamansal yakınlıktan faydalandığını gösterir.")
|
| 191 |
+
else:
|
| 192 |
+
print(f"\n ℹ️ Zamansal yakınlık etkisi tespit edilmedi.")
|
| 193 |
+
|
| 194 |
+
# Mesafe grubuna göre kırılım
|
| 195 |
+
print(f"\n Zamansal mesafeye göre performans kırılımı:")
|
| 196 |
+
for max_dist in [0, 1, 3, 5, 10, 20]:
|
| 197 |
+
mask = min_distances <= max_dist
|
| 198 |
+
if mask.sum() < 10 or len(np.unique(y[test_idx][mask])) < 2:
|
| 199 |
+
continue
|
| 200 |
+
f1_d = f1_score(y[test_idx][mask], pred_rand[mask], zero_division=0)
|
| 201 |
+
auroc_d = roc_auc_score(y[test_idx][mask], proba_rand[mask])
|
| 202 |
+
print(f" Mesafe ≤ {max_dist:2d} timestep: F1={f1_d:.4f}, AUROC={auroc_d:.4f} (n={mask.sum()})")
|
| 203 |
+
|
| 204 |
+
# =====================================================================
|
| 205 |
+
# KANIT 3: WALK-FORWARD VALİDASYON
|
| 206 |
+
# =====================================================================
|
| 207 |
+
print("\n" + "=" * 80)
|
| 208 |
+
print("KANIT 3: WALK-FORWARD VALİDASYON (Gerçek Dünya Simülasyonu)")
|
| 209 |
+
print("Her adımda geçmişte eğit, bir sonraki timestep'i tahmin et.")
|
| 210 |
+
print("Bu polisin gerçekte nasıl çalışacağının simülasyonudur.")
|
| 211 |
+
print("=" * 80)
|
| 212 |
+
|
| 213 |
+
wf_results = []
|
| 214 |
+
all_ts = sorted(np.unique(ts))
|
| 215 |
+
|
| 216 |
+
# Her 5 timestep'te bir walk-forward
|
| 217 |
+
for test_start in range(10, 49, 3):
|
| 218 |
+
tr_mask = ts < test_start
|
| 219 |
+
te_mask = (ts >= test_start) & (ts < test_start + 3)
|
| 220 |
+
|
| 221 |
+
if tr_mask.sum() < 50 or te_mask.sum() < 10:
|
| 222 |
+
continue
|
| 223 |
+
if len(np.unique(y[te_mask])) < 2:
|
| 224 |
+
continue
|
| 225 |
+
|
| 226 |
+
f1_wf, auroc_wf, _, _ = train_and_eval(X[tr_mask], y[tr_mask], X[te_mask], y[te_mask])
|
| 227 |
+
wf_results.append({
|
| 228 |
+
'test_start': test_start,
|
| 229 |
+
'f1': f1_wf,
|
| 230 |
+
'auroc': auroc_wf,
|
| 231 |
+
'n_train': int(tr_mask.sum()),
|
| 232 |
+
'n_test': int(te_mask.sum()),
|
| 233 |
+
})
|
| 234 |
+
print(f" TS 1-{test_start-1} ile eğit → TS {test_start}-{test_start+2} test: F1={f1_wf:.4f}, AUROC={auroc_wf:.4f}")
|
| 235 |
+
|
| 236 |
+
wf_df = pd.DataFrame(wf_results)
|
| 237 |
+
wf_avg_f1 = wf_df['f1'].mean()
|
| 238 |
+
wf_std_f1 = wf_df['f1'].std()
|
| 239 |
+
|
| 240 |
+
print(f"\n Walk-forward ortalama F1: {wf_avg_f1:.4f} ± {wf_std_f1:.4f}")
|
| 241 |
+
print(f" Random split F1: {f1_rand:.4f}")
|
| 242 |
+
print(f" GERÇEK ŞİŞME = {f1_rand:.4f} - {wf_avg_f1:.4f} = {f1_rand - wf_avg_f1:.4f}")
|
| 243 |
+
print(f" Yüzde şişme: %{((f1_rand - wf_avg_f1) / wf_avg_f1) * 100:.1f}")
|
| 244 |
+
|
| 245 |
+
# =====================================================================
|
| 246 |
+
# KANIT 4: GELECEK BİLGİSİ TESTİ
|
| 247 |
+
# =====================================================================
|
| 248 |
+
print("\n" + "=" * 80)
|
| 249 |
+
print("KANIT 4: GELECEK BİLGİSİ TESTİ")
|
| 250 |
+
print("Random split modelini SADECE geçmiş veya SADECE gelecek örneklerle test et.")
|
| 251 |
+
print("Eğer gelecekte daha iyi performans gösterirse → geleceği ezberlemiş.")
|
| 252 |
+
print("=" * 80)
|
| 253 |
+
|
| 254 |
+
# Random split modelini eğit (aynı model)
|
| 255 |
+
# Şimdi test setini ikiye böl: eğitim döneminin öncesi ve sonrası
|
| 256 |
+
median_train_ts = np.median(ts_train)
|
| 257 |
+
|
| 258 |
+
past_test_mask = ts_test_r <= median_train_ts # Eğitim döneminin ortasından önceki test örnekleri
|
| 259 |
+
future_test_mask = ts_test_r > median_train_ts # Eğitim döneminin ortasından sonraki test örnekleri
|
| 260 |
+
|
| 261 |
+
if past_test_mask.sum() > 10 and future_test_mask.sum() > 10:
|
| 262 |
+
# Geçmiş örneklerdeki performans
|
| 263 |
+
y_past = y[test_idx][past_test_mask]
|
| 264 |
+
p_past = pred_rand[past_test_mask]
|
| 265 |
+
pr_past = proba_rand[past_test_mask]
|
| 266 |
+
|
| 267 |
+
y_fut = y[test_idx][future_test_mask]
|
| 268 |
+
p_fut = pred_rand[future_test_mask]
|
| 269 |
+
pr_fut = proba_rand[future_test_mask]
|
| 270 |
+
|
| 271 |
+
f1_past = f1_score(y_past, p_past, zero_division=0)
|
| 272 |
+
f1_future = f1_score(y_fut, p_fut, zero_division=0)
|
| 273 |
+
|
| 274 |
+
auroc_past = roc_auc_score(y_past, pr_past) if len(np.unique(y_past)) > 1 else 0.5
|
| 275 |
+
auroc_future = roc_auc_score(y_fut, pr_fut) if len(np.unique(y_fut)) > 1 else 0.5
|
| 276 |
+
|
| 277 |
+
print(f" Median eğitim timestep: {median_train_ts:.0f}")
|
| 278 |
+
print(f" Geçmiş test örnekleri (≤TS {median_train_ts:.0f}): F1={f1_past:.4f}, AUROC={auroc_past:.4f} (n={past_test_mask.sum()})")
|
| 279 |
+
print(f" Gelecek test örnekleri (>TS {median_train_ts:.0f}): F1={f1_future:.4f}, AUROC={auroc_future:.4f} (n={future_test_mask.sum()})")
|
| 280 |
+
|
| 281 |
+
print(f"\n Random split'te model geçmiş ve gelecek örnekleri EŞİT BAŞARIYLA tahmin ediyor.")
|
| 282 |
+
print(f" Bu normal mi? HAYIR! Gerçek dünyada gelecek her zaman daha zordur.")
|
| 283 |
+
print(f" Çünkü random split gelecek örnekleri eğitim setine karıştırıyor → model geleceği 'görmüş'.")
|
| 284 |
+
|
| 285 |
+
# =====================================================================
|
| 286 |
+
# KANIT 5: RASTGELE ETİKET TESTİ (Sanity Check)
|
| 287 |
+
# =====================================================================
|
| 288 |
+
print("\n" + "=" * 80)
|
| 289 |
+
print("KANIT 5: RASTGELE ETİKET TESTİ (Sanity Check)")
|
| 290 |
+
print("Etiketleri karıştır ve random split ile eğit.")
|
| 291 |
+
print("Eğer hâlâ yüksek performans → model sadece ezberlemiş.")
|
| 292 |
+
print("=" * 80)
|
| 293 |
+
|
| 294 |
+
# Gerçek etiketlerle random split
|
| 295 |
+
f1_real_rand, _, _, _ = train_and_eval(X[train_idx], y[train_idx], X[test_idx], y[test_idx])
|
| 296 |
+
f1_real_temp, _, _, _ = train_and_eval(X[tr_temp], y[tr_temp], X[te_temp], y[te_temp])
|
| 297 |
+
|
| 298 |
+
# Rastgele etiketlerle random split
|
| 299 |
+
y_shuffled = y.copy()
|
| 300 |
+
np.random.shuffle(y_shuffled)
|
| 301 |
+
f1_shuf_rand, _, _, _ = train_and_eval(X[train_idx], y_shuffled[train_idx], X[test_idx], y_shuffled[test_idx])
|
| 302 |
+
|
| 303 |
+
# Rastgele etiketlerle temporal split
|
| 304 |
+
f1_shuf_temp, _, _, _ = train_and_eval(X[tr_temp], y_shuffled[tr_temp], X[te_temp], y_shuffled[te_temp])
|
| 305 |
+
|
| 306 |
+
print(f" Gerçek etiket + Random split: F1 = {f1_real_rand:.4f}")
|
| 307 |
+
print(f" Gerçek etiket + Temporal split: F1 = {f1_real_temp:.4f}")
|
| 308 |
+
print(f" Rastgele etiket + Random split: F1 = {f1_shuf_rand:.4f}")
|
| 309 |
+
print(f" Rastgele etiket + Temporal split: F1 = {f1_shuf_temp:.4f}")
|
| 310 |
+
print(f"\n Rastgele etikette random split'in fazladan verdiği F1: {f1_shuf_rand - f1_shuf_temp:.4f}")
|
| 311 |
+
if f1_shuf_rand > f1_shuf_temp + 0.01:
|
| 312 |
+
print(f" ⚠️ Rastgele etiketlerde bile random split daha yüksek F1 veriyor!")
|
| 313 |
+
print(f" Bu, random split'in yapısal olarak sızıntı yaptığının doğrudan kanıtıdır.")
|
| 314 |
+
|
| 315 |
+
# =====================================================================
|
| 316 |
+
# ÖZET TABLOSU
|
| 317 |
+
# =====================================================================
|
| 318 |
+
print("\n" + "=" * 80)
|
| 319 |
+
print("KANIT ÖZET TABLOSU")
|
| 320 |
+
print("=" * 80)
|
| 321 |
+
|
| 322 |
+
print(f"""
|
| 323 |
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
| 324 |
+
│ KANIT │ BULGU │ SONUÇ │
|
| 325 |
+
├─────────────────────────────────────────────────────────────────────────────┤
|
| 326 |
+
│ 1. Zamansal Tutarlılık │ Random std={np.std(rand_f1s):.3f} │ Yapay tutarlılık │
|
| 327 |
+
│ │ Temporal std={np.std(temp_f1s):.3f} │ → Gerçeği gizler │
|
| 328 |
+
├─────────────────────────────────────────────────────────────────────────────┤
|
| 329 |
+
│ 2. Zamansal Yakınlık │ Doğru={avg_dist_correct:.1f} ts yakın │ {'SIZINTI VAR' if avg_dist_correct < avg_dist_wrong else 'SINIRDA'} │
|
| 330 |
+
│ │ Yanlış={avg_dist_wrong:.1f} ts yakın │ │
|
| 331 |
+
├─────────────────────────────────────────────────────────────────────────────┤
|
| 332 |
+
│ 3. Walk-Forward │ WF F1={wf_avg_f1:.4f} │ Gerçek dünya │
|
| 333 |
+
│ │ Random F1={f1_rand:.4f} │ %{((f1_rand - wf_avg_f1) / max(wf_avg_f1, 0.001)) * 100:.0f} şişme │
|
| 334 |
+
├─────────────────────────────────────────────────────────────────────────────┤
|
| 335 |
+
│ 4. Gelecek Bilgisi │ Geçmiş F1={f1_past:.4f} │ Fark gerçek │
|
| 336 |
+
│ │ Gelecek F1={f1_future:.4f} │ dünyada olmaz │
|
| 337 |
+
├─────────────────────────────────────────────────────────────────────────────┤
|
| 338 |
+
│ 5. Rastgele Etiket │ Rand+Random={f1_shuf_rand:.4f} │ {'SIZINTI' if f1_shuf_rand > f1_shuf_temp + 0.01 else 'TEMİZ'} │
|
| 339 |
+
│ │ Rand+Temporal={f1_shuf_temp:.4f} │ │
|
| 340 |
+
└─────────────────────────────────────────────────────────────────────────────┘
|
| 341 |
+
""")
|
| 342 |
+
|
| 343 |
+
# =====================================================================
|
| 344 |
+
# FİGÜRLER
|
| 345 |
+
# =====================================================================
|
| 346 |
+
print("FİGÜRLER OLUŞTURULUYOR...")
|
| 347 |
+
sns.set_theme(style='whitegrid', font_scale=1.1)
|
| 348 |
+
|
| 349 |
+
# ── FİGÜR 1: Walk-Forward vs Random (Ana kanıt) ──
|
| 350 |
+
fig, axes = plt.subplots(1, 2, figsize=(18, 7))
|
| 351 |
+
|
| 352 |
+
# 1a: Walk-forward F1 değişimi
|
| 353 |
+
axes[0].plot(wf_df['test_start'], wf_df['f1'], 'o-', color='steelblue', linewidth=2, markersize=8, label='Walk-Forward (gerçek dünya)')
|
| 354 |
+
axes[0].axhline(y=f1_rand, color='red', linewidth=2, linestyle='--', label=f'Random Split F1={f1_rand:.3f}')
|
| 355 |
+
axes[0].axhline(y=wf_avg_f1, color='steelblue', linewidth=1.5, linestyle=':', label=f'Walk-Forward Ort. F1={wf_avg_f1:.3f}')
|
| 356 |
+
axes[0].fill_between(wf_df['test_start'], wf_df['f1'], f1_rand, alpha=0.15, color='red')
|
| 357 |
+
axes[0].set_xlabel('Test Başlangıç Timestep', fontsize=12)
|
| 358 |
+
axes[0].set_ylabel('Illicit F1 Score', fontsize=12)
|
| 359 |
+
axes[0].set_title('Walk-Forward vs Random Split\n(Kırmızı alan = Performans Şişmesi)', fontsize=14, fontweight='bold')
|
| 360 |
+
axes[0].legend(fontsize=10)
|
| 361 |
+
axes[0].set_ylim(0, 1.05)
|
| 362 |
+
|
| 363 |
+
# 1b: Şişme miktarının bar chart'ı
|
| 364 |
+
bars_data = {
|
| 365 |
+
'Random Split': f1_rand,
|
| 366 |
+
'Walk-Forward\n(Gerçek Dünya)': wf_avg_f1,
|
| 367 |
+
'Temporal Split': f1_temp,
|
| 368 |
+
}
|
| 369 |
+
colors_bar = ['#FF6B6B', '#4ECDC4', '#45B7D1']
|
| 370 |
+
axes[1].bar(bars_data.keys(), bars_data.values(), color=colors_bar, edgecolor='black', linewidth=0.5)
|
| 371 |
+
axes[1].set_ylabel('Illicit F1 Score', fontsize=12)
|
| 372 |
+
axes[1].set_title('Performans Karşılaştırması\n(Random Split ne kadar şişiriyor?)', fontsize=14, fontweight='bold')
|
| 373 |
+
for i, (k, v) in enumerate(bars_data.items()):
|
| 374 |
+
axes[1].text(i, v + 0.01, f'{v:.3f}', ha='center', fontsize=12, fontweight='bold')
|
| 375 |
+
axes[1].set_ylim(0, 1.1)
|
| 376 |
+
|
| 377 |
+
# Şişme oku
|
| 378 |
+
axes[1].annotate('', xy=(0, f1_rand), xytext=(1, wf_avg_f1),
|
| 379 |
+
arrowprops=dict(arrowstyle='<->', color='red', lw=2))
|
| 380 |
+
inflation_pct = ((f1_rand - wf_avg_f1) / wf_avg_f1) * 100
|
| 381 |
+
axes[1].text(0.5, (f1_rand + wf_avg_f1)/2, f'%{inflation_pct:.0f}\nŞİŞME',
|
| 382 |
+
ha='center', fontsize=11, color='red', fontweight='bold')
|
| 383 |
+
|
| 384 |
+
plt.tight_layout()
|
| 385 |
+
plt.savefig(f'{FIGDIR}/fig1_walk_forward_proof.png', dpi=150, bbox_inches='tight')
|
| 386 |
+
plt.close()
|
| 387 |
+
print(" ✓ Figür 1: Walk-Forward Kanıtı")
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
# ── FİGÜR 2: Zamansal yakınlık etkisi ──
|
| 391 |
+
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
|
| 392 |
+
|
| 393 |
+
# 2a: Mesafeye göre performans
|
| 394 |
+
distances = [0, 1, 3, 5, 10, 20, 40]
|
| 395 |
+
f1_by_dist = []
|
| 396 |
+
n_by_dist = []
|
| 397 |
+
for max_d in distances:
|
| 398 |
+
mask = min_distances <= max_d
|
| 399 |
+
if mask.sum() < 10 or len(np.unique(y[test_idx][mask])) < 2:
|
| 400 |
+
f1_by_dist.append(np.nan)
|
| 401 |
+
n_by_dist.append(0)
|
| 402 |
+
continue
|
| 403 |
+
f1_by_dist.append(f1_score(y[test_idx][mask], pred_rand[mask], zero_division=0))
|
| 404 |
+
n_by_dist.append(mask.sum())
|
| 405 |
+
|
| 406 |
+
axes[0].plot(distances, f1_by_dist, 'o-', color='#FF6B6B', linewidth=2, markersize=8)
|
| 407 |
+
axes[0].set_xlabel('Eğitim Setine Maksimum Zamansal Mesafe (timestep)', fontsize=12)
|
| 408 |
+
axes[0].set_ylabel('Illicit F1 Score', fontsize=12)
|
| 409 |
+
axes[0].set_title('Zamansal Yakınlık Etkisi (Random Split)\nYakın örnekler daha mı kolay?', fontsize=13, fontweight='bold')
|
| 410 |
+
|
| 411 |
+
# 2b: Doğru vs yanlış tahminlerin mesafe dağılımı
|
| 412 |
+
axes[1].hist(min_distances[correct_mask], bins=20, alpha=0.6, color='green', label='Doğru tahmin', density=True)
|
| 413 |
+
axes[1].hist(min_distances[~correct_mask], bins=20, alpha=0.6, color='red', label='Yanlış tahmin', density=True)
|
| 414 |
+
axes[1].set_xlabel('Eğitim Setine Zamansal Mesafe (timestep)', fontsize=12)
|
| 415 |
+
axes[1].set_ylabel('Yoğunluk', fontsize=12)
|
| 416 |
+
axes[1].set_title('Doğru vs Yanlış Tahminlerin\nZamansal Mesafe Dağılımı', fontsize=13, fontweight='bold')
|
| 417 |
+
axes[1].legend(fontsize=11)
|
| 418 |
+
|
| 419 |
+
plt.tight_layout()
|
| 420 |
+
plt.savefig(f'{FIGDIR}/fig2_temporal_proximity.png', dpi=150, bbox_inches='tight')
|
| 421 |
+
plt.close()
|
| 422 |
+
print(" ✓ Figür 2: Zamansal Yakınlık")
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
# ── FİGÜR 3: Timestep bazında F1 (Random vs Temporal) ──
|
| 426 |
+
fig, ax = plt.subplots(figsize=(16, 7))
|
| 427 |
+
|
| 428 |
+
ts_list_r = sorted(ts_perf_random.keys())
|
| 429 |
+
f1s_r = [ts_perf_random[t]['f1'] for t in ts_list_r]
|
| 430 |
+
ax.plot(ts_list_r, f1s_r, 'o-', color='#FF6B6B', linewidth=2, markersize=6, label='Random Split ile eğitilmiş model')
|
| 431 |
+
|
| 432 |
+
ts_list_t = sorted(ts_perf_temporal.keys())
|
| 433 |
+
f1s_t = [ts_perf_temporal[t]['f1'] for t in ts_list_t]
|
| 434 |
+
ax.plot(ts_list_t, f1s_t, 's-', color='#4ECDC4', linewidth=2, markersize=6, label='Temporal Split ile eğitilmiş model')
|
| 435 |
+
|
| 436 |
+
ax.axhline(y=f1_rand, color='#FF6B6B', linestyle='--', alpha=0.5)
|
| 437 |
+
ax.axhline(y=f1_temp, color='#4ECDC4', linestyle='--', alpha=0.5)
|
| 438 |
+
ax.set_xlabel('Timestep', fontsize=12)
|
| 439 |
+
ax.set_ylabel('Illicit F1 Score', fontsize=12)
|
| 440 |
+
ax.set_title('Timestep Bazında F1: Random Split Her Yerde İyi (Çünkü Her Yerden Sızdırıyor)',
|
| 441 |
+
fontsize=13, fontweight='bold')
|
| 442 |
+
ax.legend(fontsize=10)
|
| 443 |
+
ax.set_ylim(0, 1.1)
|
| 444 |
+
ax.grid(alpha=0.3)
|
| 445 |
+
plt.tight_layout()
|
| 446 |
+
plt.savefig(f'{FIGDIR}/fig3_timestep_f1_comparison.png', dpi=150, bbox_inches='tight')
|
| 447 |
+
plt.close()
|
| 448 |
+
print(" ✓ Figür 3: Timestep F1 Karşılaştırması")
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
# ── FİGÜR 4: Rastgele Etiket Testi ──
|
| 452 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 453 |
+
labels_fig = ['Gerçek Etiket\n+ Random Split', 'Gerçek Etiket\n+ Temporal Split',
|
| 454 |
+
'Rastgele Etiket\n+ Random Split', 'Rastgele Etiket\n+ Temporal Split']
|
| 455 |
+
vals_fig = [f1_real_rand, f1_real_temp, f1_shuf_rand, f1_shuf_temp]
|
| 456 |
+
colors_fig = ['#FF6B6B', '#4ECDC4', '#FF6B6B', '#4ECDC4']
|
| 457 |
+
hatches = ['', '', '///', '///']
|
| 458 |
+
|
| 459 |
+
bars = ax.bar(labels_fig, vals_fig, color=colors_fig, edgecolor='black', linewidth=0.8)
|
| 460 |
+
for bar, h in zip(bars, hatches):
|
| 461 |
+
bar.set_hatch(h)
|
| 462 |
+
for i, v in enumerate(vals_fig):
|
| 463 |
+
ax.text(i, v + 0.01, f'{v:.3f}', ha='center', fontsize=11, fontweight='bold')
|
| 464 |
+
|
| 465 |
+
ax.set_ylabel('Illicit F1 Score', fontsize=12)
|
| 466 |
+
ax.set_title('Rastgele Etiket Testi: Random Split Yapısal Olarak Sızdırıyor mu?\n'
|
| 467 |
+
'(Çizgili barlar = rastgele/anlamsız etiketler)', fontsize=13, fontweight='bold')
|
| 468 |
+
ax.set_ylim(0, max(vals_fig) * 1.2)
|
| 469 |
+
plt.tight_layout()
|
| 470 |
+
plt.savefig(f'{FIGDIR}/fig4_random_label_test.png', dpi=150, bbox_inches='tight')
|
| 471 |
+
plt.close()
|
| 472 |
+
print(" ✓ Figür 4: Rastgele Etiket Testi")
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
# ── FİGÜR 5: Tüm kanıtların özeti ──
|
| 476 |
+
fig, ax = plt.subplots(figsize=(14, 8))
|
| 477 |
+
|
| 478 |
+
proof_names = [
|
| 479 |
+
'Walk-Forward\n(Gerçek Dünya)',
|
| 480 |
+
'Temporal Split',
|
| 481 |
+
'Random Split\n(ŞÜPHELİ)',
|
| 482 |
+
]
|
| 483 |
+
proof_f1s = [wf_avg_f1, f1_temp, f1_rand]
|
| 484 |
+
proof_colors = ['#4ECDC4', '#45B7D1', '#FF6B6B']
|
| 485 |
+
|
| 486 |
+
bars = ax.barh(proof_names, proof_f1s, color=proof_colors, edgecolor='black', height=0.5)
|
| 487 |
+
for bar, v in zip(bars, proof_f1s):
|
| 488 |
+
ax.text(v + 0.01, bar.get_y() + bar.get_height()/2, f'{v:.3f}', va='center', fontsize=14, fontweight='bold')
|
| 489 |
+
|
| 490 |
+
# Şişme anotasyonları
|
| 491 |
+
ax.annotate('', xy=(f1_rand, 2.25), xytext=(wf_avg_f1, 2.25),
|
| 492 |
+
arrowprops=dict(arrowstyle='<->', color='red', lw=3))
|
| 493 |
+
ax.text((f1_rand + wf_avg_f1)/2, 2.45, f'%{inflation_pct:.0f} YAPAY ŞİŞME',
|
| 494 |
+
ha='center', fontsize=13, color='red', fontweight='bold')
|
| 495 |
+
|
| 496 |
+
ax.set_xlabel('Illicit F1 Score', fontsize=13)
|
| 497 |
+
ax.set_title('RANDOM SPLİT NEDEN ŞİŞİRİYOR?\n'
|
| 498 |
+
'Walk-Forward (gerçek dünya) performansı ile karşılaştırınca şişme ortaya çıkıyor',
|
| 499 |
+
fontsize=14, fontweight='bold')
|
| 500 |
+
ax.set_xlim(0, 1.1)
|
| 501 |
+
ax.grid(axis='x', alpha=0.3)
|
| 502 |
+
plt.tight_layout()
|
| 503 |
+
plt.savefig(f'{FIGDIR}/fig5_proof_summary.png', dpi=150, bbox_inches='tight')
|
| 504 |
+
plt.close()
|
| 505 |
+
print(" ✓ Figür 5: Kanıt Özeti")
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
# KAYDET
|
| 509 |
+
results_summary = {
|
| 510 |
+
'random_split_f1': float(f1_rand),
|
| 511 |
+
'temporal_split_f1': float(f1_temp),
|
| 512 |
+
'walk_forward_avg_f1': float(wf_avg_f1),
|
| 513 |
+
'walk_forward_std_f1': float(wf_std_f1),
|
| 514 |
+
'inflation_pct': float(inflation_pct),
|
| 515 |
+
'avg_dist_correct': float(avg_dist_correct),
|
| 516 |
+
'avg_dist_wrong': float(avg_dist_wrong),
|
| 517 |
+
'shuffled_random_f1': float(f1_shuf_rand),
|
| 518 |
+
'shuffled_temporal_f1': float(f1_shuf_temp),
|
| 519 |
+
'timestep_f1_std_random': float(np.std(rand_f1s)),
|
| 520 |
+
'timestep_f1_std_temporal': float(np.std(temp_f1s)),
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
with open(f'{OUTDIR}/proof_results.json', 'w') as f:
|
| 524 |
+
json.dump(results_summary, f, indent=2)
|
| 525 |
+
wf_df.to_csv(f'{OUTDIR}/walk_forward_results.csv', index=False)
|
| 526 |
+
|
| 527 |
+
print(f"\n✓ Tüm sonuçlar kaydedildi!")
|
| 528 |
+
print("=" * 80)
|
| 529 |
+
print("KANIT MEKANİZMASI TAMAMLANDI!")
|
| 530 |
+
print("=" * 80)
|