Skip to main content

Calibrated Synthetic Data Generator for Monte Carlo simulations

Project description

calsyn

Calibrated Synthetic Data Generator for Monte Carlo simulations.

Generate realistic synthetic trajectories using a piecewise-linear data-generating process calibrated on real data:

Y = effect(t) + f(X) + ε

where:

  • f(X) — CatBoost model trained on real features, capturing nonlinear structure
  • effect(t) — date-based treatment effect (single start date or arbitrary windows with different τ)
  • τ — user-defined effect size added to the target trajectory during the treatment period
  • ε — noise sampled from a distribution fitted to calibration residuals

Installation

pip install calsyn

With Optuna hyperparameter tuning:

pip install "calsyn[tune]"

Quick Start

import polars as pl
from calsyn import CalibratedGenerator

# df is the source Polars DataFrame with date, feature columns, and target column.
# Split it into X (date + features) and y (target) before fitting.
target_col = "SBER"
X = df.drop(target_col)
y = df[target_col].to_numpy()

gen = CalibratedGenerator(date_col="date", noise="auto")
gen.fit(X, y)

# mode A: effect starts at a given date
result = gen.generate(X, treatment_start="2024-06-01", tau=0.02, n_simulations=500)
result.simulations.shape  # (500, n_samples)

Input Format

The library expects the input data as:

  • df — source pl.DataFrame with one date/datetime column, numeric feature columns, and one target column
  • X — model input: df without the target column
  • y — target array extracted from df[target_col]

X must contain:

  • a date/datetime column (name passed via date_col)
  • numeric feature columns used to predict the target

X must not contain the target column. The order of rows in X and y must match: y[i] is the target value for row X[i]. The date column is used for treatment windows and stratified validation; all other columns in X are passed to CatBoost as numeric features.

from datetime import date, timedelta
import polars as pl

start = date(2023, 1, 1)
df = pl.DataFrame({
    "date": [start + timedelta(days=i) for i in range(500)],
    "GAZP": gazp_log_returns,
    "LKOH": lkoh_log_returns,
    "USDRUB": usdrub_log_returns,
    "SBER": sber_log_returns,
})

target_col = "SBER"
X = df.drop(target_col)
y = df[target_col].to_numpy()

gen = CalibratedGenerator(date_col="date", noise="auto")
gen.fit(X, y)

Equivalent shape requirements:

assert len(X) == len(y)
assert "date" in X.columns
assert target_col not in X.columns

Treatment Modes

tau (Greek letter τ) is the treatment effect size that you set manually for the target variable. It is not estimated by the model; it is added directly to the generated target value for dates where treatment is active:

Y_sim(t) = f(X_t) + tau + ε_t

For example, if the target is log-returns, tau=0.02 means an additive shock of 0.02 to the simulated log-return on treated dates. tau=0.0 is the no-effect baseline, and negative values model a negative shock.

Mode A — single start date

Everything after treatment_start gets effect τ. Supports tau grid for sweep.

# single tau
result = gen.generate(X, treatment_start="2024-06-01", tau=0.02)

# tau grid — returns dict[float, GenerationResult]
results = gen.generate(
    X, treatment_start="2024-06-01",
    tau=[0.0, 0.01, 0.02, 0.05],
    n_simulations=500,
)
for tau_val, res in results.items():
    print(f"tau={tau_val}: mean={res.simulations.mean():.4f}")

Mode B — arbitrary windows with different effects

Each window is (start_date, end_date, tau). Dates are inclusive. Outside all windows, effect is zero.

result = gen.generate(
    X,
    treatment=[
        ("2024-03-01", "2024-03-15", 0.03),   # positive shock in early March
        ("2024-06-01", "2024-06-10", -0.01),   # negative shock in June
        ("2024-09-01", "2024-09-30", 0.05),    # larger effect in September
    ],
    n_simulations=500,
)

Feature Noise Simulation

A separate mode for sensitivity analysis: study how noise in individual features propagates to Y.

Y_sim = f(X̃) + ε_Y
X̃[j] = X[j] + ε_X[j]   for j in feature_noise
X̃[j] = X[j]             otherwise

The model f is not re-trained — user-specified noise is injected into the features directly. This is independent of treatment effects; do not mix with generate().

result = gen.generate_with_feature_noise(
    X,
    feature_noise={
        "USDRUB": {"distribution": "normal", "scale": 0.01},
        "OIL":    {"distribution": "t", "scale": 0.02, "df": 4},
    },
    n_simulations=500,
)

result.simulations.shape              # (500, n_samples) — Y trajectories
result.X_simulations["USDRUB"].shape  # (500, n_samples) — noisy feature values
result.f_X_mean                       # f(X) on original features, for comparison

feature_noise spec

Each entry maps a feature name to a noise spec dict:

Key Required Description
distribution yes "normal" or "t"
scale yes Std dev (normal) or scale parameter (t)
loc no (default 0) Location shift
df yes for "t" Degrees of freedom

X_simulations contains only the noised features — un-noised features remain in the original X.

Validation

The model is trained on ~90% of data (configurable via val_fraction). The held-out set is stratified by time period — from each period, val_fraction of observations go to OOS.

Stratification period is set via strat_freq:

strat_freq Period Use case
"M" Month Daily/weekly financial data (default)
"W" Week Daily data with short history
"d" Day Intraday (hourly/minute) data
"h" Hour Sub-minute tick data
"m" Minute High-frequency tick data
# daily data — stratify by month (default)
gen = CalibratedGenerator(date_col="date", noise="auto", val_fraction=0.1)

# intraday data — stratify by day
gen = CalibratedGenerator(date_col="date", noise="auto", val_fraction=0.1, strat_freq="d")

gen.fit(X, y)
print(gen.oos_metrics)
# {'r2': 0.82, 'rmse': 0.017, 'bias': -0.0003}

Noise Distribution

Three modes for the residual noise ε:

# auto: pick normal or t by BIC on residuals
gen = CalibratedGenerator(date_col="date", noise="auto")

# force Gaussian
gen = CalibratedGenerator(date_col="date", noise="normal")

# force Student-t (heavier tails)
gen = CalibratedGenerator(date_col="date", noise="t")

After fitting, inspect the chosen distribution:

gen.fit(X, y)
print(gen.noise_params)
# {'distribution': 't', 'df': 4.2, 'loc': 0.001, 'scale': 0.012}

Overriding the noise scale

When residual variance is unreliable (e.g. low data variability or poor model fit), you can set the noise scale directly instead of relying on the fitted residuals:

gen = CalibratedGenerator(date_col="date", noise="normal", noise_scale=0.05)
gen.fit(X, y)

print(gen.noise_params)
# {'distribution': 'normal', 'loc': ..., 'scale': 0.05}

Random Seed

By default random_seed=None, so simulation outputs vary between runs — which is the correct behavior for Monte Carlo. Pass an integer to fix the seed for reproducibility:

# reproducible
gen = CalibratedGenerator(date_col="date", random_seed=42)

# stochastic (default) — different trajectories each time
gen = CalibratedGenerator(date_col="date")

Diagnostics

result_h0 = gen.generate(X, treatment_start="2024-06-01", tau=0.0, n_simulations=500)
report = gen.diagnose(result_h0)

print(report.oos_metrics)
# {'r2': 0.82, 'rmse': 0.017, 'bias': -0.0003}

print(report.ks)
# KSResult(statistic=0.04, pvalue=0.72)

print(report.coverage)
# CoverageDiagnostic(coverage_1sigma=0.68, coverage_2sigma=0.95, n_points=500)

for c in report.correlations:
    print(f"  X{c.feature_index}: real={c.corr_real:.3f} synth={c.corr_synthetic:.3f}")

Plots

# coverage plot: real trajectory vs 1σ/2σ bands
gen.plot_coverage(result_h0)

# feature correlation comparison
gen.plot_correlations(result_h0, feature_names=["GAZP", "LKOH", "USDRUB"])

Optuna Tuning

gen = CalibratedGenerator(date_col="date", noise="auto", auto_tune=True, n_trials=100)
gen.fit(X, y)

API Reference

CalibratedGenerator

Parameter Type Default Description
date_col str "date" Name of date/datetime column in X
noise "auto" | "normal" | "t" "auto" Noise distribution
noise_scale float | None None Override fitted noise scale
auto_tune bool False Optuna hyperparameter search
n_trials int 50 Optuna trials
val_fraction float 0.1 OOS fraction per time period
strat_freq str "M" Stratification: "M", "W", "d", "h", "m"
catboost_params dict | None None Custom CatBoost params
random_seed int | None None Random seed (None = varies between runs)

Methods:

  • .fit(X, y) — fit calibration model (train-only) and noise distribution
  • .generate(X, treatment_start=..., tau=..., n_simulations=...) — mode A
  • .generate(X, treatment=[...], n_simulations=...) — mode B
  • .diagnose(result) — KS, correlations, coverage, OOS metrics
  • .plot_coverage(result) — coverage plot with 1σ/2σ bands
  • .plot_correlations(result, feature_names=...) — correlation bar chart
  • .oos_metrics — dict with R², RMSE, bias
  • .noise_params — fitted noise distribution parameters

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

calsyn-0.1.0.tar.gz (21.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

calsyn-0.1.0-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

File details

Details for the file calsyn-0.1.0.tar.gz.

File metadata

  • Download URL: calsyn-0.1.0.tar.gz
  • Upload date:
  • Size: 21.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for calsyn-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1f14d62e61b1a42720a7eb17e23bf283550747da71f4259070ad94796768cbe5
MD5 0990f22d41d195a180c8efed94e88d03
BLAKE2b-256 6fecf408d72b79de0860cd302ce19da310c385a0ae8cae6606fde3e65c67838e

See more details on using hashes here.

File details

Details for the file calsyn-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: calsyn-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for calsyn-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 020ec25303484989bbb3aaa6ae6ea6f9ac7a1441abc2301d074b337cae52eabf
MD5 fd21de9e00b2bcd35a5810d2857dfbdb
BLAKE2b-256 85be3f04b3d7ad5b261ee43454d9b99aad42caa478a80a471e5eb693bd2208df

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page