Serialization
Three ways to persist a BenchResult, and one way to avoid persisting it at all.
All of them carry the full metadata alongside X and y. A dataset without its metadata has
lost the thing that makes it worth using — you would no longer know its error floor or which
features were supposed to matter.
Parquet
The default choice: compact, typed, and the metadata rides inside the file. Needs the io
extra.
from synthbench import BenchPipeline, BenchResult, LinearDGP, MeasurementNoiseCorruptor
pipeline = BenchPipeline(
LinearDGP(task_type="regression"),
corruptors=[MeasurementNoiseCorruptor(noise_level=0.2)],
)
result = pipeline.run(n_samples=500, n_features=10, random_state=42)
result.to_parquet("data.parquet")
restored = BenchResult.from_parquet("data.parquet")
print(restored.X.shape) # (500, 10)
print(restored.metadata["dgp_class"]) # "LinearDGP"
Features become columns feature_0, feature_1, ...; the target goes in a sentinel column named
__y__; the metadata is JSON in the schema header under the bytes key
b"synthbench_metadata". Bytes keys are what the Parquet spec requires, so any Parquet reader
can retrieve the metadata even without synthbench installed.
Everything is coerced to float64. Integer and boolean features survive numerically but come
back as floats. pyarrow is imported inside the method rather than at module load, so
import synthbench works without the extra and only these two calls raise ImportError.
CSV with a sidecar
For when something downstream has to read the data without a Parquet library, or when you want to look at it in a text editor.
from synthbench import BenchPipeline, BenchResult, LinearDGP
result = BenchPipeline(LinearDGP(task_type="classification")).run(
n_samples=300, n_features=8, random_state=7
)
result.to_csv("data.csv")
# writes data.csv and data.meta.json
restored = BenchResult.from_csv("data.csv")
print(restored.X.shape) # (300, 8)
Two files, and both are required: from_csv raises FileNotFoundError if the
{stem}.meta.json sidecar is missing rather than handing back a result with empty metadata.
Keep them together when you share a dataset.
Replay instead of storage
The metadata contains the DGP class and its arguments, the corruptor chain, and the master seed. That is everything needed to regenerate the data, so for large datasets you can ship a few kilobytes of metadata instead of gigabytes of arrays.
import numpy as np
from synthbench import BenchPipeline, BenchResult, LinearDGP, MeasurementNoiseCorruptor
pipeline = BenchPipeline(
LinearDGP(task_type="classification"),
corruptors=[MeasurementNoiseCorruptor(noise_level=0.1)],
)
result = pipeline.run(n_samples=200, n_features=10, random_state=42)
result.to_parquet("data.parquet")
# Later, elsewhere:
restored = BenchResult.from_parquet("data.parquet")
replayed = BenchPipeline.from_metadata(restored.metadata)
params = restored.metadata["dgp_params"]
again = replayed.run(
n_samples=params["n_samples"],
n_features=params["n_features"],
random_state=params["random_state"],
)
assert np.array_equal(again.X, restored.X)
assert np.array_equal(again.y, restored.y)
What makes this work is dgp_params["dgp_key"] — a short registry key such as "linear" —
together with the saved corruptor keys and parameters. Note that dgp_params["random_state"]
holds the pipeline's master seed, not the derived seed the DGP actually ran with, precisely so
that passing it back to run() reproduces the whole thing.
Replay is bit-identical when you pass the same n_samples, n_features, and random_state,
and when the synthbench, NumPy, and scikit-learn versions match. Those versions are recorded in
the metadata so you can check rather than assume. Replaying a RandomNeuralDGP result also
needs the neural extra; the registry populates on demand, so from_metadata triggers the
import itself.
Difficulty fields in saved metadata
| Key | Type | Notes |
|---|---|---|
bayes_error_analytic |
float or None |
Exact label-noise floor. None for regression and GeometricDGP. |
bayes_error |
float or None |
Empirical 1-NN LOO estimate. None for regression, for n < 2, and when X has NaN. |
bayes_error_method |
str or None |
"empirical_knn" when the estimate was computed. |
bayes_error_n_subsample |
int |
Present only when the sample count exceeded the 2000-row cap. |
effective_rank |
float or None |
Roy & Vetterli (2007), on the post-corruption X. None on NaN. |
dgp_params.dgp_key |
str |
Registry key used by from_metadata. |
The distinction between the two Bayes-error fields matters when you report numbers — see BenchPipeline.