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— sourcepl.DataFramewith one date/datetime column, numeric feature columns, and one target columnX— model input:dfwithout the target columny— target array extracted fromdf[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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f14d62e61b1a42720a7eb17e23bf283550747da71f4259070ad94796768cbe5
|
|
| MD5 |
0990f22d41d195a180c8efed94e88d03
|
|
| BLAKE2b-256 |
6fecf408d72b79de0860cd302ce19da310c385a0ae8cae6606fde3e65c67838e
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
020ec25303484989bbb3aaa6ae6ea6f9ac7a1441abc2301d074b337cae52eabf
|
|
| MD5 |
fd21de9e00b2bcd35a5810d2857dfbdb
|
|
| BLAKE2b-256 |
85be3f04b3d7ad5b261ee43454d9b99aad42caa478a80a471e5eb693bd2208df
|