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¶
- Learn the exact input data contract.
- Follow the visual map example, including missing costs.
- Tune the regularized MDS objective.
- Compare the deterministic Isomap backend.
- Keep several hours comparable with a shared scale.