Input data¶
Edge table contract¶
SpatialDeformer.fit(X, y=None) expects X to be a non-empty
geopandas.GeoDataFrame with LineString or MultiLineString geometry.
Required information:
- a geometry for every edge;
- a positive travel cost for every edge, or an explicit missing-cost policy;
- a way to identify the source and target node.
Explicit node identifiers¶
The recommended form contains source and target columns:
| source | target | travel_time_s | geometry |
|---|---|---|---|
A |
B |
74.2 | LINESTRING (…) |
B |
C |
105.0 | LINESTRING (…) |
Identifiers may be strings, integers, or any hashable scalar. Geometry order is assumed to run from source to target.
Geometry endpoint inference¶
If endpoint columns are unavailable, use geometry coordinates as node IDs:
Coordinates are rounded to node_precision decimal places before matching.
Lines that should connect must therefore share endpoints after rounding.
Snapping is not topology repair
node_precision handles small floating-point differences. It does not snap
visibly separated lines, split crossings, or create missing junctions. Clean
the network with an appropriate GIS topology workflow first.
Travel costs¶
Costs can represent time, generalized cost, distance, energy, or another positive additive quantity. Shortest paths sum edge costs.
Zero and negative costs always raise a ValueError. Missing values (NaN or
infinity) raise by default, but can be handled explicitly with missing_cost:
| Policy | Behavior | Best used when |
|---|---|---|
"error" |
list the affected edge indices and stop | completeness is required |
"drop" |
omit those edges from graph fitting | the remaining network stays connected |
"length" |
use working-CRS length / fallback_speed |
a defensible fallback speed is known |
"median" |
use the global median, or a group median with impute_by |
peer edges are comparable |
| callable | run a project-specific resolver | domain rules or an external model are available |
For example, use road-class medians and inspect exactly what changed:
model = SpatialDeformer(
cost="travel_time_s",
missing_cost="median",
impute_by="road_class",
)
warped = model.fit_transform(edges)
print(model.imputed_edges_) # input index labels
print(model.dropped_edges_)
print(model.costs_) # resolved values, aligned with edges
print(model.edge_mask_) # rows used to fit the graph
Groups with no observed value fall back to the global median. Length-based
imputation uses the projected working CRS, so with metre coordinates and costs
in seconds, fallback_speed must be metres per second:
A callable receives copies of the input GeoDataFrame and cost array and must return one positive finite value per row:
import numpy as np
def resolve_costs(frame, costs):
missing = ~np.isfinite(costs)
costs[missing] = frame.loc[missing, "modelled_time_s"]
return costs
model = SpatialDeformer(missing_cost=resolve_costs)
Dropping can break connectivity
missing_cost="drop" excludes edges only from graph fitting; transformed
output still contains every input geometry. If excluded edges disconnect
the fitted graph, fitting raises the normal disconnected-components error.
Other invalid inputs raise a ValueError:
- zero or negative costs;
- an array whose length differs from
len(X).
For asymmetric networks, set directed=True. A Euclidean embedding is
necessarily symmetric, so reciprocal path costs are combined using
directed_policy="mean", "min", or "max".
Connected components¶
One estimator currently accepts one connected component. Disconnected input raises an actionable error rather than silently inventing distances.
component_models = {}
outputs = []
for component_id, component_edges in split_connected_components(edges):
model = SpatialDeformer(cost="travel_time_s")
outputs.append(model.fit_transform(component_edges))
component_models[component_id] = model
This keeps component placement an explicit cartographic decision.
CRS rules¶
| Input CRS | Behavior |
|---|---|
| projected | optimized directly in that CRS |
geographic and auto_project=True |
estimated UTM working CRS, then restored |
geographic and auto_project=False |
rejected |
| absent | coordinates treated as planar unitless values |
Use a local projected CRS when a network crosses UTM zones, spans a continent, or requires a specific datum.
Geometry deformation¶
Graph nodes move exactly to their fitted embedding coordinates. Interior line vertices use inverse-distance weighted node displacement:
where n_neighbors is \(k\) and displacement_power is \(q\). Larger powers
make nearby nodes dominate more strongly.
Z coordinates are retained while X/Y are deformed.