Files
2026-07-13 13:26:28 +08:00

868 lines
28 KiB
Python

"""
TimeGAN Evaluation Metrics
Implements the evaluation protocol from:
Yoon, J., Jarrett, D., & van der Schaar, M. (2019).
"Time-series Generative Adversarial Networks." NeurIPS 2019.
Also used for GT-GAN evaluation (same metrics apply to GRU-ODE generators).
Paper Section 5.2 specifies:
1. Predictive Score: Train post-hoc RNN to predict next step
- Train on synthetic, test on real (TSTR with MAE)
- Lower MAE = better utility
2. Discriminative Score: Train post-hoc RNN classifier
- Binary: distinguish real vs synthetic
- Lower accuracy = better fidelity (harder to tell apart)
Two evaluation modes:
1. **Yoon Mode** (compute_predictive_score_yoon):
- Matches official TimeGAN repo
- Predicts ONLY the last feature using other features as input
- GRU with sigmoid output (expects [0,1] normalized data)
- Reports raw MAE
2. **Comprehensive Mode** (compute_predictive_score):
- Predicts ALL features
- Compares TSTR vs TRTR baseline
- Reports MAE ratio
Reference: [ref:NCTZ79FF] TimeGAN
"""
from __future__ import annotations
from typing import Any
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.metrics import accuracy_score, roc_auc_score
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
class PostHocLSTMPredictor(nn.Module):
"""
2-layer LSTM for next-step prediction per TimeGAN paper.
Architecture (Section 5.2):
- Input: sequences of shape (batch, seq_len, n_features)
- 2 LSTM layers with hidden_dim units
- Dense output layer to n_features
- Predicts next timestep given previous timesteps
Used for computing predictive score (TSTR with MAE).
"""
def __init__(self, input_dim: int, hidden_dim: int = 128, num_layers: int = 2):
super().__init__()
self.lstm = nn.LSTM(
input_dim,
hidden_dim,
num_layers=num_layers,
batch_first=True,
bidirectional=False,
)
self.fc = nn.Linear(hidden_dim, input_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Forward pass.
Args:
x: Input sequences (batch, seq_len, n_features)
Returns:
Predictions for next timestep at each position (batch, seq_len, n_features)
"""
h, _ = self.lstm(x)
return self.fc(h)
class PostHocLSTMDiscriminator(nn.Module):
"""
2-layer LSTM classifier for discriminative score per TimeGAN paper.
Architecture (Section 5.2):
- Input: sequences of shape (batch, seq_len, n_features)
- 2 LSTM layers with hidden_dim units
- Dense output layer to 1 (sigmoid for binary classification)
- Classifies sequences as real (1) or synthetic (0)
Used for computing discriminative score (lower accuracy = better fidelity).
"""
def __init__(self, input_dim: int, hidden_dim: int = 128, num_layers: int = 2):
super().__init__()
self.lstm = nn.LSTM(
input_dim,
hidden_dim,
num_layers=num_layers,
batch_first=True,
bidirectional=False,
)
self.fc = nn.Linear(hidden_dim, 1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Forward pass.
Args:
x: Input sequences (batch, seq_len, n_features)
Returns:
Logits for real/fake classification (batch, 1)
"""
h, _ = self.lstm(x)
# Use last hidden state for classification
return self.fc(h[:, -1, :])
class GRUPredictor(nn.Module):
"""
GRU predictor matching official TimeGAN repo.
Architecture:
- GRU (not LSTM)
- hidden_dim = dim // 2
- Sigmoid output (expects [0, 1] normalized data)
- Predicts last feature using other features
"""
def __init__(self, input_dim: int, hidden_dim: int | None = None):
super().__init__()
# Paper: hidden_dim = int(dim/2) where dim is total features
if hidden_dim is None:
hidden_dim = max(input_dim // 2, 4)
self.gru = nn.GRU(
input_dim,
hidden_dim,
num_layers=1, # Paper uses single layer
batch_first=True,
bidirectional=False,
)
self.fc = nn.Linear(hidden_dim, 1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Forward pass.
Args:
x: Input sequences (batch, seq_len, n_features-1)
Note: Last feature already excluded by caller
Returns:
Predictions for last feature (batch, seq_len, 1) with sigmoid
"""
h, _ = self.gru(x)
return torch.sigmoid(self.fc(h))
class GRUDiscriminator(nn.Module):
"""
GRU discriminator for TimeGAN evaluation.
Architecture: GRU with hidden_dim = input_dim // 2, single layer.
Reports |accuracy - 0.5| where 0.0 = indistinguishable, 0.5 = trivially separable.
"""
def __init__(self, input_dim: int, hidden_dim: int | None = None):
super().__init__()
if hidden_dim is None:
hidden_dim = max(input_dim // 2, 4)
self.hidden_dim = hidden_dim
self.gru = nn.GRU(input_dim, hidden_dim, num_layers=1, batch_first=True)
self.fc = nn.Linear(hidden_dim, 1)
def forward(self, x: torch.Tensor) -> torch.Tensor:
h, _ = self.gru(x)
return self.fc(h[:, -1, :])
def compute_predictive_score_yoon(
synthetic: np.ndarray,
real: np.ndarray,
iterations: int = 5000,
batch_size: int = 128,
device: str | None = None,
quick_test: bool = False,
verbose: bool = True,
) -> dict[str, Any]:
"""
Compute predictive score per Yoon et al. (2019).
Replicates metrics/predictive_metrics.py from official repo.
Methodology:
- Predicts ONLY the last feature using other features
- GRU with sigmoid output (expects [0,1] normalized data)
- Reports raw MAE (no baseline comparison)
Args:
synthetic: Synthetic sequences (n_samples, seq_len, n_features)
real: Real sequences for evaluation (same shape)
iterations: Training iterations (default: 5000)
batch_size: Batch size (default: 128)
device: 'cuda' or 'cpu' (auto-detected if None)
quick_test: If True, reduce iterations for testing
verbose: Print progress
Returns:
dict with mae, feature_idx, model
"""
if quick_test:
iterations = min(100, iterations)
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
device = torch.device(device)
n_features = synthetic.shape[-1]
# Check if data appears to be normalized to [0, 1]
data_min, data_max = synthetic.min(), synthetic.max()
if data_min < -0.1 or data_max > 1.1:
if verbose:
print(f"\nWARNING: Data range [{data_min:.2f}, {data_max:.2f}]")
print(" Yoon metric expects [0, 1] normalized data.")
print(" Results may not be comparable to paper.\n")
if verbose:
print("Predictive Score (Yoon et al.)")
print(f" Features: {n_features - 1} input → 1 output (last feature)")
print(f" Iterations: {iterations}, Batch size: {batch_size}")
hidden_dim = max(n_features // 2, 4)
model = GRUPredictor(n_features - 1, hidden_dim).to(device)
optimizer = optim.Adam(model.parameters())
criterion = nn.L1Loss() # MAE loss
# Prepare training data from synthetic
# X: all features except last, for all timesteps except last
# Y: last feature only, shifted by 1 (next-step prediction)
def prepare_paper_data(data: np.ndarray) -> tuple[torch.Tensor, torch.Tensor]:
"""Prepare X and Y per paper's methodology."""
# X: [:-1, :(dim-1)] - all but last timestep, all but last feature
X = torch.tensor(data[:, :-1, :-1], dtype=torch.float32)
# Y: [1:, (dim-1)] - shifted by 1, only last feature
Y = torch.tensor(data[:, 1:, -1:], dtype=torch.float32)
return X, Y
X_train, Y_train = prepare_paper_data(synthetic)
n_train = len(X_train)
# Training loop (iteration-based, not epoch-based)
model.train()
iterator = tqdm(range(iterations), desc="Yoon Predictive", disable=not verbose)
for _ in iterator:
# Random mini-batch (paper's approach)
idx = np.random.permutation(n_train)[:batch_size]
X_batch = X_train[idx].to(device)
Y_batch = Y_train[idx].to(device)
optimizer.zero_grad()
Y_pred = model(X_batch)
loss = criterion(Y_pred, Y_batch)
loss.backward()
optimizer.step()
if verbose:
iterator.set_postfix(loss=loss.item())
# Evaluate on real data
model.eval()
X_real, Y_real = prepare_paper_data(real)
X_real = X_real.to(device)
Y_real = Y_real.to(device)
with torch.no_grad():
Y_pred = model(X_real)
mae = torch.abs(Y_pred - Y_real).mean().item()
if verbose:
print("\nPredictive Score (Yoon et al.):")
print(f" MAE on real data: {mae:.6f}")
print(" (Paper reports ~0.04 for GOOG stock)")
return {
"mae": mae,
"feature_idx": n_features - 1,
"model": model,
}
def compute_discriminative_score_yoon(
real: np.ndarray,
synthetic: np.ndarray,
iterations: int = 2000,
batch_size: int = 128,
device: str | None = None,
quick_test: bool = False,
verbose: bool = True,
) -> dict[str, Any]:
"""
Compute discriminative score per Yoon et al. (2019).
Metric: |accuracy - 0.5| where 0.0 = indistinguishable, 0.5 = trivially separable.
Data should be MinMax normalized to [0, 1].
Args:
real: Real sequences (n_samples, seq_len, n_features)
synthetic: Synthetic sequences (same shape)
iterations: Training iterations (default: 2000)
batch_size: Batch size (default: 128)
device: 'cuda' or 'cpu' (auto-detected if None)
quick_test: If True, reduce iterations for testing
verbose: Print progress
Returns:
dict with score, accuracy, model
"""
if quick_test:
iterations = min(100, iterations)
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
device = torch.device(device)
n_features = real.shape[-1]
# Warn if data not normalized
data_min, data_max = real.min(), real.max()
if (data_min < -0.1 or data_max > 1.1) and verbose:
print(f"WARNING: Data range [{data_min:.2f}, {data_max:.2f}] - expected [0, 1]")
hidden_dim = max(n_features // 2, 4)
model = GRUDiscriminator(n_features, hidden_dim).to(device)
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()
# Balance and combine datasets
n_samples = min(len(real), len(synthetic))
X = np.concatenate([real[:n_samples], synthetic[:n_samples]], axis=0)
y = np.array([1] * n_samples + [0] * n_samples)
# Shuffle and split 70/30
indices = np.random.permutation(len(X))
X, y = X[indices], y[indices]
n_train = int(0.7 * len(X))
X_train, X_test = X[:n_train], X[n_train:]
y_train, y_test = y[:n_train], y[n_train:]
X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
# Training (iteration-based)
model.train()
iterator = tqdm(range(iterations), desc="Discriminator", disable=not verbose)
for _ in iterator:
idx = np.random.permutation(len(X_train_t))[:batch_size]
X_batch = X_train_t[idx].to(device)
y_batch = y_train_t[idx].to(device)
optimizer.zero_grad()
loss = criterion(model(X_batch), y_batch)
loss.backward()
optimizer.step()
# Evaluation
model.eval()
with torch.no_grad():
probs = torch.sigmoid(model(X_test_t.to(device))).cpu().numpy().flatten()
preds = (probs > 0.5).astype(int)
accuracy = accuracy_score(y_test, preds)
score = abs(accuracy - 0.5)
if verbose:
print(f"Discriminative: accuracy={accuracy:.1%}, score={score:.4f}")
return {"score": score, "accuracy": accuracy, "model": model}
def compute_predictive_score(
synthetic: np.ndarray,
real_holdout: np.ndarray,
hidden_dim: int = 128,
num_layers: int = 2,
epochs: int = 100,
batch_size: int = 64,
learning_rate: float = 1e-3,
device: str | None = None,
quick_test: bool = False,
verbose: bool = True,
) -> dict[str, Any]:
"""
Compute predictive score per TimeGAN paper (Section 5.2).
Protocol:
1. Train LSTM predictor on SYNTHETIC data to predict next timestep
2. Evaluate on REAL holdout data using MAE
3. Compare to TRTR baseline (train on real, test on real)
The ratio TSTR_MAE / TRTR_MAE indicates utility:
- Ratio ~1.0: Synthetic data preserves predictive patterns
- Ratio >> 1.0: Synthetic data lost temporal structure
IMPORTANT: Data is normalized per-feature before evaluation to prevent
high-variance features (e.g., volume) from dominating the MAE calculation.
Args:
synthetic: Synthetic sequences (n_samples, seq_len, n_features)
real_holdout: Real holdout sequences (never seen by generator)
hidden_dim: LSTM hidden dimension (paper: 128)
num_layers: Number of LSTM layers (paper: 2)
epochs: Training epochs
batch_size: Batch size
learning_rate: Learning rate
device: 'cuda' or 'cpu' (auto-detected if None)
quick_test: If True, reduce epochs for faster testing
verbose: Print progress
Returns:
dict with:
- mae_tstr: MAE when trained on synthetic, tested on real (normalized)
- mae_trtr: MAE when trained on real, tested on real (baseline, normalized)
- ratio: mae_tstr / mae_trtr (lower is better, 1.0 = perfect)
- model_tstr: Trained TSTR model
- model_trtr: Trained TRTR model
"""
if quick_test:
epochs = min(10, epochs)
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
device = torch.device(device)
n_features = synthetic.shape[-1]
# Normalize data per-feature to ensure equal contribution to MAE
# Compute mean/std from real holdout (the evaluation target)
holdout_flat = real_holdout.reshape(-1, n_features)
feat_mean = holdout_flat.mean(axis=0, keepdims=True)
feat_std = holdout_flat.std(axis=0, keepdims=True) + 1e-8
if verbose:
print("\nNormalizing data for fair evaluation...")
print(f" Feature means: {feat_mean.flatten()[:3]}... (showing first 3)")
print(f" Feature stds: {feat_std.flatten()[:3]}... (showing first 3)")
# Normalize all data using holdout statistics
synthetic_norm = (synthetic - feat_mean) / feat_std
real_holdout_norm = (real_holdout - feat_mean) / feat_std
# Split real holdout into train (for TRTR) and test
n_holdout = len(real_holdout_norm)
n_train_real = int(0.7 * n_holdout)
real_train = real_holdout_norm[:n_train_real]
real_test = real_holdout_norm[n_train_real:]
def prepare_prediction_data(sequences: np.ndarray) -> tuple[torch.Tensor, torch.Tensor]:
"""Prepare X (all but last timestep) and Y (shifted by 1)."""
X = torch.tensor(sequences[:, :-1, :], dtype=torch.float32)
Y = torch.tensor(sequences[:, 1:, :], dtype=torch.float32)
return X, Y
def train_predictor(
train_data: np.ndarray,
n_epochs: int,
desc: str,
) -> PostHocLSTMPredictor:
"""Train LSTM predictor on given data."""
model = PostHocLSTMPredictor(n_features, hidden_dim, num_layers).to(device)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
criterion = nn.L1Loss() # MAE loss
X_train, Y_train = prepare_prediction_data(train_data)
dataset = TensorDataset(X_train, Y_train)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
model.train()
iterator = tqdm(range(n_epochs), desc=desc, disable=not verbose)
for _ in iterator:
epoch_loss = 0.0
for X_batch, Y_batch in loader:
X_batch, Y_batch = X_batch.to(device), Y_batch.to(device)
optimizer.zero_grad()
Y_pred = model(X_batch)
loss = criterion(Y_pred, Y_batch)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
if verbose:
iterator.set_postfix(loss=epoch_loss / len(loader))
return model
def evaluate_predictor(model: PostHocLSTMPredictor, test_data: np.ndarray) -> float:
"""Evaluate MAE on test data."""
model.eval()
X_test, Y_test = prepare_prediction_data(test_data)
X_test, Y_test = X_test.to(device), Y_test.to(device)
with torch.no_grad():
Y_pred = model(X_test)
mae = torch.abs(Y_pred - Y_test).mean().item()
return mae
# TSTR: Train on Synthetic (normalized), Test on Real (normalized)
model_tstr = train_predictor(synthetic_norm, epochs, "TSTR Predictor")
mae_tstr = evaluate_predictor(model_tstr, real_test)
# TRTR: Train on Real, Test on Real (baseline)
model_trtr = train_predictor(real_train, epochs, "TRTR Predictor")
mae_trtr = evaluate_predictor(model_trtr, real_test)
# Compute ratio
ratio = mae_tstr / mae_trtr if mae_trtr > 0 else float("inf")
if verbose:
print("\nPredictive Score (MAE, lower is better):")
print(f" TRTR (baseline): {mae_trtr:.6f}")
print(f" TSTR (synthetic): {mae_tstr:.6f}")
print(f" Ratio (TSTR/TRTR): {ratio:.3f}")
if ratio <= 1.1:
print(" Assessment: [OK] Good utility")
elif ratio <= 1.5:
print(" Assessment: WARNING: Moderate utility")
else:
print(" Assessment: [FAIL] Poor utility")
return {
"mae_tstr": mae_tstr,
"mae_trtr": mae_trtr,
"ratio": ratio,
"model_tstr": model_tstr,
"model_trtr": model_trtr,
}
def compute_discriminative_score(
real: np.ndarray,
synthetic: np.ndarray,
hidden_dim: int = 128,
num_layers: int = 2,
epochs: int = 50,
batch_size: int = 64,
learning_rate: float = 1e-3,
device: str | None = None,
quick_test: bool = False,
verbose: bool = True,
) -> dict[str, Any]:
"""
Compute discriminative score per TimeGAN paper (Section 5.2).
Protocol:
1. Train LSTM classifier to distinguish real vs synthetic
2. Evaluate accuracy on held-out test set
3. Lower accuracy = better fidelity (harder to tell apart)
Target: Accuracy ~50% = indistinguishable
Good: Accuracy < 70%
Poor: Accuracy > 85%
Args:
real: Real sequences (n_samples, seq_len, n_features)
synthetic: Synthetic sequences (same shape)
hidden_dim: LSTM hidden dimension (paper: 128)
num_layers: Number of LSTM layers (paper: 2)
epochs: Training epochs
batch_size: Batch size
learning_rate: Learning rate
device: 'cuda' or 'cpu' (auto-detected if None)
quick_test: If True, reduce epochs for faster testing
verbose: Print progress
Returns:
dict with:
- accuracy: Classification accuracy (lower = better fidelity)
- auc: ROC AUC score
- model: Trained discriminator
"""
if quick_test:
epochs = min(10, epochs)
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
device = torch.device(device)
n_features = real.shape[-1]
# Normalize data per-feature (using real data statistics)
real_flat = real.reshape(-1, n_features)
feat_mean = real_flat.mean(axis=0, keepdims=True)
feat_std = real_flat.std(axis=0, keepdims=True) + 1e-8
real_norm = (real - feat_mean) / feat_std
synth_norm = (synthetic - feat_mean) / feat_std
# Balance dataset sizes
n_samples = min(len(real_norm), len(synth_norm))
real_subset = real_norm[:n_samples]
synth_subset = synth_norm[:n_samples]
# Fixed sequential split (80/20) - matches benchmark methodology
# This ensures temporal consistency: test set has most recent data from both
n_train = int(0.8 * n_samples)
train_idx = np.arange(n_train)
test_idx = np.arange(n_train, n_samples)
# Stack real and synthetic for train/test separately
X_train = np.concatenate([real_subset[train_idx], synth_subset[train_idx]], axis=0)
X_test = np.concatenate([real_subset[test_idx], synth_subset[test_idx]], axis=0)
n_train_samples = len(train_idx)
n_test_samples = len(test_idx)
y_train = np.concatenate([np.ones(n_train_samples), np.zeros(n_train_samples)])
y_test = np.concatenate([np.ones(n_test_samples), np.zeros(n_test_samples)])
# Convert to tensors
X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
X_test_t = torch.tensor(X_test, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32).unsqueeze(1)
# Create data loader
train_dataset = TensorDataset(X_train_t, y_train_t)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# Initialize model
model = PostHocLSTMDiscriminator(n_features, hidden_dim, num_layers).to(device)
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
criterion = nn.BCEWithLogitsLoss()
# Training loop
model.train()
iterator = tqdm(range(epochs), desc="Discriminator", disable=not verbose)
for _ in iterator:
epoch_loss = 0.0
for X_batch, y_batch in train_loader:
X_batch, y_batch = X_batch.to(device), y_batch.to(device)
optimizer.zero_grad()
logits = model(X_batch)
loss = criterion(logits, y_batch)
loss.backward()
optimizer.step()
epoch_loss += loss.item()
if verbose:
iterator.set_postfix(loss=epoch_loss / len(train_loader))
# Evaluation
model.eval()
X_test_t = X_test_t.to(device)
with torch.no_grad():
logits = model(X_test_t)
probs = torch.sigmoid(logits).cpu().numpy().flatten()
preds = (probs > 0.5).astype(int)
accuracy = accuracy_score(y_test, preds)
try:
auc = roc_auc_score(y_test, probs)
except ValueError:
auc = 0.5
if verbose:
print("\nDiscriminative Score (lower accuracy = better):")
print(f" Accuracy: {accuracy:.1%}")
print(f" ROC AUC: {auc:.4f}")
if accuracy <= 0.55:
print(" Assessment: [OK] Excellent fidelity (indistinguishable)")
elif accuracy <= 0.70:
print(" Assessment: [OK] Good fidelity")
elif accuracy <= 0.85:
print(" Assessment: WARNING: Moderate fidelity")
else:
print(" Assessment: [FAIL] Poor fidelity (easily distinguished)")
return {
"accuracy": accuracy,
"auc": auc,
"model": model,
}
def run_timegan_evaluation(
synthetic: np.ndarray,
real_train: np.ndarray,
real_holdout: np.ndarray,
hidden_dim: int = 128,
num_layers: int = 2,
epochs_pred: int = 100,
epochs_disc: int = 50,
iterations_yoon: int = 5000,
device: str | None = None,
quick_test: bool = False,
verbose: bool = True,
include_yoon: bool = True,
) -> dict[str, Any]:
"""
Run complete TimeGAN evaluation suite.
Combines:
1. Yoon predictive score (optional): Last-feature prediction
2. Comprehensive predictive score: All-feature TSTR with baseline
3. Discriminative score
Args:
synthetic: Synthetic sequences from generator
real_train: Real sequences for discriminative training
real_holdout: Real sequences for predictive evaluation
hidden_dim: LSTM hidden dimension
num_layers: Number of LSTM layers
epochs_pred: Epochs for comprehensive predictive model
epochs_disc: Epochs for discriminator
iterations_yoon: Iterations for Yoon metric (default: 5000)
device: Compute device
quick_test: Reduce epochs/iterations for testing
verbose: Print results
include_yoon: If True, run Yoon metric for comparison
Returns:
dict with yoon, predictive, discriminative, summary
"""
if verbose:
print("=" * 60)
print("TimeGAN Evaluation Suite")
print("=" * 60)
print("\nReference: Yoon et al., NeurIPS 2019, Section 5.2")
# Align shapes
min_samples = min(len(synthetic), len(real_train), len(real_holdout))
min_seq_len = min(synthetic.shape[1], real_train.shape[1], real_holdout.shape[1])
min_features = min(synthetic.shape[2], real_train.shape[2], real_holdout.shape[2])
synthetic = synthetic[:min_samples, :min_seq_len, :min_features]
real_train = real_train[:min_samples, :min_seq_len, :min_features]
real_holdout = real_holdout[:min_samples, :min_seq_len, :min_features]
results = {}
# 1. Yoon predictive score (optional)
if include_yoon:
if verbose:
print("\n1. YOON PREDICTIVE SCORE")
print("-" * 40)
print("(Last-feature prediction per Yoon et al.)")
yoon_results = compute_predictive_score_yoon(
synthetic=synthetic,
real=real_holdout,
iterations=iterations_yoon,
device=device,
quick_test=quick_test,
verbose=verbose,
)
results["yoon"] = yoon_results
# 2. Comprehensive predictive score
if verbose:
section_num = "2" if include_yoon else "1"
print(f"\n{section_num}. COMPREHENSIVE PREDICTIVE SCORE (Utility)")
print("-" * 40)
print("(All-feature TSTR with TRTR baseline)")
pred_results = compute_predictive_score(
synthetic=synthetic,
real_holdout=real_holdout,
hidden_dim=hidden_dim,
num_layers=num_layers,
epochs=epochs_pred,
device=device,
quick_test=quick_test,
verbose=verbose,
)
results["predictive"] = pred_results
# 3. Discriminative score
if verbose:
section_num = "3" if include_yoon else "2"
print(f"\n{section_num}. DISCRIMINATIVE SCORE (Fidelity)")
print("-" * 40)
disc_results = compute_discriminative_score(
real=real_train,
synthetic=synthetic,
hidden_dim=hidden_dim,
num_layers=num_layers,
epochs=epochs_disc,
device=device,
quick_test=quick_test,
verbose=verbose,
)
results["discriminative"] = disc_results
# Combined summary
if verbose:
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
if include_yoon:
print("\nYoon Metric (last-feature prediction):")
print(f" MAE on last feature: {yoon_results['mae']:.6f}")
print(" (Paper reports ~0.04 for GOOG stock)")
print("\nComprehensive Assessment:")
print(f" Predictive Score (MAE ratio): {pred_results['ratio']:.3f}")
print(f" Discriminative Score (accuracy): {disc_results['accuracy']:.1%}")
# Overall assessment
pred_good = pred_results["ratio"] <= 1.2
disc_good = disc_results["accuracy"] <= 0.70
if pred_good and disc_good:
print("\n Overall: [OK] Generator passes both tests")
elif pred_good or disc_good:
print("\n Overall: WARNING: Generator passes one test")
else:
print("\n Overall: [FAIL] Generator fails both tests")
# Build summary
summary = {
"mae_ratio": pred_results["ratio"],
"disc_accuracy": disc_results["accuracy"],
"disc_auc": disc_results["auc"],
}
if include_yoon:
summary["yoon_mae"] = yoon_results["mae"]
results["summary"] = summary
return results
if __name__ == "__main__":
# Quick test with random data
print("TimeGAN Metrics Module - Quick Test")
print("=" * 40)
# Generate random test data
np.random.seed(42)
n_samples = 100
seq_len = 24
n_features = 6
real_train = np.random.randn(n_samples, seq_len, n_features).astype(np.float32)
real_holdout = np.random.randn(n_samples, seq_len, n_features).astype(np.float32)
synthetic = np.random.randn(n_samples, seq_len, n_features).astype(np.float32)
# Run quick evaluation
results = run_timegan_evaluation(
synthetic=synthetic,
real_train=real_train,
real_holdout=real_holdout,
quick_test=True,
verbose=True,
)
print("\nTest completed successfully!")