Skip to content

GeometricDGP

Two-class datasets whose boundary is a shape rather than a formula: interleaving half-circles, concentric rings, or interleaving spiral arms. Classification only — the shapes define class regions, so there is no continuous target to regress on.

These are the cases where the boundary is simple to describe and hard to represent. A linear model cannot separate concentric rings at any sample size, which makes "circles" a quick way to confirm a benchmark is actually measuring flexibility.

Choose the shape with shape="moons", "circles", or "spirals". complexity sets the Gaussian jitter added to the two geometric coordinates: σ = 0.05 at "low" keeps the boundary crisp, 0.15 at "medium", 0.30 at "high" blurs the classes into each other.

from synthbench import BenchPipeline, GeometricDGP

dgp = GeometricDGP(shape="moons", complexity="medium", random_state=0)
result = BenchPipeline(dgp).run(n_samples=500, n_features=10, random_state=42)

print(result.X.shape)  # (500, 10)
print(sum(result.metadata["signal_feature_importances"].values()))  # 1.0

The first two columns hold the geometry and get 0.5 importance each; anything beyond them is N(0,1) padding with 0.0. Those are structural constants, not measured quantities. Shapes are built from NumPy trigonometry directly, with no scikit-learn dependency.

class_weight splits the sample deterministically rather than probabilistically — n_half = int(n_samples · class_weight) points go to class 0 — so the realised balance matches the requested one exactly rather than in expectation.

One consequence of building labels from geometry: metadata["bayes_error_analytic"] is None here. The other classification DGPs draw each label from an explicit probability, which makes their error floor computable; jitter on coordinates has no such closed form. Use metadata["bayes_error"] as a difficulty proxy instead, and see the pipeline page for what that number can and cannot support.

n_classes adds structure rather than just labels: "circles" becomes concentric rings with radii evenly spaced in [0.5, 1], and "spirals" grows extra arms at even rotations. The rings crowd together as classes are added, so the task gets harder rather than merely wider. "moons" stays at two — two interleaving arcs is what a moons dataset is, and it raises if you ask for more. Class allocation is deterministic, so the realised split matches your priors exactly rather than in expectation.

Full parameter list: GeometricDGP in the API reference.