SECTION 1: LEARNING OBJECTIVES

By the end of this lesson, you will be able to:

  • Understand the importance of synthetic data in financial analytics – privacy, data scarcity, regulatory compliance (GDPR, CCPA), and model testing.

  • Distinguish between different synthetic data generation methods – statistical models, Generative Adversarial Networks (GANs), Variational Autoencoders (VAEs), and diffusion models.

  • Apply CTGAN (Conditional Tabular GAN) to generate synthetic tabular financial data.

  • Evaluate synthetic data quality using statistical similarity (Kolmogorov-Smirnov, Wasserstein distance), correlation preservation, and utility metrics (train-on-synthetic, test-on-real).

  • Understand the privacy implications of synthetic data – differential privacy and the risk of re-identification.

  • Implement synthetic data generation for a credit dataset using Python.

  • Use synthetic data for model development – augmenting training data, testing under edge cases, and enabling collaboration without sharing sensitive data.

  • Understand the regulatory stance on synthetic data – the EU AI Act, GDPR, and FDA guidance.


SECTION 2: WHY SYNTHETIC DATA IN FINANCE?

2.1 The Data Challenge in Banking
 
 
Challenge Description Impact
Privacy Regulations GDPR, CCPA, and other data protection laws restrict data sharing. Limited data for model development and collaboration.
Data Scarcity Rare events (e.g., fraud, default) are underrepresented. Models perform poorly on rare events.
Imbalanced Data Minority classes (e.g., fraud cases) are rare. Models are biased toward the majority class.
Data Silos Data is distributed across departments/entities. Limited data for comprehensive analysis.
Testing Production-like data is needed for testing new models. Risk of using real customer data in testing.
2.2 How Synthetic Data Solves These Problems
 
 
Problem Synthetic Data Solution
Privacy Generate data that preserves statistical properties but is not tied to real individuals.
Scarcity Generate additional samples of rare events.
Imbalance Oversample minority classes with synthetic data.
Silos Share synthetic data across teams/partners.
Testing Generate realistic test data without privacy risks.

SECTION 3: SYNTHETIC DATA GENERATION METHODS

3.1 Statistical Methods
 
 
Method Description Use Case
Parametric Models Assume a distribution (normal, lognormal) and sample from it. Simple cases; not flexible.
Copula Models Model correlation structure separately from marginals. Preserving multivariate relationships.
Resampling (Bootstrapping) Sample with replacement from real data. Baseline, but doesn’t solve privacy.
3.2 Generative Models
 
 
Method Description Pros Cons
GANs (Generative Adversarial Networks) Two networks (generator, discriminator) compete to generate realistic data. High quality; flexible. Training instability; mode collapse.
VAEs (Variational Autoencoders) Encode data to latent space, then decode. Stable training; generates diverse data. Can produce blurry samples.
CTGAN (Conditional Tabular GAN) GAN designed for tabular data with mixed types. Handles categorical and continuous variables well. Requires careful tuning.
Diffusion Models Gradually add noise, then learn to reverse. State-of-the-art; high quality. Computationally expensive.
SMOTE (Synthetic Minority Over-sampling) Generates synthetic samples for minority classes. Simple; effective for imbalance. Limited to simple interpolation.
3.3 Differential Privacy

Differential Privacy (DP) ensures that the output of a data generation process does not reveal whether any particular individual was in the training data.

  • Definition: A mechanism M satisfies ϵ-differential privacy if for any two datasets differing by one record, and any output O:

    P(M(D1)=O)≤eϵ⋅P(M(D2)=O)

  • DP-GAN: GAN trained with differential privacy guarantees.


SECTION 4: EVALUATING SYNTHETIC DATA

4.1 Statistical Similarity Metrics
 
 
Metric Description Formula
Kolmogorov-Smirnov (KS) Maximum difference between CDFs of real and synthetic data. ( \sup_x F_{real}(x) – F_{syn}(x) )
Wasserstein Distance Minimum cost to transform one distribution to another. Earth mover’s distance.
Correlation Difference Difference in correlation matrices between real and synthetic. ∥Σreal−Σsyn∥F
4.2 Utility Metrics
 
 
Metric Description Interpretation
Train-on-Synthetic, Test-on-Real Train a model on synthetic data, test on real data. Higher performance = better utility.
Train-on-Real, Test-on-Synthetic Train a model on real data, test on synthetic. Higher performance = better fidelity.
Member Inference Attack Test if the synthetic data reveals information about real data. Lower success = better privacy.
4.3 Privacy Metrics
 
 
Metric Description
Re-identification Risk Probability of matching synthetic records to real individuals.
k-Anonymity Each record is indistinguishable from at least k−1 others.
Differential Privacy Budget ϵ value; lower = more privacy.

SECTION 5: IMPLEMENTATION IN PYTHON – SYNTHETIC DATA GENERATION

We’ll use the CTGAN library (from SDV) to generate synthetic credit data.

python
# ===================================================================
# MODULE 7, LESSON 2: SYNTHETIC DATA GENERATION FOR FINANCE
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import ks_2samp, wasserstein_distance
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, classification_report
import warnings
warnings.filterwarnings('ignore')

# Check if SDV is installed
try:
    from sdv.tabular import CTGAN
    from sdv.metadata import SingleTableMetadata
    SDV_AVAILABLE = True
except ImportError:
    SDV_AVAILABLE = False
    print("SDV not installed. Install with: pip install sdv")
    print("Using a simplified synthetic generation for demonstration.")

# Set style
sns.set_style("whitegrid")
np.random.seed(42)

print("="*70)
print("SYNTHETIC DATA GENERATION FOR FINANCIAL APPLICATIONS")
print("="*70)

# ----------------------------------------------------------------
# PART A: REAL DATA – CREDIT DATASET
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART A: Real Credit Data")
print("-"*60)

# Generate synthetic "real" credit data (we'll treat this as our ground truth)
def generate_credit_data(n=5000):
    """Generate realistic credit data."""
    np.random.seed(42)
    df = pd.DataFrame({
        'age': np.random.normal(45, 12, n).clip(22, 75).astype(int),
        'income': np.random.gamma(5, 15, n) + 20,
        'dti': np.random.beta(2, 5, n) * 60,
        'credit_score': np.random.normal(700, 50, n).clip(550, 850).astype(int),
        'loan_amount': np.random.gamma(4, 50, n) + 30,
        'employment_years': np.random.gamma(3, 5, n).clip(0, 30).astype(int),
        'home_owner': np.random.binomial(1, 0.65, n),
        'marital_status': np.random.choice(['Single', 'Married', 'Other'], n, p=[0.4, 0.4, 0.2]),
        'education': np.random.choice(['High School', 'College', 'Bachelors', 'Post-Grad'], n, p=[0.2, 0.3, 0.3, 0.2]),
        'default': np.random.binomial(1, 0.05, n)  # 5% default rate
    })
    # Add correlation: higher credit_score -> lower default
    # We'll do this by adjusting default based on credit score
    prob_default = 0.1 - 0.0001 * (df['credit_score'] - 550)
    prob_default = prob_default.clip(0.01, 0.3)
    df['default'] = np.random.binomial(1, prob_default)
    return df

# Generate "real" data
real_df = generate_credit_data(10000)
print(f"Real data shape: {real_df.shape}")
print(f"Default rate: {real_df['default'].mean():.4f}")

# ----------------------------------------------------------------
# PART B: SYNTHETIC DATA GENERATION (CTGAN)
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Synthetic Data Generation")
print("-"*60)

if SDV_AVAILABLE:
    print("Using CTGAN for synthetic data generation...")
    
    # Create metadata
    metadata = SingleTableMetadata()
    metadata.detect_from_dataframe(real_df)
    
    # Train CTGAN
    ctgan = CTGAN(epochs=100, batch_size=500, verbose=False)
    ctgan.fit(real_df)
    
    # Generate synthetic data
    synthetic_df = ctgan.sample(num_rows=len(real_df))
    print(f"Synthetic data shape: {synthetic_df.shape}")
    
    # Save models
    ctgan.save('ctgan_model.pkl')
    print("CTGAN model saved.")
    
else:
    print("CTGAN not available. Using a simpler approach (bootstrapping with noise).")
    # Generate synthetic data by bootstrapping with noise
    synthetic_df = real_df.sample(n=len(real_df), replace=True).reset_index(drop=True)
    # Add noise to continuous features
    for col in ['age', 'income', 'dti', 'credit_score', 'loan_amount', 'employment_years']:
        noise = np.random.normal(0, synthetic_df[col].std() * 0.05, len(synthetic_df))
        synthetic_df[col] = synthetic_df[col] + noise
        synthetic_df[col] = synthetic_df[col].clip(synthetic_df[col].min(), synthetic_df[col].max())
    
    print("Synthetic data generated using bootstrapping with noise.")

# ----------------------------------------------------------------
# PART C: STATISTICAL EVALUATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Statistical Evaluation")
print("-"*60)

# Compare distributions
def compare_distributions(real, synthetic, columns):
    """Compare real and synthetic distributions."""
    results = []
    for col in columns:
        if col in real.columns and col in synthetic.columns:
            # Kolmogorov-Smirnov test
            ks_stat, ks_p = ks_2samp(real[col].dropna(), synthetic[col].dropna())
            # Wasserstein distance
            wass_dist = wasserstein_distance(real[col].dropna(), synthetic[col].dropna())
            results.append({
                'Feature': col,
                'KS Statistic': ks_stat,
                'KS P-value': ks_p,
                'Wasserstein Distance': wass_dist
            })
    return pd.DataFrame(results)

# Select continuous features
continuous_features = ['age', 'income', 'dti', 'credit_score', 'loan_amount', 'employment_years']
comparison_df = compare_distributions(real_df, synthetic_df, continuous_features)
print("\nDistribution Comparison (Real vs Synthetic):")
print(comparison_df.to_string(index=False))

# Visualise distributions
fig, axes = plt.subplots(2, 3, figsize=(15, 10))

for i, col in enumerate(continuous_features):
    ax = axes[i // 3, i % 3]
    ax.hist(real_df[col], bins=30, alpha=0.5, label='Real', color='blue')
    ax.hist(synthetic_df[col], bins=30, alpha=0.5, label='Synthetic', color='green')
    ax.set_xlabel(col)
    ax.set_ylabel('Frequency')
    ax.set_title(f'{col}')
    ax.legend()
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('distribution_comparison.png', dpi=300)
plt.show()

# Correlation comparison
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Real correlation
ax = axes[0]
corr_real = real_df[continuous_features + ['default']].corr()
sns.heatmap(corr_real, annot=True, fmt='.2f', cmap='coolwarm', ax=ax, cbar=False)
ax.set_title('Real Data Correlation')

# Synthetic correlation
ax = axes[1]
corr_synth = synthetic_df[continuous_features + ['default']].corr()
sns.heatmap(corr_synth, annot=True, fmt='.2f', cmap='coolwarm', ax=ax, cbar=False)
ax.set_title('Synthetic Data Correlation')

plt.tight_layout()
plt.savefig('correlation_comparison.png', dpi=300)
plt.show()

# Correlation difference
corr_diff = np.abs(corr_real - corr_synth).mean().mean()
print(f"\nAverage correlation difference: {corr_diff:.4f}")

# ----------------------------------------------------------------
# PART D: UTILITY EVALUATION – TRAIN-ON-SYNTHETIC, TEST-ON-REAL
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Utility Evaluation (Train-on-Synthetic, Test-on-Real)")
print("-"*60)

# Prepare features
features = continuous_features + ['home_owner', 'marital_status', 'education']
# Encode categorical variables
def encode_categorical(df):
    df_enc = df.copy()
    # One-hot encode categorical variables
    categorical_cols = ['marital_status', 'education']
    for col in categorical_cols:
        dummies = pd.get_dummies(df[col], prefix=col)
        df_enc = pd.concat([df_enc, dummies], axis=1)
        df_enc = df_enc.drop(columns=[col])
    # Home_owner is already binary
    return df_enc

# Encode both datasets
real_enc = encode_categorical(real_df)
synthetic_enc = encode_categorical(synthetic_df)

# Get feature columns (after encoding)
X_cols = [col for col in real_enc.columns if col != 'default']
y_col = 'default'

# Split real data for final testing
X_real, X_test, y_real, y_test = train_test_split(real_enc[X_cols], real_enc[y_col], test_size=0.3, random_state=42)

# Train on synthetic, test on real
print("Training on Synthetic, Testing on Real...")
model_syn = RandomForestClassifier(n_estimators=100, max_depth=8, random_state=42)
model_syn.fit(synthetic_enc[X_cols], synthetic_enc[y_col])
y_pred_syn = model_syn.predict_proba(X_test)[:, 1]
auc_syn = roc_auc_score(y_test, y_pred_syn)
print(f"AUC (Train Synthetic, Test Real): {auc_syn:.4f}")

# Train on real, test on real (baseline)
print("\nTraining on Real, Testing on Real (Baseline)...")
model_real = RandomForestClassifier(n_estimators=100, max_depth=8, random_state=42)
model_real.fit(X_real, y_real)
y_pred_real = model_real.predict_proba(X_test)[:, 1]
auc_real = roc_auc_score(y_test, y_pred_real)
print(f"AUC (Train Real, Test Real): {auc_real:.4f}")

# Utility ratio
utility_ratio = auc_syn / auc_real
print(f"\nUtility Ratio (Synthetic/Real): {utility_ratio:.4f}")
if utility_ratio > 0.9:
    print("  ✓ Excellent utility – synthetic data is a good substitute.")
elif utility_ratio > 0.7:
    print("  ✓ Good utility – synthetic data can be used for development.")
else:
    print("  ⚠ Limited utility – synthetic data may need improvement.")

# ----------------------------------------------------------------
# PART E: PRIVACY EVALUATION – MEMBER INFERENCE ATTACK
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Privacy Evaluation – Member Inference Attack")
print("-"*60)

def member_inference_attack(real_df, synthetic_df, model):
    """
    Simplified member inference attack.
    If the model can distinguish between real and synthetic data, privacy may be compromised.
    """
    # Create combined dataset with labels: 0 = synthetic, 1 = real
    combined = pd.concat([
        synthetic_df.assign(label=0),
        real_df.assign(label=1)
    ]).sample(frac=1, random_state=42).reset_index(drop=True)
    
    # Use the same features as before
    X_attack = combined[X_cols]
    y_attack = combined['label']
    
    # Train-test split
    X_attack_train, X_attack_test, y_attack_train, y_attack_test = train_test_split(
        X_attack, y_attack, test_size=0.3, random_state=42
    )
    
    # Train a classifier to distinguish real from synthetic
    attack_model = RandomForestClassifier(n_estimators=100, random_state=42)
    attack_model.fit(X_attack_train, y_attack_train)
    
    # Evaluate
    y_attack_pred = attack_model.predict_proba(X_attack_test)[:, 1]
    auc_attack = roc_auc_score(y_attack_test, y_attack_pred)
    
    return auc_attack

# Run attack
auc_attack = member_inference_attack(real_enc, synthetic_enc, None)
print(f"Member Inference Attack AUC: {auc_attack:.4f}")
print(f"Interpretation: {auc_attack - 0.5:.4f} above random (0.5)")
if auc_attack < 0.6:
    print("  ✓ Good privacy – synthetic data does not leak real data.")
elif auc_attack < 0.7:
    print("  ⚠ Moderate privacy – some risk of re-identification.")
else:
    print("  ✗ Poor privacy – synthetic data resembles real data too closely.")

# ----------------------------------------------------------------
# PART F: SYNTHETIC DATA FOR IMBALANCE MITIGATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Using Synthetic Data to Address Class Imbalance")
print("-"*60)

# Check class imbalance in real data
default_rate = real_df['default'].mean()
print(f"Default rate in real data: {default_rate:.4f} ({default_rate*100:.2f}%)")

# Generate more synthetic samples for the minority class (default)
print("Generating additional synthetic data for the minority class...")

if SDV_AVAILABLE:
    # Generate more synthetic data
    extra_synthetic = ctgan.sample(num_rows=5000)
    # Identify the default class
    extra_defaults = extra_synthetic[extra_synthetic['default'] == 1]
    print(f"Generated {len(extra_defaults)} additional default samples.")
else:
    # Use bootstrapped approach
    default_samples = real_df[real_df['default'] == 1]
    extra_defaults = default_samples.sample(n=5000, replace=True).reset_index(drop=True)
    # Add noise
    for col in continuous_features:
        noise = np.random.normal(0, extra_defaults[col].std() * 0.05, len(extra_defaults))
        extra_defaults[col] = extra_defaults[col] + noise
    print(f"Generated {len(extra_defaults)} additional default samples (bootstrapped).")

# Combine with original data
augmented_df = pd.concat([real_df, extra_defaults], ignore_index=True)
new_default_rate = augmented_df['default'].mean()
print(f"New default rate: {new_default_rate:.4f} ({new_default_rate*100:.2f}%)")

# Train on augmented data
augmented_enc = encode_categorical(augmented_df)
X_aug = augmented_enc[X_cols]
y_aug = augmented_enc[y_col]

model_aug = RandomForestClassifier(n_estimators=100, max_depth=8, random_state=42)
model_aug.fit(X_aug, y_aug)
y_pred_aug = model_aug.predict_proba(X_test)[:, 1]
auc_aug = roc_auc_score(y_test, y_pred_aug)

print(f"\nAUC with augmented data: {auc_aug:.4f}")
print(f"Improvement over real-only: {auc_aug - auc_real:.4f}")

# ----------------------------------------------------------------
# PART G: REGULATORY PERSPECTIVE
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART G: Regulatory Perspective on Synthetic Data")
print("-"*60)

print("""
Regulatory Stance on Synthetic Data:

1. GDPR (EU):
   - Synthetic data is NOT considered personal data if it cannot be re-identified.
   - Benefits: Can be shared freely, reducing compliance burden.
   - Risks: If synthetic data can be linked back to individuals, it becomes personal data.

2. EU AI Act:
   - Synthetic data is encouraged for model training (reduces privacy risks).
   - Must be properly validated and documented.

3. CCPA (California):
   - Similar to GDPR: Synthetic data is not personal data if it cannot be re-identified.

4. FDA Guidance:
   - Synthetic data can be used in clinical trials (drug discovery, device testing).
   - Must demonstrate that synthetic data is representative.

5. Banking Regulators (SR 11-7):
   - Synthetic data can be used for model development and testing.
   - Must be validated against real data.
   - Models trained on synthetic data still require validation on real data.

Best Practices:
  - Always evaluate synthetic data quality (statistical and utility).
  - Test for re-identification risk.
  - Document the generation process.
  - Use differential privacy for sensitive applications.
  - Combine synthetic and real data for best results.
""")

# ----------------------------------------------------------------
# PART H: SUMMARY AND RECOMMENDATIONS
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART H: Summary and Recommendations")
print("="*70)

print("""
Synthetic Data Generation – Key Takeaways:

1. Synthetic data solves privacy, scarcity, and imbalance challenges.
2. Methods: GANs (CTGAN), VAEs, Diffusion models, Statistical models.
3. Evaluate using: Statistical similarity (KS, Wasserstein), utility (AUC), privacy (member inference).
4. Utility Ratio = AUC(synthetic-trained) / AUC(real-trained); > 0.9 is excellent.
5. Synthetic data can significantly improve model performance on rare events.
6. CTGAN is the leading library for tabular financial data.
7. Differential privacy should be considered for highly sensitive data.
8. Regulatory acceptance is growing; document generation process thoroughly.

Recommendations:
  - Start with CTGAN for tabular financial data.
  - Evaluate synthetic data quality rigorously.
  - Use synthetic data for internal development and testing.
  - For regulatory submissions, validate models on real data.
  - Consider differential privacy for high-risk applications.
""")

print("="*70)
print("END OF LESSON 2 – MODULE 7")
print("="*70)

SECTION 6: COMPARISON OF SYNTHETIC DATA METHODS

 
 
Method Quality Privacy Speed Tabular Data Categorical Data Differential Privacy
Bootstrapping Low Low Fast Yes Yes No
Parametric Models Low Moderate Fast Yes Limited Yes
Copula Models Moderate Moderate Medium Yes Limited Yes
SMOTE Moderate (for minority) High Fast Yes Yes No
CTGAN High High Medium Yes Yes Limited
VAEs High High Medium Yes Yes Yes
Diffusion Models Very High High Slow Yes Yes Yes

SECTION 7: SUMMARY FOR THE DATA PRACTITIONER

  • Synthetic data is a powerful tool for addressing privacy, scarcity, and imbalance in financial analytics.

  • CTGAN is the leading method for tabular synthetic data.

  • Evaluation requires statistical similarity, utility (AUC), and privacy (re-identification risk).

  • Utility Ratio > 0.9 indicates synthetic data is a good substitute for real data.

  • Regulatory acceptance is growing; synthetic data can be shared and used for development.

  • Best practice: Combine synthetic and real data for optimal model performance.


SECTION 8: RECOMMENDED NEXT STEPS

  1. Install SDV and experiment with CTGAN on your own datasets.

  2. Evaluate synthetic data quality using the techniques from this lesson.

  3. Explore differential privacy (e.g., using TensorFlow Privacy).

  4. Apply synthetic data augmentation to improve model performance on imbalanced datasets.

  5. Prepare for the next lesson on Federated Learning.


[END OF LESSON 2 – MODULE 7]

 
Â