Metadata-Version: 2.4
Name: odflows
Version: 0.1.1
Summary: Origin-destination flow toolkit for mobility and commuting analysis: OD tables, desire lines, zone re-aggregation, net/gross flows, filtering, Tao & Thill flow clustering, self-containment indicators, Intramax functional regions, and flowmap.blue / kepler.gl export.
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/odflows_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/odflows_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/odflows_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/odflows_sdk/blob/main/CHANGELOG.md
Keywords: origin-destination,od-matrix,flows,desire-lines,commuting,mobility,functional-regions,intramax,self-containment,flow-clustering,flowmap,kepler-gl,geojson,gis,transport-planning
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Environment :: Console
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.21
Provides-Extra: pandas
Requires-Dist: pandas>=1.3.0; extra == "pandas"
Provides-Extra: geo
Requires-Dist: shapely>=2.0; extra == "geo"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# odflows

[![PyPI version](https://img.shields.io/pypi/v/odflows.svg)](https://pypi.org/project/odflows/)
[![Python versions](https://img.shields.io/pypi/pyversions/odflows.svg)](https://pypi.org/project/odflows/)
[![CI](https://github.com/YusufEminoglu/odflows_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/odflows_sdk/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/YusufEminoglu/odflows_sdk/blob/main/LICENSE)

**Origin-destination flow toolkit for mobility and commuting analysis.**

`odflows` covers the usual steps of an OD study in one small package that needs only
numpy:

- **Read** OD tables in long form (`origin,destination,flow`) or as a square matrix, and
  zone centroids from GeoJSON (points, or area centroids of polygons) or CSV.
- **Desire lines** as GeoJSON LineStrings: directed, net or gross; straight, **curved**
  or **offset**, so both directions of a two-way pair stay visible.
- **Change zone systems**: aggregate and disaggregate with weighted lookup tables,
  including area weights computed from two polygon layers (optional `shapely`).
- **Net and gross flows**, symmetric part, asymmetry index, intrazonal handling.
- **Filter** flows: minimum size, top-k, share of origin / destination / total,
  cumulative share, dominant (largest outgoing) flows.
- **Cluster and bundle** flows with the Tao & Thill flow distance: agglomerative
  (single / complete / average linkage) and DBSCAN, in pure numpy.
- **Indicators**: in/out degree and strength, supply- and demand-side
  self-containment, commuting job ratio, net balance.
- **Functional regions** (commuting zones) with **Intramax** (Masser & Brown 1975),
  optionally contiguity-constrained.
- **Export** to GeoJSON, **flowmap.blue** (`locations` + `flows` sheets) and a
  **kepler.gl** arc-layer CSV.
- A command-line tool, `odflows`.

```bash
pip install odflows            # numpy only
pip install "odflows[pandas]"  # + DataFrame helpers
pip install "odflows[geo]"     # + shapely, for area-weighted lookups from polygons
```

Requires Python 3.9 or newer.

## Quick start

```python
from odflows import (
    ODMatrix, read_od_csv, centroids_from_geojson, desire_lines, write_geojson,
    zone_summary, intramax, region_summary, filter_top_k,
)

od = read_od_csv("commute.csv")                 # origin,destination,flow
zones = centroids_from_geojson("zones.geojson", id_field="zone")

for row in zone_summary(od):                     # self-containment, job ratio, ...
    print(row["zone"], round(row["supply_sc"], 3), round(row["job_ratio"], 2))

lines = desire_lines(filter_top_k(od, 20), zones, style="curved")
write_geojson(lines, "desire_lines.geojson")

hierarchy = intramax(od)                         # full Intramax hierarchy
membership = hierarchy.membership(n_regions=4)   # cut it at 4 regions
print(region_summary(od, membership))
```

A runnable example with synthetic data is in
[`examples/quickstart.py`](examples/quickstart.py).

Matrices can also be built in memory:

```python
od = ODMatrix.from_long([("A", "B", 120), ("B", "A", 40), ("A", "A", 300)])
od = ODMatrix(["A", "B"], [[300, 120], [40, 0]])  # rows = origins, columns = destinations
od.net().to_long()   # [('A', 'B', 80.0)]
```

## Command line

```bash
odflows summary commute.csv -o zones.csv              # per-zone indicators
odflows summary commute.csv --json                    # matrix statistics
odflows lines commute.csv --centroids zones.geojson --id-field zone \
        --style curved --min-flow 50 -o lines.geojson
odflows filter commute.csv --min-share 0.1 --basis origin -o main_flows.csv
odflows transform commute.csv net -o net.csv          # net | gross | symmetric | ...
odflows aggregate commute.csv --lookup lookup.csv -o districts.csv
odflows regions commute.csv -n 5 -o regions.csv --summary regions_sc.csv
odflows cluster commute.csv --centroids zones.geojson --id-field zone \
        --n-clusters 8 -o bundles.geojson
odflows export commute.csv --centroids zones.geojson --id-field zone \
        --format flowmap -o mymap                       # mymap-locations.csv, mymap-flows.csv
```

Every command accepts `--matrix` for square-matrix input and `--origin-col`,
`--dest-col`, `--flow-col` for other column names. Centroids may be a GeoJSON file or a
CSV (`--id-field`, `--x-col`, `--y-col`).

## API overview

| Area | Functions / classes |
|---|---|
| Container | `ODMatrix` (`from_long`, `to_long`, `outflows`, `inflows`, `intrazonal`, `without_intrazonal`, `net`, `gross`, `symmetric`, `transpose`, `asymmetry_index`, `subset`) |
| Input / output | `read_od_csv`, `read_matrix_csv`, `write_od_csv`, `write_matrix_csv`, `centroids_from_geojson`, `read_centroids_csv`, `geometry_centroid`, `write_geojson` |
| Desire lines | `desire_lines`, `line_coordinates`, `haversine_km` |
| Zone systems | `ZoneLookup` (`from_records`, `from_mapping`, `from_csv`, `from_geojson`), `reaggregate`, `aggregate` |
| Filtering | `filter_min_flow`, `filter_top_k`, `filter_share`, `filter_cumulative`, `dominant_flows` |
| Indicators | `degree`, `self_containment`, `job_ratio`, `net_balance`, `zone_summary`, `matrix_summary` |
| Clustering | `FlowSet`, `flow_distance_matrix`, `flow_neighbour_counts`, `hierarchical_clusters`, `flow_dbscan`, `bundle_flows` |
| Regions | `intramax`, `IntramaxResult`, `region_summary` |
| Visualisation export | `write_flowmap_blue`, `write_kepler_csv` |
| pandas | `from_dataframe`, `to_dataframe` |

All filters and transformations return a new `ODMatrix`, so they chain:
`filter_top_k(filter_share(od, 0.1), 50).without_intrazonal()`.

## Methods and sources

**Conventions.** `T[i, j]` is the flow from zone `i` to zone `j`. For commuting, rows
are places of residence and columns places of work, so `O_i = Σ_j T_ij` are resident
workers and `D_j = Σ_i T_ij` are jobs; both include intrazonal flows `T_ii`.

**Net and gross flows.** Gross `G_ij = T_ij + T_ji`; net `N_ij = max(T_ij − T_ji, 0)`.
The asymmetry index is `Σ_{i<j} |T_ij − T_ji| / Σ_{i<j} (T_ij + T_ji)`.

**Desire lines.** Curved lines are quadratic Bézier curves whose control point is the
midpoint moved to the right of the direction of travel by `bend × length`; offset lines
are the segment shifted to the right by the same amount. A→B and B→A therefore fall on
opposite sides. Lengths are haversine kilometres for longitude/latitude centroids
(mean Earth radius 6371.0088 km) and planar units otherwise.

**Polygon centroids** use the shoelace (area-weighted) formula on the exterior ring
minus holes, treating coordinates as planar.

**Zone re-aggregation.** With a row-normalised lookup `W` (`W[s, t]` = share of source
zone `s` in target zone `t`), `T' = Wᵀ T W`. Flows are split assuming origins and
destinations are spread within each source zone in proportion to the weights - areal
weighting (Goodchild & Lam 1980). Different origin and destination lookups (e.g.
population vs. employment weights) are supported; totals are preserved.

**Filtering.** `filter_share(basis="origin")` keeps `T_ij / O_i ≥ s`;
`dominant_flows` keeps each origin's largest outgoing flow, the first step of nodal
region analysis (Nystuen & Dacey 1961).

**Flow distance.** Tao & Thill (2016) define, for flows `i` and `j` with origin
distance `d_O` and destination distance `d_D`,
`FD = sqrt(α d_O² + β d_D²)` with `α + β = 2`, and the length-normalised
dissimilarity `FDS = sqrt((α d_O² + β d_D²) / (L_i L_j))`. Coordinates must be
projected. `flow_neighbour_counts` gives the local counting term of their flow
K-function (no edge correction or significance test).

**Clustering.** Agglomerative clustering uses the Lance-Williams (1967) update for
single, complete and average (UPGMA) linkage; DBSCAN follows Ester et al. (1996), with
an option to count flow volume instead of flow numbers. `bundle_flows` replaces every
cluster by the flow from its volume-weighted mean origin to its volume-weighted mean
destination.

**Self-containment.** Supply-side `SC_s = T_ii / O_i` (share of resident workers who
work locally) and demand-side `SC_d = T_ii / D_i` (share of local jobs filled by
residents), as used for local labour market areas (Goodman 1970; Casado-Díaz 2000).
The commuting job ratio is `D_i / O_i`.

**Intramax.** Hierarchical aggregation (Masser & Brown 1975) that repeatedly merges
the pair of current regions maximising `T_ij / (O_i D_j) + T_ji / (O_j D_i)` on the
aggregated matrix; merged flows become intrazonal. The full hierarchy is stored and can
be cut at any number of regions. An optional contiguity constraint only merges adjacent
regions (Drobne & Lakner 2016). This is Intramax, not a modularity / Louvain method.

**Export formats.** flowmap.blue reads a Google Sheet with a `locations` sheet
(`id,name,lat,lon`) and a `flows` sheet (`origin,dest,count`); `write_flowmap_blue`
writes one CSV per sheet. `write_kepler_csv` writes `origin_lat/origin_lng` and
`dest_lat/dest_lng` column pairs, which kepler.gl detects as point fields for its arc
and line layers.

### References

- Casado-Díaz, J. M. (2000). Local labour market areas in Spain: a case study. *Regional Studies*, 34(9), 843-856.
- Drobne, S., & Lakner, M. (2016). Use of constraints in the hierarchical aggregation procedure Intramax. *Business Systems Research*, 7(2), 5-22.
- Ester, M., Kriegel, H.-P., Sander, J., & Xu, X. (1996). A density-based algorithm for discovering clusters in large spatial databases with noise. *Proceedings of KDD-96*, 226-231.
- Goodchild, M. F., & Lam, N. S.-N. (1980). Areal interpolation: a variant of the traditional spatial problem. *Geo-Processing*, 1, 297-312.
- Goodman, J. F. B. (1970). The definition and analysis of local labour markets: some empirical problems. *British Journal of Industrial Relations*, 8(2), 179-196.
- Lance, G. N., & Williams, W. T. (1967). A general theory of classificatory sorting strategies: 1. Hierarchical systems. *The Computer Journal*, 9(4), 373-380.
- Masser, I., & Brown, P. J. B. (1975). Hierarchical aggregation procedures for interaction data. *Environment and Planning A*, 7(5), 509-523.
- Nystuen, J. D., & Dacey, M. F. (1961). A graph theory interpretation of nodal regions. *Papers of the Regional Science Association*, 7, 29-42.
- Tao, R., & Thill, J.-C. (2016). Spatial cluster detection in spatial flow data. *Geographical Analysis*, 48(4), 355-372.

## Limitations

- Centroids, Bézier offsets, polygon centroids and the flow distance are planar. Use a
  projected CRS for clustering and for exact curve shapes; `desire_lines` guesses
  longitude/latitude from coordinate ranges (override with `geographic=`), which can
  misfire for small projected coordinates.
- Re-aggregation assumes uniform spreading within source zones (areal weighting).
  Use population or employment weights where you have them.
- Intramax and agglomerative clustering are O(n³); fine for hundreds of zones and a few
  thousand flows, not for full national matrices. Pairs with no interaction are never
  merged, so Intramax may stop before reaching the requested number of regions.
- Flow statistics stop at the flow distance, dissimilarity and neighbour counts: the
  flow K-function with edge correction, and significance testing, are not implemented.
- No travel-to-work-area (TTWA) thresholds are built in; check region self-containment
  against the criteria of your own study.

## Academic citation

If you use `odflows` in research, please cite it:

```bibtex
@software{eminoglu_odflows,
  author  = {Eminoğlu, Yusuf},
  title   = {odflows},
  year    = {2026},
  version = {0.1.1},
  url     = {https://github.com/YusufEminoglu/odflows_sdk}
}
```

See also [`CITATION.cff`](CITATION.cff).

## License

MIT © 2026 Yusuf Eminoğlu. Contact: geospacephilo@gmail.com
