Skip to content

Multiple scenarios

Hourly travel times, disruption states, and before/after interventions should share one cost-to-map scale. Otherwise scale="auto" independently normalizes each state and hides uniform expansion or contraction.

Reuse the reference scale

from spatialdeform import SpatialDeformer

reference = SpatialDeformer(
    cost="free_flow_s",
    geo_weight=0.1,
).fit(edges)

models = {}
warped = {}

for column in ["morning_s", "midday_s", "evening_s"]:
    model = SpatialDeformer(
        cost=column,
        scale=reference.scale_,
        geo_weight=0.1,
    )
    warped[column] = model.fit_transform(edges)
    models[column] = model

If every edge becomes 50% slower, its target graph distances are now 50% larger in the shared coordinate frame.

Keep topology and ordering fixed

For meaningful animation:

  • use the same edge rows in the same order;
  • keep source and target IDs stable;
  • use the same CRS;
  • keep estimator hyperparameters constant except for cost/y;
  • reuse the reference scale_.

Validate these conditions before fitting a batch.

Temporal smoothing

The current standalone estimator fits each scenario independently. For smooth animation, interpolate the resulting node displacement arrays or use the application pipeline’s temporally regularized landmark solver.

Linear interpolation between two fitted states is straightforward:

alpha = 0.25
xy = (
    (1 - alpha) * models["morning_s"].embedding_
    + alpha * models["midday_s"].embedding_
)

For many scenarios, align state embeddings to a shared reference or use a non-zero geographic penalty so every fit remains in the same absolute frame.

Comparing diagnostics

for name, model in models.items():
    mean_displacement = (
        ((model.embedding_ - model.geographic_coordinates_) ** 2).sum(axis=1) ** 0.5
    ).mean()
    print(name, model.normalized_stress_, mean_displacement)

Track both stress and displacement. A state with low stress but enormous movement may be unsuitable for a recognizable map.