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=populationThe 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")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.
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 87ba1bb4a26245e58e0edd263233182f4acd37e4d44c53dcc3a01e7687a3b9c6This 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.
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.2596817The 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.