odflows 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odflows-0.1.0/CHANGELOG.md +27 -0
- odflows-0.1.0/CITATION.cff +20 -0
- odflows-0.1.0/LICENSE +21 -0
- odflows-0.1.0/MANIFEST.in +9 -0
- odflows-0.1.0/PKG-INFO +256 -0
- odflows-0.1.0/README.md +216 -0
- odflows-0.1.0/examples/commute.csv +21 -0
- odflows-0.1.0/examples/quickstart.py +67 -0
- odflows-0.1.0/examples/zones.geojson +171 -0
- odflows-0.1.0/pyproject.toml +122 -0
- odflows-0.1.0/setup.cfg +4 -0
- odflows-0.1.0/src/odflows/__init__.py +102 -0
- odflows-0.1.0/src/odflows/__main__.py +7 -0
- odflows-0.1.0/src/odflows/_version.py +1 -0
- odflows-0.1.0/src/odflows/cli.py +340 -0
- odflows-0.1.0/src/odflows/clustering.py +286 -0
- odflows-0.1.0/src/odflows/errors.py +11 -0
- odflows-0.1.0/src/odflows/filtering.py +131 -0
- odflows-0.1.0/src/odflows/indicators.py +119 -0
- odflows-0.1.0/src/odflows/io.py +326 -0
- odflows-0.1.0/src/odflows/lines.py +163 -0
- odflows-0.1.0/src/odflows/matrix.py +245 -0
- odflows-0.1.0/src/odflows/pandas_ext.py +43 -0
- odflows-0.1.0/src/odflows/py.typed +0 -0
- odflows-0.1.0/src/odflows/regions.py +178 -0
- odflows-0.1.0/src/odflows/zones.py +198 -0
- odflows-0.1.0/src/odflows.egg-info/PKG-INFO +256 -0
- odflows-0.1.0/src/odflows.egg-info/SOURCES.txt +41 -0
- odflows-0.1.0/src/odflows.egg-info/dependency_links.txt +1 -0
- odflows-0.1.0/src/odflows.egg-info/entry_points.txt +2 -0
- odflows-0.1.0/src/odflows.egg-info/requires.txt +13 -0
- odflows-0.1.0/src/odflows.egg-info/top_level.txt +1 -0
- odflows-0.1.0/tests/conftest.py +21 -0
- odflows-0.1.0/tests/test_cli.py +160 -0
- odflows-0.1.0/tests/test_clustering.py +157 -0
- odflows-0.1.0/tests/test_filtering.py +91 -0
- odflows-0.1.0/tests/test_indicators.py +60 -0
- odflows-0.1.0/tests/test_io.py +212 -0
- odflows-0.1.0/tests/test_lines.py +90 -0
- odflows-0.1.0/tests/test_matrix.py +103 -0
- odflows-0.1.0/tests/test_pandas.py +27 -0
- odflows-0.1.0/tests/test_regions.py +61 -0
- odflows-0.1.0/tests/test_zones.py +105 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- `ODMatrix`: square OD matrix with zone labels; long/matrix conversion, outflows,
|
|
10
|
+
inflows, intrazonal flows, net / gross / symmetric flows, asymmetry index, subsets.
|
|
11
|
+
- Readers and writers: long and matrix CSV, zone centroids from GeoJSON (points or
|
|
12
|
+
polygon area centroids) or CSV, GeoJSON, flowmap.blue `locations`/`flows` CSVs,
|
|
13
|
+
kepler.gl arc-layer CSV.
|
|
14
|
+
- `desire_lines`: directed, net or gross desire lines; straight, curved (quadratic
|
|
15
|
+
Bezier) or offset geometry separating the two directions of a pair.
|
|
16
|
+
- `ZoneLookup` and `reaggregate`: (dis)aggregation between zone systems with
|
|
17
|
+
row-normalised weighted lookups, including area weights from polygons (shapely).
|
|
18
|
+
- Filters: minimum flow, top-k, share of origin/destination/total, cumulative share,
|
|
19
|
+
dominant flows.
|
|
20
|
+
- Indicators: degree and strength, supply- and demand-side self-containment, commuting
|
|
21
|
+
job ratio, net balance, zone and matrix summaries.
|
|
22
|
+
- Tao & Thill flow distance and dissimilarity, flow neighbour counts, agglomerative
|
|
23
|
+
(single/complete/average) and DBSCAN flow clustering, volume-weighted flow bundling.
|
|
24
|
+
- `intramax`: Masser & Brown Intramax hierarchy with optional contiguity, region
|
|
25
|
+
self-containment summary.
|
|
26
|
+
- `odflows` command-line interface.
|
|
27
|
+
- Optional pandas helpers `from_dataframe` / `to_dataframe`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "geospacephilo@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "odflows"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/odflows_sdk"
|
|
13
|
+
url: "https://pypi.org/project/odflows/"
|
|
14
|
+
abstract: "Origin-destination flow toolkit for mobility and commuting analysis: OD tables, desire lines with curved two-way lines, area-weighted re-aggregation between zone systems, net and gross flows, flow filtering, Tao and Thill flow distance with hierarchical and DBSCAN flow clustering, self-containment and job-ratio indicators, Intramax functional regions, and export to GeoJSON, flowmap.blue and kepler.gl."
|
|
15
|
+
keywords:
|
|
16
|
+
- origin-destination
|
|
17
|
+
- commuting
|
|
18
|
+
- mobility
|
|
19
|
+
- functional regions
|
|
20
|
+
- flow mapping
|
odflows-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
odflows-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odflows
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
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.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/odflows_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/odflows_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/odflows_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/odflows_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: origin-destination,od-matrix,flows,desire-lines,commuting,mobility,functional-regions,intramax,self-containment,flow-clustering,flowmap,kepler-gl,geojson,gis,transport-planning
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Environment :: Console
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Classifier: Intended Audience :: Science/Research
|
|
24
|
+
Classifier: Intended Audience :: Developers
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.21
|
|
30
|
+
Provides-Extra: pandas
|
|
31
|
+
Requires-Dist: pandas>=1.3.0; extra == "pandas"
|
|
32
|
+
Provides-Extra: geo
|
|
33
|
+
Requires-Dist: shapely>=2.0; extra == "geo"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# odflows
|
|
42
|
+
|
|
43
|
+
[](https://pypi.org/project/odflows/)
|
|
44
|
+
[](https://pypi.org/project/odflows/)
|
|
45
|
+
[](https://github.com/YusufEminoglu/odflows_sdk/actions/workflows/ci.yml)
|
|
46
|
+
[](https://github.com/YusufEminoglu/odflows_sdk/blob/main/LICENSE)
|
|
47
|
+
|
|
48
|
+
**Origin-destination flow toolkit for mobility and commuting analysis.**
|
|
49
|
+
|
|
50
|
+
`odflows` covers the usual steps of an OD study in one small package that needs only
|
|
51
|
+
numpy:
|
|
52
|
+
|
|
53
|
+
- **Read** OD tables in long form (`origin,destination,flow`) or as a square matrix, and
|
|
54
|
+
zone centroids from GeoJSON (points, or area centroids of polygons) or CSV.
|
|
55
|
+
- **Desire lines** as GeoJSON LineStrings: directed, net or gross; straight, **curved**
|
|
56
|
+
or **offset**, so both directions of a two-way pair stay visible.
|
|
57
|
+
- **Change zone systems**: aggregate and disaggregate with weighted lookup tables,
|
|
58
|
+
including area weights computed from two polygon layers (optional `shapely`).
|
|
59
|
+
- **Net and gross flows**, symmetric part, asymmetry index, intrazonal handling.
|
|
60
|
+
- **Filter** flows: minimum size, top-k, share of origin / destination / total,
|
|
61
|
+
cumulative share, dominant (largest outgoing) flows.
|
|
62
|
+
- **Cluster and bundle** flows with the Tao & Thill flow distance: agglomerative
|
|
63
|
+
(single / complete / average linkage) and DBSCAN, in pure numpy.
|
|
64
|
+
- **Indicators**: in/out degree and strength, supply- and demand-side
|
|
65
|
+
self-containment, commuting job ratio, net balance.
|
|
66
|
+
- **Functional regions** (commuting zones) with **Intramax** (Masser & Brown 1975),
|
|
67
|
+
optionally contiguity-constrained.
|
|
68
|
+
- **Export** to GeoJSON, **flowmap.blue** (`locations` + `flows` sheets) and a
|
|
69
|
+
**kepler.gl** arc-layer CSV.
|
|
70
|
+
- A command-line tool, `odflows`.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install odflows # numpy only
|
|
74
|
+
pip install "odflows[pandas]" # + DataFrame helpers
|
|
75
|
+
pip install "odflows[geo]" # + shapely, for area-weighted lookups from polygons
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Requires Python 3.9 or newer.
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from odflows import (
|
|
84
|
+
ODMatrix, read_od_csv, centroids_from_geojson, desire_lines, write_geojson,
|
|
85
|
+
zone_summary, intramax, region_summary, filter_top_k,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
od = read_od_csv("commute.csv") # origin,destination,flow
|
|
89
|
+
zones = centroids_from_geojson("zones.geojson", id_field="zone")
|
|
90
|
+
|
|
91
|
+
for row in zone_summary(od): # self-containment, job ratio, ...
|
|
92
|
+
print(row["zone"], round(row["supply_sc"], 3), round(row["job_ratio"], 2))
|
|
93
|
+
|
|
94
|
+
lines = desire_lines(filter_top_k(od, 20), zones, style="curved")
|
|
95
|
+
write_geojson(lines, "desire_lines.geojson")
|
|
96
|
+
|
|
97
|
+
hierarchy = intramax(od) # full Intramax hierarchy
|
|
98
|
+
membership = hierarchy.membership(n_regions=4) # cut it at 4 regions
|
|
99
|
+
print(region_summary(od, membership))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A runnable example with synthetic data is in
|
|
103
|
+
[`examples/quickstart.py`](examples/quickstart.py).
|
|
104
|
+
|
|
105
|
+
Matrices can also be built in memory:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
od = ODMatrix.from_long([("A", "B", 120), ("B", "A", 40), ("A", "A", 300)])
|
|
109
|
+
od = ODMatrix(["A", "B"], [[300, 120], [40, 0]]) # rows = origins, columns = destinations
|
|
110
|
+
od.net().to_long() # [('A', 'B', 80.0)]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Command line
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
odflows summary commute.csv -o zones.csv # per-zone indicators
|
|
117
|
+
odflows summary commute.csv --json # matrix statistics
|
|
118
|
+
odflows lines commute.csv --centroids zones.geojson --id-field zone \
|
|
119
|
+
--style curved --min-flow 50 -o lines.geojson
|
|
120
|
+
odflows filter commute.csv --min-share 0.1 --basis origin -o main_flows.csv
|
|
121
|
+
odflows transform commute.csv net -o net.csv # net | gross | symmetric | ...
|
|
122
|
+
odflows aggregate commute.csv --lookup lookup.csv -o districts.csv
|
|
123
|
+
odflows regions commute.csv -n 5 -o regions.csv --summary regions_sc.csv
|
|
124
|
+
odflows cluster commute.csv --centroids zones.geojson --id-field zone \
|
|
125
|
+
--n-clusters 8 -o bundles.geojson
|
|
126
|
+
odflows export commute.csv --centroids zones.geojson --id-field zone \
|
|
127
|
+
--format flowmap -o mymap # mymap-locations.csv, mymap-flows.csv
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Every command accepts `--matrix` for square-matrix input and `--origin-col`,
|
|
131
|
+
`--dest-col`, `--flow-col` for other column names. Centroids may be a GeoJSON file or a
|
|
132
|
+
CSV (`--id-field`, `--x-col`, `--y-col`).
|
|
133
|
+
|
|
134
|
+
## API overview
|
|
135
|
+
|
|
136
|
+
| Area | Functions / classes |
|
|
137
|
+
|---|---|
|
|
138
|
+
| Container | `ODMatrix` (`from_long`, `to_long`, `outflows`, `inflows`, `intrazonal`, `without_intrazonal`, `net`, `gross`, `symmetric`, `transpose`, `asymmetry_index`, `subset`) |
|
|
139
|
+
| Input / output | `read_od_csv`, `read_matrix_csv`, `write_od_csv`, `write_matrix_csv`, `centroids_from_geojson`, `read_centroids_csv`, `geometry_centroid`, `write_geojson` |
|
|
140
|
+
| Desire lines | `desire_lines`, `line_coordinates`, `haversine_km` |
|
|
141
|
+
| Zone systems | `ZoneLookup` (`from_records`, `from_mapping`, `from_csv`, `from_geojson`), `reaggregate`, `aggregate` |
|
|
142
|
+
| Filtering | `filter_min_flow`, `filter_top_k`, `filter_share`, `filter_cumulative`, `dominant_flows` |
|
|
143
|
+
| Indicators | `degree`, `self_containment`, `job_ratio`, `net_balance`, `zone_summary`, `matrix_summary` |
|
|
144
|
+
| Clustering | `FlowSet`, `flow_distance_matrix`, `flow_neighbour_counts`, `hierarchical_clusters`, `flow_dbscan`, `bundle_flows` |
|
|
145
|
+
| Regions | `intramax`, `IntramaxResult`, `region_summary` |
|
|
146
|
+
| Visualisation export | `write_flowmap_blue`, `write_kepler_csv` |
|
|
147
|
+
| pandas | `from_dataframe`, `to_dataframe` |
|
|
148
|
+
|
|
149
|
+
All filters and transformations return a new `ODMatrix`, so they chain:
|
|
150
|
+
`filter_top_k(filter_share(od, 0.1), 50).without_intrazonal()`.
|
|
151
|
+
|
|
152
|
+
## Methods and sources
|
|
153
|
+
|
|
154
|
+
**Conventions.** `T[i, j]` is the flow from zone `i` to zone `j`. For commuting, rows
|
|
155
|
+
are places of residence and columns places of work, so `O_i = Σ_j T_ij` are resident
|
|
156
|
+
workers and `D_j = Σ_i T_ij` are jobs; both include intrazonal flows `T_ii`.
|
|
157
|
+
|
|
158
|
+
**Net and gross flows.** Gross `G_ij = T_ij + T_ji`; net `N_ij = max(T_ij − T_ji, 0)`.
|
|
159
|
+
The asymmetry index is `Σ_{i<j} |T_ij − T_ji| / Σ_{i<j} (T_ij + T_ji)`.
|
|
160
|
+
|
|
161
|
+
**Desire lines.** Curved lines are quadratic Bézier curves whose control point is the
|
|
162
|
+
midpoint moved to the right of the direction of travel by `bend × length`; offset lines
|
|
163
|
+
are the segment shifted to the right by the same amount. A→B and B→A therefore fall on
|
|
164
|
+
opposite sides. Lengths are haversine kilometres for longitude/latitude centroids
|
|
165
|
+
(mean Earth radius 6371.0088 km) and planar units otherwise.
|
|
166
|
+
|
|
167
|
+
**Polygon centroids** use the shoelace (area-weighted) formula on the exterior ring
|
|
168
|
+
minus holes, treating coordinates as planar.
|
|
169
|
+
|
|
170
|
+
**Zone re-aggregation.** With a row-normalised lookup `W` (`W[s, t]` = share of source
|
|
171
|
+
zone `s` in target zone `t`), `T' = Wᵀ T W`. Flows are split assuming origins and
|
|
172
|
+
destinations are spread within each source zone in proportion to the weights - areal
|
|
173
|
+
weighting (Goodchild & Lam 1980). Different origin and destination lookups (e.g.
|
|
174
|
+
population vs. employment weights) are supported; totals are preserved.
|
|
175
|
+
|
|
176
|
+
**Filtering.** `filter_share(basis="origin")` keeps `T_ij / O_i ≥ s`;
|
|
177
|
+
`dominant_flows` keeps each origin's largest outgoing flow, the first step of nodal
|
|
178
|
+
region analysis (Nystuen & Dacey 1961).
|
|
179
|
+
|
|
180
|
+
**Flow distance.** Tao & Thill (2016) define, for flows `i` and `j` with origin
|
|
181
|
+
distance `d_O` and destination distance `d_D`,
|
|
182
|
+
`FD = sqrt(α d_O² + β d_D²)` with `α + β = 2`, and the length-normalised
|
|
183
|
+
dissimilarity `FDS = sqrt((α d_O² + β d_D²) / (L_i L_j))`. Coordinates must be
|
|
184
|
+
projected. `flow_neighbour_counts` gives the local counting term of their flow
|
|
185
|
+
K-function (no edge correction or significance test).
|
|
186
|
+
|
|
187
|
+
**Clustering.** Agglomerative clustering uses the Lance-Williams (1967) update for
|
|
188
|
+
single, complete and average (UPGMA) linkage; DBSCAN follows Ester et al. (1996), with
|
|
189
|
+
an option to count flow volume instead of flow numbers. `bundle_flows` replaces every
|
|
190
|
+
cluster by the flow from its volume-weighted mean origin to its volume-weighted mean
|
|
191
|
+
destination.
|
|
192
|
+
|
|
193
|
+
**Self-containment.** Supply-side `SC_s = T_ii / O_i` (share of resident workers who
|
|
194
|
+
work locally) and demand-side `SC_d = T_ii / D_i` (share of local jobs filled by
|
|
195
|
+
residents), as used for local labour market areas (Goodman 1970; Casado-Díaz 2000).
|
|
196
|
+
The commuting job ratio is `D_i / O_i`.
|
|
197
|
+
|
|
198
|
+
**Intramax.** Hierarchical aggregation (Masser & Brown 1975) that repeatedly merges
|
|
199
|
+
the pair of current regions maximising `T_ij / (O_i D_j) + T_ji / (O_j D_i)` on the
|
|
200
|
+
aggregated matrix; merged flows become intrazonal. The full hierarchy is stored and can
|
|
201
|
+
be cut at any number of regions. An optional contiguity constraint only merges adjacent
|
|
202
|
+
regions (Drobne & Lakner 2016). This is Intramax, not a modularity / Louvain method.
|
|
203
|
+
|
|
204
|
+
**Export formats.** flowmap.blue reads a Google Sheet with a `locations` sheet
|
|
205
|
+
(`id,name,lat,lon`) and a `flows` sheet (`origin,dest,count`); `write_flowmap_blue`
|
|
206
|
+
writes one CSV per sheet. `write_kepler_csv` writes `origin_lat/origin_lng` and
|
|
207
|
+
`dest_lat/dest_lng` column pairs, which kepler.gl detects as point fields for its arc
|
|
208
|
+
and line layers.
|
|
209
|
+
|
|
210
|
+
### References
|
|
211
|
+
|
|
212
|
+
- Casado-Díaz, J. M. (2000). Local labour market areas in Spain: a case study. *Regional Studies*, 34(9), 843-856.
|
|
213
|
+
- Drobne, S., & Lakner, M. (2016). Use of constraints in the hierarchical aggregation procedure Intramax. *Business Systems Research*, 7(2), 5-22.
|
|
214
|
+
- 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.
|
|
215
|
+
- Goodchild, M. F., & Lam, N. S.-N. (1980). Areal interpolation: a variant of the traditional spatial problem. *Geo-Processing*, 1, 297-312.
|
|
216
|
+
- 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.
|
|
217
|
+
- Lance, G. N., & Williams, W. T. (1967). A general theory of classificatory sorting strategies: 1. Hierarchical systems. *The Computer Journal*, 9(4), 373-380.
|
|
218
|
+
- Masser, I., & Brown, P. J. B. (1975). Hierarchical aggregation procedures for interaction data. *Environment and Planning A*, 7(5), 509-523.
|
|
219
|
+
- Nystuen, J. D., & Dacey, M. F. (1961). A graph theory interpretation of nodal regions. *Papers of the Regional Science Association*, 7, 29-42.
|
|
220
|
+
- Tao, R., & Thill, J.-C. (2016). Spatial cluster detection in spatial flow data. *Geographical Analysis*, 48(4), 355-372.
|
|
221
|
+
|
|
222
|
+
## Limitations
|
|
223
|
+
|
|
224
|
+
- Centroids, Bézier offsets, polygon centroids and the flow distance are planar. Use a
|
|
225
|
+
projected CRS for clustering and for exact curve shapes; `desire_lines` guesses
|
|
226
|
+
longitude/latitude from coordinate ranges (override with `geographic=`), which can
|
|
227
|
+
misfire for small projected coordinates.
|
|
228
|
+
- Re-aggregation assumes uniform spreading within source zones (areal weighting).
|
|
229
|
+
Use population or employment weights where you have them.
|
|
230
|
+
- Intramax and agglomerative clustering are O(n³); fine for hundreds of zones and a few
|
|
231
|
+
thousand flows, not for full national matrices. Pairs with no interaction are never
|
|
232
|
+
merged, so Intramax may stop before reaching the requested number of regions.
|
|
233
|
+
- Flow statistics stop at the flow distance, dissimilarity and neighbour counts: the
|
|
234
|
+
flow K-function with edge correction, and significance testing, are not implemented.
|
|
235
|
+
- No travel-to-work-area (TTWA) thresholds are built in; check region self-containment
|
|
236
|
+
against the criteria of your own study.
|
|
237
|
+
|
|
238
|
+
## Academic citation
|
|
239
|
+
|
|
240
|
+
If you use `odflows` in research, please cite it:
|
|
241
|
+
|
|
242
|
+
```bibtex
|
|
243
|
+
@software{eminoglu_odflows,
|
|
244
|
+
author = {Eminoğlu, Yusuf},
|
|
245
|
+
title = {odflows},
|
|
246
|
+
year = {2026},
|
|
247
|
+
version = {0.1.0},
|
|
248
|
+
url = {https://github.com/YusufEminoglu/odflows_sdk}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
See also [`CITATION.cff`](CITATION.cff).
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT © 2026 Yusuf Eminoğlu. Contact: geospacephilo@gmail.com
|
odflows-0.1.0/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# odflows
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/odflows/)
|
|
4
|
+
[](https://pypi.org/project/odflows/)
|
|
5
|
+
[](https://github.com/YusufEminoglu/odflows_sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/YusufEminoglu/odflows_sdk/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Origin-destination flow toolkit for mobility and commuting analysis.**
|
|
9
|
+
|
|
10
|
+
`odflows` covers the usual steps of an OD study in one small package that needs only
|
|
11
|
+
numpy:
|
|
12
|
+
|
|
13
|
+
- **Read** OD tables in long form (`origin,destination,flow`) or as a square matrix, and
|
|
14
|
+
zone centroids from GeoJSON (points, or area centroids of polygons) or CSV.
|
|
15
|
+
- **Desire lines** as GeoJSON LineStrings: directed, net or gross; straight, **curved**
|
|
16
|
+
or **offset**, so both directions of a two-way pair stay visible.
|
|
17
|
+
- **Change zone systems**: aggregate and disaggregate with weighted lookup tables,
|
|
18
|
+
including area weights computed from two polygon layers (optional `shapely`).
|
|
19
|
+
- **Net and gross flows**, symmetric part, asymmetry index, intrazonal handling.
|
|
20
|
+
- **Filter** flows: minimum size, top-k, share of origin / destination / total,
|
|
21
|
+
cumulative share, dominant (largest outgoing) flows.
|
|
22
|
+
- **Cluster and bundle** flows with the Tao & Thill flow distance: agglomerative
|
|
23
|
+
(single / complete / average linkage) and DBSCAN, in pure numpy.
|
|
24
|
+
- **Indicators**: in/out degree and strength, supply- and demand-side
|
|
25
|
+
self-containment, commuting job ratio, net balance.
|
|
26
|
+
- **Functional regions** (commuting zones) with **Intramax** (Masser & Brown 1975),
|
|
27
|
+
optionally contiguity-constrained.
|
|
28
|
+
- **Export** to GeoJSON, **flowmap.blue** (`locations` + `flows` sheets) and a
|
|
29
|
+
**kepler.gl** arc-layer CSV.
|
|
30
|
+
- A command-line tool, `odflows`.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install odflows # numpy only
|
|
34
|
+
pip install "odflows[pandas]" # + DataFrame helpers
|
|
35
|
+
pip install "odflows[geo]" # + shapely, for area-weighted lookups from polygons
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.9 or newer.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from odflows import (
|
|
44
|
+
ODMatrix, read_od_csv, centroids_from_geojson, desire_lines, write_geojson,
|
|
45
|
+
zone_summary, intramax, region_summary, filter_top_k,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
od = read_od_csv("commute.csv") # origin,destination,flow
|
|
49
|
+
zones = centroids_from_geojson("zones.geojson", id_field="zone")
|
|
50
|
+
|
|
51
|
+
for row in zone_summary(od): # self-containment, job ratio, ...
|
|
52
|
+
print(row["zone"], round(row["supply_sc"], 3), round(row["job_ratio"], 2))
|
|
53
|
+
|
|
54
|
+
lines = desire_lines(filter_top_k(od, 20), zones, style="curved")
|
|
55
|
+
write_geojson(lines, "desire_lines.geojson")
|
|
56
|
+
|
|
57
|
+
hierarchy = intramax(od) # full Intramax hierarchy
|
|
58
|
+
membership = hierarchy.membership(n_regions=4) # cut it at 4 regions
|
|
59
|
+
print(region_summary(od, membership))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A runnable example with synthetic data is in
|
|
63
|
+
[`examples/quickstart.py`](examples/quickstart.py).
|
|
64
|
+
|
|
65
|
+
Matrices can also be built in memory:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
od = ODMatrix.from_long([("A", "B", 120), ("B", "A", 40), ("A", "A", 300)])
|
|
69
|
+
od = ODMatrix(["A", "B"], [[300, 120], [40, 0]]) # rows = origins, columns = destinations
|
|
70
|
+
od.net().to_long() # [('A', 'B', 80.0)]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Command line
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
odflows summary commute.csv -o zones.csv # per-zone indicators
|
|
77
|
+
odflows summary commute.csv --json # matrix statistics
|
|
78
|
+
odflows lines commute.csv --centroids zones.geojson --id-field zone \
|
|
79
|
+
--style curved --min-flow 50 -o lines.geojson
|
|
80
|
+
odflows filter commute.csv --min-share 0.1 --basis origin -o main_flows.csv
|
|
81
|
+
odflows transform commute.csv net -o net.csv # net | gross | symmetric | ...
|
|
82
|
+
odflows aggregate commute.csv --lookup lookup.csv -o districts.csv
|
|
83
|
+
odflows regions commute.csv -n 5 -o regions.csv --summary regions_sc.csv
|
|
84
|
+
odflows cluster commute.csv --centroids zones.geojson --id-field zone \
|
|
85
|
+
--n-clusters 8 -o bundles.geojson
|
|
86
|
+
odflows export commute.csv --centroids zones.geojson --id-field zone \
|
|
87
|
+
--format flowmap -o mymap # mymap-locations.csv, mymap-flows.csv
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Every command accepts `--matrix` for square-matrix input and `--origin-col`,
|
|
91
|
+
`--dest-col`, `--flow-col` for other column names. Centroids may be a GeoJSON file or a
|
|
92
|
+
CSV (`--id-field`, `--x-col`, `--y-col`).
|
|
93
|
+
|
|
94
|
+
## API overview
|
|
95
|
+
|
|
96
|
+
| Area | Functions / classes |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Container | `ODMatrix` (`from_long`, `to_long`, `outflows`, `inflows`, `intrazonal`, `without_intrazonal`, `net`, `gross`, `symmetric`, `transpose`, `asymmetry_index`, `subset`) |
|
|
99
|
+
| Input / output | `read_od_csv`, `read_matrix_csv`, `write_od_csv`, `write_matrix_csv`, `centroids_from_geojson`, `read_centroids_csv`, `geometry_centroid`, `write_geojson` |
|
|
100
|
+
| Desire lines | `desire_lines`, `line_coordinates`, `haversine_km` |
|
|
101
|
+
| Zone systems | `ZoneLookup` (`from_records`, `from_mapping`, `from_csv`, `from_geojson`), `reaggregate`, `aggregate` |
|
|
102
|
+
| Filtering | `filter_min_flow`, `filter_top_k`, `filter_share`, `filter_cumulative`, `dominant_flows` |
|
|
103
|
+
| Indicators | `degree`, `self_containment`, `job_ratio`, `net_balance`, `zone_summary`, `matrix_summary` |
|
|
104
|
+
| Clustering | `FlowSet`, `flow_distance_matrix`, `flow_neighbour_counts`, `hierarchical_clusters`, `flow_dbscan`, `bundle_flows` |
|
|
105
|
+
| Regions | `intramax`, `IntramaxResult`, `region_summary` |
|
|
106
|
+
| Visualisation export | `write_flowmap_blue`, `write_kepler_csv` |
|
|
107
|
+
| pandas | `from_dataframe`, `to_dataframe` |
|
|
108
|
+
|
|
109
|
+
All filters and transformations return a new `ODMatrix`, so they chain:
|
|
110
|
+
`filter_top_k(filter_share(od, 0.1), 50).without_intrazonal()`.
|
|
111
|
+
|
|
112
|
+
## Methods and sources
|
|
113
|
+
|
|
114
|
+
**Conventions.** `T[i, j]` is the flow from zone `i` to zone `j`. For commuting, rows
|
|
115
|
+
are places of residence and columns places of work, so `O_i = Σ_j T_ij` are resident
|
|
116
|
+
workers and `D_j = Σ_i T_ij` are jobs; both include intrazonal flows `T_ii`.
|
|
117
|
+
|
|
118
|
+
**Net and gross flows.** Gross `G_ij = T_ij + T_ji`; net `N_ij = max(T_ij − T_ji, 0)`.
|
|
119
|
+
The asymmetry index is `Σ_{i<j} |T_ij − T_ji| / Σ_{i<j} (T_ij + T_ji)`.
|
|
120
|
+
|
|
121
|
+
**Desire lines.** Curved lines are quadratic Bézier curves whose control point is the
|
|
122
|
+
midpoint moved to the right of the direction of travel by `bend × length`; offset lines
|
|
123
|
+
are the segment shifted to the right by the same amount. A→B and B→A therefore fall on
|
|
124
|
+
opposite sides. Lengths are haversine kilometres for longitude/latitude centroids
|
|
125
|
+
(mean Earth radius 6371.0088 km) and planar units otherwise.
|
|
126
|
+
|
|
127
|
+
**Polygon centroids** use the shoelace (area-weighted) formula on the exterior ring
|
|
128
|
+
minus holes, treating coordinates as planar.
|
|
129
|
+
|
|
130
|
+
**Zone re-aggregation.** With a row-normalised lookup `W` (`W[s, t]` = share of source
|
|
131
|
+
zone `s` in target zone `t`), `T' = Wᵀ T W`. Flows are split assuming origins and
|
|
132
|
+
destinations are spread within each source zone in proportion to the weights - areal
|
|
133
|
+
weighting (Goodchild & Lam 1980). Different origin and destination lookups (e.g.
|
|
134
|
+
population vs. employment weights) are supported; totals are preserved.
|
|
135
|
+
|
|
136
|
+
**Filtering.** `filter_share(basis="origin")` keeps `T_ij / O_i ≥ s`;
|
|
137
|
+
`dominant_flows` keeps each origin's largest outgoing flow, the first step of nodal
|
|
138
|
+
region analysis (Nystuen & Dacey 1961).
|
|
139
|
+
|
|
140
|
+
**Flow distance.** Tao & Thill (2016) define, for flows `i` and `j` with origin
|
|
141
|
+
distance `d_O` and destination distance `d_D`,
|
|
142
|
+
`FD = sqrt(α d_O² + β d_D²)` with `α + β = 2`, and the length-normalised
|
|
143
|
+
dissimilarity `FDS = sqrt((α d_O² + β d_D²) / (L_i L_j))`. Coordinates must be
|
|
144
|
+
projected. `flow_neighbour_counts` gives the local counting term of their flow
|
|
145
|
+
K-function (no edge correction or significance test).
|
|
146
|
+
|
|
147
|
+
**Clustering.** Agglomerative clustering uses the Lance-Williams (1967) update for
|
|
148
|
+
single, complete and average (UPGMA) linkage; DBSCAN follows Ester et al. (1996), with
|
|
149
|
+
an option to count flow volume instead of flow numbers. `bundle_flows` replaces every
|
|
150
|
+
cluster by the flow from its volume-weighted mean origin to its volume-weighted mean
|
|
151
|
+
destination.
|
|
152
|
+
|
|
153
|
+
**Self-containment.** Supply-side `SC_s = T_ii / O_i` (share of resident workers who
|
|
154
|
+
work locally) and demand-side `SC_d = T_ii / D_i` (share of local jobs filled by
|
|
155
|
+
residents), as used for local labour market areas (Goodman 1970; Casado-Díaz 2000).
|
|
156
|
+
The commuting job ratio is `D_i / O_i`.
|
|
157
|
+
|
|
158
|
+
**Intramax.** Hierarchical aggregation (Masser & Brown 1975) that repeatedly merges
|
|
159
|
+
the pair of current regions maximising `T_ij / (O_i D_j) + T_ji / (O_j D_i)` on the
|
|
160
|
+
aggregated matrix; merged flows become intrazonal. The full hierarchy is stored and can
|
|
161
|
+
be cut at any number of regions. An optional contiguity constraint only merges adjacent
|
|
162
|
+
regions (Drobne & Lakner 2016). This is Intramax, not a modularity / Louvain method.
|
|
163
|
+
|
|
164
|
+
**Export formats.** flowmap.blue reads a Google Sheet with a `locations` sheet
|
|
165
|
+
(`id,name,lat,lon`) and a `flows` sheet (`origin,dest,count`); `write_flowmap_blue`
|
|
166
|
+
writes one CSV per sheet. `write_kepler_csv` writes `origin_lat/origin_lng` and
|
|
167
|
+
`dest_lat/dest_lng` column pairs, which kepler.gl detects as point fields for its arc
|
|
168
|
+
and line layers.
|
|
169
|
+
|
|
170
|
+
### References
|
|
171
|
+
|
|
172
|
+
- Casado-Díaz, J. M. (2000). Local labour market areas in Spain: a case study. *Regional Studies*, 34(9), 843-856.
|
|
173
|
+
- Drobne, S., & Lakner, M. (2016). Use of constraints in the hierarchical aggregation procedure Intramax. *Business Systems Research*, 7(2), 5-22.
|
|
174
|
+
- 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.
|
|
175
|
+
- Goodchild, M. F., & Lam, N. S.-N. (1980). Areal interpolation: a variant of the traditional spatial problem. *Geo-Processing*, 1, 297-312.
|
|
176
|
+
- 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.
|
|
177
|
+
- Lance, G. N., & Williams, W. T. (1967). A general theory of classificatory sorting strategies: 1. Hierarchical systems. *The Computer Journal*, 9(4), 373-380.
|
|
178
|
+
- Masser, I., & Brown, P. J. B. (1975). Hierarchical aggregation procedures for interaction data. *Environment and Planning A*, 7(5), 509-523.
|
|
179
|
+
- Nystuen, J. D., & Dacey, M. F. (1961). A graph theory interpretation of nodal regions. *Papers of the Regional Science Association*, 7, 29-42.
|
|
180
|
+
- Tao, R., & Thill, J.-C. (2016). Spatial cluster detection in spatial flow data. *Geographical Analysis*, 48(4), 355-372.
|
|
181
|
+
|
|
182
|
+
## Limitations
|
|
183
|
+
|
|
184
|
+
- Centroids, Bézier offsets, polygon centroids and the flow distance are planar. Use a
|
|
185
|
+
projected CRS for clustering and for exact curve shapes; `desire_lines` guesses
|
|
186
|
+
longitude/latitude from coordinate ranges (override with `geographic=`), which can
|
|
187
|
+
misfire for small projected coordinates.
|
|
188
|
+
- Re-aggregation assumes uniform spreading within source zones (areal weighting).
|
|
189
|
+
Use population or employment weights where you have them.
|
|
190
|
+
- Intramax and agglomerative clustering are O(n³); fine for hundreds of zones and a few
|
|
191
|
+
thousand flows, not for full national matrices. Pairs with no interaction are never
|
|
192
|
+
merged, so Intramax may stop before reaching the requested number of regions.
|
|
193
|
+
- Flow statistics stop at the flow distance, dissimilarity and neighbour counts: the
|
|
194
|
+
flow K-function with edge correction, and significance testing, are not implemented.
|
|
195
|
+
- No travel-to-work-area (TTWA) thresholds are built in; check region self-containment
|
|
196
|
+
against the criteria of your own study.
|
|
197
|
+
|
|
198
|
+
## Academic citation
|
|
199
|
+
|
|
200
|
+
If you use `odflows` in research, please cite it:
|
|
201
|
+
|
|
202
|
+
```bibtex
|
|
203
|
+
@software{eminoglu_odflows,
|
|
204
|
+
author = {Eminoğlu, Yusuf},
|
|
205
|
+
title = {odflows},
|
|
206
|
+
year = {2026},
|
|
207
|
+
version = {0.1.0},
|
|
208
|
+
url = {https://github.com/YusufEminoglu/odflows_sdk}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
See also [`CITATION.cff`](CITATION.cff).
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
MIT © 2026 Yusuf Eminoğlu. Contact: geospacephilo@gmail.com
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
origin,destination,flow
|
|
2
|
+
Centre,Centre,900
|
|
3
|
+
Centre,North,60
|
|
4
|
+
Centre,East,80
|
|
5
|
+
Centre,South,40
|
|
6
|
+
Centre,West,30
|
|
7
|
+
North,Centre,420
|
|
8
|
+
North,North,300
|
|
9
|
+
North,East,50
|
|
10
|
+
North,West,20
|
|
11
|
+
East,Centre,380
|
|
12
|
+
East,North,40
|
|
13
|
+
East,East,350
|
|
14
|
+
East,South,25
|
|
15
|
+
South,Centre,260
|
|
16
|
+
South,East,30
|
|
17
|
+
South,South,280
|
|
18
|
+
South,West,90
|
|
19
|
+
West,Centre,210
|
|
20
|
+
West,South,110
|
|
21
|
+
West,West,240
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""odflows quick start on a small synthetic commuting matrix.
|
|
2
|
+
|
|
3
|
+
python examples/quickstart.py
|
|
4
|
+
|
|
5
|
+
Reads ``commute.csv`` (rows = residence, columns = workplace) and ``zones.geojson``
|
|
6
|
+
(five fictional square zones in projected metres), prints zone indicators and an
|
|
7
|
+
Intramax regionalisation, and writes curved desire lines and flow bundles to
|
|
8
|
+
``examples/out/``. All data are synthetic.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from odflows import (
|
|
16
|
+
FlowSet,
|
|
17
|
+
bundle_flows,
|
|
18
|
+
centroids_from_geojson,
|
|
19
|
+
desire_lines,
|
|
20
|
+
filter_share,
|
|
21
|
+
flow_distance_matrix,
|
|
22
|
+
hierarchical_clusters,
|
|
23
|
+
intramax,
|
|
24
|
+
matrix_summary,
|
|
25
|
+
read_od_csv,
|
|
26
|
+
region_summary,
|
|
27
|
+
write_geojson,
|
|
28
|
+
zone_summary,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
HERE = Path(__file__).parent
|
|
32
|
+
OUT = HERE / "out"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main() -> None:
|
|
36
|
+
od = read_od_csv(HERE / "commute.csv")
|
|
37
|
+
centroids = centroids_from_geojson(HERE / "zones.geojson", id_field="zone")
|
|
38
|
+
print(od)
|
|
39
|
+
print({k: round(v, 3) for k, v in matrix_summary(od).items()})
|
|
40
|
+
|
|
41
|
+
print("\nzone SC(supply) SC(demand) job ratio")
|
|
42
|
+
for row in zone_summary(od):
|
|
43
|
+
print(
|
|
44
|
+
f"{row['zone']:<10} {row['supply_sc']:>10.3f} {row['demand_sc']:>10.3f}"
|
|
45
|
+
f" {row['job_ratio']:>9.3f}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
hierarchy = intramax(od)
|
|
49
|
+
for step in hierarchy.steps:
|
|
50
|
+
print(f"Intramax: {step.absorbed} -> {step.region} (score {step.score:.5f})")
|
|
51
|
+
membership = hierarchy.membership(2)
|
|
52
|
+
for row in region_summary(od, membership):
|
|
53
|
+
print(f"region {row['region']}: {row['n_zones']} zones, supply SC {row['supply_sc']:.3f}")
|
|
54
|
+
|
|
55
|
+
OUT.mkdir(exist_ok=True)
|
|
56
|
+
main_flows = filter_share(od, 0.05) # flows >= 5 % of their origin's outflow
|
|
57
|
+
lines = desire_lines(main_flows, centroids, style="curved", geographic=False)
|
|
58
|
+
write_geojson(lines, OUT / "desire_lines.geojson")
|
|
59
|
+
|
|
60
|
+
flows = FlowSet.from_od(od, centroids)
|
|
61
|
+
labels = hierarchical_clusters(flow_distance_matrix(flows), threshold=8000.0)
|
|
62
|
+
write_geojson(bundle_flows(flows, labels), OUT / "bundles.geojson")
|
|
63
|
+
print(f"\n{len(lines['features'])} desire lines, {labels.max() + 1} flow bundles -> {OUT}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main()
|