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.