Skip to content

Synthetic data

datasets

Synthetic data with structure worth recovering.

Gaussian noise tells you nothing about whether an embedding worked. Everything here comes with ground truth: the swiss roll has a known unrolling, the trajectory has known lineages, the hierarchical clusters have a known two-level structure. Score against that rather than looking at the picture and nodding.

float64 throughout. Cast to float32 yourself if you are feeding a GPU estimator, though those cast for you anyway.

swiss_roll

swiss_roll(
    n_samples: int = 5000,
    *,
    noise: float = 0.05,
    density_bias: float = 0.0,
    seed: int = 42,
) -> tuple[ndarray, ndarray]

A 2-D manifold rolled up in 3-D.

The standard unrolling benchmark, and still a good one: an embedding that tears the roll or folds it back on itself is doing something wrong that no cluster dataset would have shown you.

Parameters:

Name Type Description Default
n_samples int

Number of points.

5000
noise float

Standard deviation of the noise added to the surface.

0.05
density_bias float

Sampling bias along the roll. 0.0 samples uniformly; higher values pile points up at the inner end, which is what makes an embedding's density handling visible. 2.5 roughly matches the accumulation you see in real trajectory data.

0.0
seed int

Fixes the sampling.

42

Returns:

Type Description
ndarray

(X, t) where X is (n_samples, 3) and t is the position along

ndarray

the roll, which is what a correct unrolling recovers as one axis.

clustered

clustered(
    n_samples: int = 5000,
    *,
    dim: int = 50,
    n_clusters: int = 10,
    seed: int = 42,
) -> tuple[ndarray, ndarray]

Gaussian clusters of varying size and spread.

Parameters:

Name Type Description Default
n_samples int

Number of points.

5000
dim int

Ambient dimensionality.

50
n_clusters int

Number of clusters.

10
seed int

Fixes the centres and the sampling.

42

Returns:

Type Description
tuple[ndarray, ndarray]

(X, labels) with X of shape (n_samples, dim).

trajectory

trajectory(
    n_samples: int = 5000,
    *,
    topology: str = "bifurcation",
    dim: int = 50,
    noise: float = 0.1,
    seed: int = 42,
) -> tuple[ndarray, ndarray]

A branching differentiation trajectory.

This is the one that separates the algorithms. PHATE and PaCMAP tend to keep the branch points; t-SNE tends to shatter the backbone into blobs.

Parameters:

Name Type Description Default
n_samples int

Number of points, split evenly across branches.

5000
topology str

"bifurcation" for a cascading tree, "linear" for one continuous lineage, "combination" for a backbone with branches leaving it mid-way.

'bifurcation'
dim int

Ambient dimensionality. Must be at least the branch count.

50
noise float

Base noise standard deviation, which grows along pseudotime.

0.1
seed int

Fixes the sampling.

42

Returns:

Type Description
tuple[ndarray, ndarray]

(X, branch) with X of shape (n_samples, dim).

hierarchical

hierarchical(
    n_samples: int = 5000,
    *,
    dim: int = 50,
    n_supergroups: int = 4,
    n_subclusters: int = 5,
    supergroup_spread: float = 15.0,
    subcluster_spread: float = 3.0,
    point_std: float = 0.5,
    seed: int = 42,
) -> tuple[ndarray, ndarray, ndarray]

Clusters within clusters.

The test for whether an embedding kept two scales at once. Resolving the subclusters is easy; keeping the supergroups apart while doing it is the part that global-structure claims are usually made about.

Parameters:

Name Type Description Default
n_samples int

Number of points.

5000
dim int

Ambient dimensionality.

50
n_supergroups int

Top-level groups.

4
n_subclusters int

Subclusters within each group.

5
supergroup_spread float

How far apart the group centres sit.

15.0
subcluster_spread float

How far apart subcluster centres sit within a group.

3.0
point_std float

Spread of points around a subcluster centre.

0.5
seed int

Fixes the centres and the sampling.

42

Returns:

Type Description
tuple[ndarray, ndarray, ndarray]

(X, supergroup, subcluster).