This release is a pre-release and may not be stable for production use.
spatialdeform
spatialdeform turns a cost-weighted line network into a travel-time
cartogram. It accepts a GeoPandas edge table, computes graph shortest-path
costs, embeds the graph, and smoothly warps every vertex of every line.
The API follows scikit-learn conventions: constructor arguments are
hyperparameters, fit learns a deformation, transform applies it, and fitted
attributes end in _.
Install
During development:
cd spatialdeform
uv sync --dev
Quick start
The input must have LineString or MultiLineString geometry and positive travel
costs. Missing costs fail fast by default, or can be dropped, estimated from
length, median-imputed, or resolved by a callable. source and target
columns are recommended; if they are missing, line endpoints are used as node
IDs.
import geopandas as gpd
from spatialdeform import SpatialDeformer
edges = gpd.read_file("roads.gpkg")
model = SpatialDeformer(
backend="mds",
cost="travel_time_s",
source="source",
target="target",
geo_weight=0.1,
spatial_weight="gaussian",
)
warped_edges = model.fit_transform(edges)
warped_nodes = model.get_nodes()
print(model.scale_, model.normalized_stress_)
Missing-cost handling is explicit and auditable:
model = SpatialDeformer(
cost="travel_time_s",
missing_cost="neighbor",
).fit(edges)
print(model.imputed_edges_, model.costs_)
You may pass travel costs as y, like a normal scikit-learn estimator:
warped_edges = model.fit_transform(edges, y=edges["morning_time_s"])
Sparse pairwise observations
Full output geometry does not require a dense all-pairs metric. Pass network
nodes, baseline edges, and an explicit sparse observation table to
fit_sparse. Missing pairs remain absent; graph edges preserve local
structure. A displacement-Laplacian penalty can additionally make neighbouring
road nodes move coherently without introducing temporal coupling:
model = SpatialDeformer(geo_weight=0.01, max_iter=300).fit_sparse(
nodes,
edges,
observations, # source, target, value, optional weight
edge_weight=2.0,
displacement_smooth_weight=6.0,
)
deformed_nodes = model.get_nodes()
For each time (t), the sparse objective is
$$ \begin{aligned} \mathcal{L}t(Z_t) ={}& \underbrace{\sum{(i,j)\in\Omega_t} w_{ij,t} \left(\lVert z_{i,t}-z_{j,t}\rVert_2-D_{ij,t}\right)^2}{ \text{travel-time fit}} \ &+ \lambda_E \underbrace{\sum{(i,j)\in E} \left(\lVert z_{i,t}-z_{j,t}\rVert_2-d^G_{ij}\right)^2}{ \text{road-edge preservation}} \ &+ \lambda_S \underbrace{\sum{(i,j)\in E} \left|(z_{i,t}-x_i)-(z_{j,t}-x_j)\right|2^2}{ \text{neighbouring-displacement smoothness}} \ &+ \lambda_G \underbrace{\sum_i\lVert z_{i,t}-x_i\rVert_2^2}_{ \text{geographic anchoring}}. \end{aligned} $$
Here (x_i) is the geographic node coordinate, (E) is the road-edge set, and (d^G_{ij}) is a road edge's geographic length. Unobserved pairs remain absent from (\Omega_t); the API never constructs a dense all-pairs matrix. Observation completion or shrinkage belongs in the domain adapter, not in the generic estimator.
Multiple time periods
Cost units have no intrinsic map scale. Fit an automatic scale on a reference period, then reuse it so later periods visibly contract or expand:
reference = SpatialDeformer(cost="free_flow_s").fit(edges)
morning = SpatialDeformer(
cost="morning_s",
scale=reference.scale_,
).fit(edges)
evening = SpatialDeformer(
cost="evening_s",
scale=reference.scale_,
).fit(edges)
Backends
backend="mds"uses regularized SMACOF. Its sparse per-time objective combines travel-time fit, road-edge preservation, neighbouring-displacement smoothness, and geographic anchoring. Pair weights can be uniform, Gaussian spatially weighted, or inverse-distance weighted.backend="isomap"uses the Isomap/classical-scaling embedding stage on the shortest-path distances already supplied by the edge network, aligns the result to the original map, and blends it with geography usinggeo_weight.
MDS is the default for a recognizable cartogram. Isomap is a fast, deterministic comparison backend. PCA is intentionally not a backend because a travel-cost matrix is a dissimilarity matrix rather than a feature matrix.
CRS behavior
Geographic input (for example EPSG:4326) is automatically projected to a local
UTM CRS for optimization and transformed back afterward. Set
auto_project=False to require callers to supply projected data.
Notebook tour
notebooks/test_spatialdeform.ipynb is an
annotated, end-to-end tour covering synthetic data, OpenStreetMap networks,
temporal scenarios, NYC taxi-derived costs, diagnostics, and export. Downloaded
datasets and generated outputs are intentionally excluded from Git.
The maintained NYC research workflow is the staged command under
../experiments/nyc_taxi_2016. It uses sparse
coordinate-to-node observations over the full Manhattan drive graph and writes
provenance-checked checkpoints and figures.
Contributing
See the repository contribution guide for setup, quality checks, notebook expectations, and pull-request guidance.
Current scope
- one connected network component per estimator;
- two-dimensional output;
- positive edge costs;
- symmetric output distances (directed costs can be combined by mean/min/max);
- exact node displacement plus inverse-distance interpolation for interior geometry vertices.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file spatialdeform-0.1.0a1.tar.gz.
File metadata
- Download URL: spatialdeform-0.1.0a1.tar.gz
- Upload date:
- Size: 17.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f21de09bd8bcd1827806d22b93e305196a8ba0f16bab46c8fb26b8a82eaae481
|
|
| MD5 |
20d5ba4c1f998984b641b820cb5e9f9c
|
|
| BLAKE2b-256 |
665b33295f23892879554c8dfa40dda17755d1fe91077cade40eefb4eb1d8a7a
|
File details
Details for the file spatialdeform-0.1.0a1-py3-none-any.whl.
File metadata
- Download URL: spatialdeform-0.1.0a1-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04d4f41466c581d644252e867b0c7f5aa2644712f6e1deb583734ec2bac787d5
|
|
| MD5 |
7e9aa1c551df65775dc3d3b25317a9dc
|
|
| BLAKE2b-256 |
e152cd81ec637fdc6e560c98622433b65fb015bc40f7dd82d691d9fd75013262
|