odflows
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+flowssheets) and a kepler.gl arc-layer CSV. - A command-line tool,
odflows.
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
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.
Matrices can also be built in memory:
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
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_linesguesses longitude/latitude from coordinate ranges (override withgeographic=), 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:
@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.
License
MIT © 2026 Yusuf Eminoğlu. Contact: geospacephilo@gmail.com
Metadata
Release files for odflows 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| odflows-0.1.1.tar.gz | 51.8 kB | With provenance | Details for odflows-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| odflows-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for odflows-0.1.1-py3-none-any.whl |
Total release size: 90.8 kB
Release files / odflows-0.1.1.tar.gz
| Download URL | odflows-0.1.1.tar.gz |
|---|---|
| Size | 51.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3f89ead34d74e1799cab12d358fc2be4fbd3349f62b54398f3572c1e87ff627d
|
|
BLAKE2b-256 checksum How to use checksums |
024609d401d62570693efb0cff5299e7ff017df6a1a60201d9bf551b6ed72687
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logRelease files / odflows-0.1.1-py3-none-any.whl
| Download URL | odflows-0.1.1-py3-none-any.whl |
|---|---|
| Size | 38.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7230a817d0eb33227604493682593a9b3245b3c1774235bb51d887e242c0c42f
|
|
BLAKE2b-256 checksum How to use checksums |
9425ad87b38a69680864cbfd1e2f6ed925860c37c46a1911f1dcf8488a5f5384
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for odflows 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for odflows-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.