Metadata-Version: 2.4
Name: spatialdeform
Version: 0.1.0a1
Summary: Scikit-learn-style spatial deformation of GeoPandas networks from travel costs
Keywords: cartogram,geopandas,gis,isomap,mds,network
Author: Jason Pit
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Dist: geopandas>=0.14
Requires-Dist: numpy>=1.24
Requires-Dist: scikit-learn>=1.3
Requires-Dist: scipy>=1.10
Requires-Dist: shapely>=2.0
Requires-Dist: matplotlib>=3.8 ; extra == 'plot'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/jasoncpit/spatialDeform
Project-URL: Repository, https://github.com/jasoncpit/spatialDeform
Project-URL: Issues, https://github.com/jasoncpit/spatialDeform/issues
Provides-Extra: plot
Description-Content-Type: text/markdown

# spatialdeform

[Documentation](https://spatialdeform-docs.vercel.app/) ·
[Source](https://github.com/jasoncpit/spatialDeform/tree/main/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:

```bash
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.

```python
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:

```python
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:

```python
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:

```python
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:

```python
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`](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`](../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](../CONTRIBUTING.md) 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.
