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.