Skip to content

Getting started

This example deforms a small network whose eastern segment takes four times as long to traverse as the other sides.

1. Create an edge GeoDataFrame

import geopandas as gpd
from shapely.geometry import LineString

edges = gpd.GeoDataFrame(
    {
        "source": ["a", "b", "c", "d", "a"],
        "target": ["b", "c", "d", "a", "c"],
        "travel_time_s": [60.0, 240.0, 60.0, 60.0, 85.0],
    },
    geometry=[
        LineString([(0, 0), (500, 40), (1000, 0)]),
        LineString([(1000, 0), (1000, 1000)]),
        LineString([(1000, 1000), (0, 1000)]),
        LineString([(0, 1000), (0, 0)]),
        LineString([(0, 0), (1000, 1000)]),
    ],
    crs="EPSG:27700",
)

Each row is one graph edge. Travel costs must be positive. Missing values fail fast unless you select an explicit missing-cost policy.

2. Fit and transform

from spatialdeform import SpatialDeformer

deformer = SpatialDeformer(
    cost="travel_time_s",
    backend="mds",
    geo_weight=0.1,
)

warped_edges = deformer.fit_transform(edges)
warped_nodes = deformer.get_nodes()

warped_edges retains the input columns, index, and CRS. Its active geometry contains deformed LineStrings. warped_nodes contains one Point per inferred graph node.

3. Inspect learned state

print(deformer.embedding_.shape)
print(deformer.scale_)
print(deformer.normalized_stress_)
print(deformer.n_iter_)
Attribute Meaning
embedding_ deformed node coordinates in working_crs_
geographic_coordinates_ original node coordinates in working_crs_
graph_distances_ symmetric all-pairs travel-cost distances
scale_ fitted cost-unit → map-unit conversion
normalized_stress_ residual mismatch; lower is better
working_crs_ projected CRS used by the optimizer

4. Plot both geometries

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(11, 5))
edges.plot(ax=axes[0], color="#64748b", linewidth=2)
axes[0].set_title("Geographic network")

warped_edges.plot(ax=axes[1], color="#f97316", linewidth=2)
warped_nodes.plot(ax=axes[1], color="white", edgecolor="black", markersize=35)
axes[1].set_title("Travel-time deformation")

for axis in axes:
    axis.set_aspect("equal")
    axis.set_axis_off()

plt.tight_layout()
plt.show()

Passing costs as y

Like an unsupervised scikit-learn transformer, fit accepts y=None. Passing an array as y overrides the configured cost column:

morning = SpatialDeformer(cost="unused")
morning_edges = morning.fit_transform(edges, y=edges["morning_time_s"])

This is helpful when scenarios are stored outside the edge table.

Geographic coordinates

EPSG:4326 input is accepted. By default, SpatialDeform estimates a suitable UTM CRS, optimizes in metres, and converts the result back:

wgs84_edges = gpd.read_file("network.geojson")
warped = SpatialDeformer(cost="travel_time_s").fit_transform(wgs84_edges)

assert warped.crs == wgs84_edges.crs

Set auto_project=False when automatic projection would be inappropriate. In that case, callers must provide a planar CRS.

Next steps