Skip to content

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.

pip install "synthbench[io]"
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.