Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

spatialdeform

Documentation · Source

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 using geo_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

spatialdeform-0.1.0a1.tar.gz (17.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

spatialdeform-0.1.0a1-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

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

Hashes for spatialdeform-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 f21de09bd8bcd1827806d22b93e305196a8ba0f16bab46c8fb26b8a82eaae481
MD5 20d5ba4c1f998984b641b820cb5e9f9c
BLAKE2b-256 665b33295f23892879554c8dfa40dda17755d1fe91077cade40eefb4eb1d8a7a

See more details on using hashes here.

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

Hashes for spatialdeform-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 04d4f41466c581d644252e867b0c7f5aa2644712f6e1deb583734ec2bac787d5
MD5 7e9aa1c551df65775dc3d3b25317a9dc
BLAKE2b-256 e152cd81ec637fdc6e560c98622433b65fb015bc40f7dd82d691d9fd75013262

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page