Synthetic datasets for geometric smoothing

Simulated data with a known true response are useful for checking a smoother. This vignette shows how to describe and generate such data reproducibly. For an overview of the whole package, see Finding your way around geosmooth.

geosmooth describes a synthetic dataset by four components: the geometry (the shape on which the points lie and how it sits in the observed coordinates), the sampling (how points are drawn on that shape), the truth (the true response at each point), and the response (the noise added to the truth). synthetic.spec() combines the four components into a specification without drawing any data. materialize.synthetic() then draws the data, using the seed and the random-number policy you give it, and returns a validated synthetic_dataset.

library(geosmooth)

circle.spec <- synthetic.spec(
  geometry = dgraphs::synthetic.circle(radius = 1, ambient.dim = 2),
  sampling = dgraphs::synthetic.sampling.grid.interval(
    0, 2 * pi, endpoints = "exclude.lower"),
  truth = synthetic.truth.polynomial(c(b0 = 0)),
  response = synthetic.response.gaussian(sd = 0.05)
)
circle.data <- materialize.synthetic(circle.spec, n = 80, seed = 2026)
circle.data
#> <synthetic_dataset> adhoc__specd155e302ac4d6d54d5298f321d3b01cbba0f539a0eaca5e892e69502f3fce186__n80__seed2026__rngnamed.stream.v1
#>   n=80  intrinsic=1  ambient=2  truth=population

The circle has one latent coordinate, the angle, and two observed coordinates, the position in the plane. The truth is deliberately constant, so the response in this example is pure noise around zero; the example is not meant to test whether a smoother recovers a varying signal.

old.par <- par(mfrow = c(1, 2), mar = c(4, 4, 2, 1))
plot(circle.data, view = "geometry", col = "#0072B2", asp = 1,
     main = "Observed geometry")
plot(circle.data, view = "response", col = "grey40", xlab = "Angle (radians)",
     main = "Response and truth")
A circle of 80 points beside noisy responses scattered around a dashed zero line over angles from zero to two pi.

Left: the circle in its two observed coordinates. Right: responses versus latent angle, with the constant zero truth shown by a dashed line. These views describe the same 80 observations.

par(old.par)

The second kind of geometry, dgraphs::synthetic.quadform(), is a surface of a chosen intrinsic dimension inside a space of a chosen, larger ambient dimension. Its shape is given by quadratic forms: a flat surface has none, and a paraboloid or a saddle has one or more.

surface.spec <- synthetic.spec(
  geometry = dgraphs::synthetic.quadform(
    intrinsic.dim = 2,
    ambient.dim = 5,
    forms = list(diag(c(1, -1))),
    frame = "random.orthonormal"
  ),
  sampling = dgraphs::synthetic.sampling.uniform.box(-1, 1),
  truth = synthetic.truth.polynomial(c(b0 = 0, b1 = 1, b22 = -0.5)),
  response = synthetic.response.gaussian(sd = 0.1)
)
surface.data <- materialize.synthetic(surface.spec, n = 100, seed = 17)
dim(surface.data$latent)
#> [1] 100   2
dim(surface.data$predictors)
#> [1] 100   5
head(as.data.frame(surface.data))
#>                                                                                                      dataset.id
#> 1 adhoc__spec90adf9b19cf8db3041878a96a32d5b64aefa1f3bcd1b727a58bb377520b71d2f__n100__seed17__rngnamed.stream.v1
#>   recipe.id registry.tag   n intrinsic.dim ambient.dim truth.scope
#> 1      <NA>         <NA> 100             2           5  population
#>        rng.policy
#> 1 named.stream.v1
#>                                                           checksum
#> 1 87ba1bb4a26245e58e0edd263233182f4acd37e4d44c53dcc3a01e7687a3b9c6

This surface has two latent coordinates and five observed coordinates; a random orthonormal frame places it in five dimensions. A plot of two of the observed coordinates is a projection, so points that look close in the plot may be far apart in the coordinates that are not shown.

plot(surface.data, view = "geometry", color = "truth", main = "Projected surface")
A projected surface point cloud colored by its known conditional mean, with a numeric color key.

Two-coordinate projection of a surface embedded in five dimensions. Point colors indicate the known, nonconstant conditional mean; the key uses its actual numerical range.

In this example the truth is a polynomial that varies over the surface. A smoother is given only the observed coordinates and the noisy response, not the latent coordinates or the truth:

surface.fit <- fit.lps(surface.data$predictors, surface.data$response,
  foldid = rep(1:5, length.out = surface.data$n),
  support.grid = c(20L, 30L), degree.grid = 1L,
  kernel.grid = "gaussian", coordinate.method = "local.pca", chart.dim = 2L)
surface.fit$selected
#>   support.size degree   kernel bandwidth.multiplier candidate.id chart.dim
#> 2           30      1 gaussian                    1            2         2
#>   cv.rmse.observed
#> 2        0.1237177
head(data.frame(response = surface.data$response, fitted = fitted(surface.fit),
                truth = surface.data$truth))
#>     response     fitted      truth
#> 1 -1.0535767 -1.0190866 -0.9362011
#> 2  0.3064608  0.3704115  0.3825321
#> 3 -0.6452932 -0.6436731 -0.5908854
#> 4  0.8054648  0.8094692  0.7589337
#> 5 -1.2332430 -1.4065217 -1.3718367
#> 6 -0.3173372 -0.2974997 -0.2596817

The fitted values are at the observed points. The truth is known only because the data are simulated. The cross-validation score that selected the settings is not an independent estimate of prediction accuracy. The first-fit guide explains that distinction.

The package also contains a registry of predefined specifications, listed by synthetic.registry.ids(). The identifiers S01–S16 name signal shapes, and the suffixes V1–V3 name combinations of sampling and noise; together they form the test suite used for Hessian-energy regression. For example, S01.V1 is a signal with two separated, narrow peaks of equal height, sampled uniformly, with Gaussian noise. The identifiers G1–G7 are kept from earlier versions: G1 is a polynomial in the observed coordinates, and G2 is a polynomial on a flat surface. The dataset.id of a generated dataset includes a hash of its complete specification.

ids <- c("G1", "G2", "S01.V1")
do.call(rbind, lapply(ids, function(id) {
  spec <- synthetic.registry.spec(id)
  data.frame(id, geometry = spec$geometry.spec$family,
             truth = spec$truth.spec$parameters$estimand)
}))
#>       id geometry
#> 1     G1 quadform
#> 2     G2 quadform
#> 3 S01.V1 quadform
#>                                                               truth
#> 1                                       Polynomial conditional mean
#> 2                                       Polynomial conditional mean
#> 3 Gaussian-mixture signal normalized by its realized design maximum
registered <- synthetic.registry.spec("S01.V1")
registered.data <- materialize.synthetic(
  registered, n = 160,
  seed = synthetic.registry.seed("S01.V1", replicate = 1),
  rng.policy = "legacy"
)
synthetic.dataset.checksum(registered.data)
#> [1] "b157f04855125d66efa3802655da6ba495dd9655ea44becc1076daf76c5c5f5c"

For new work, use rng.policy = "named.stream.v1". The "legacy" policy reproduces the random numbers of earlier versions exactly and exists only so that earlier results can be reproduced. Noise in the predictors, noise in the response, and the normalization of the truth over the sampled points are specified separately, and changing one does not change the others.