odflows 0.1.0__py3-none-any.whl
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/__init__.py +102 -0
- odflows/__main__.py +7 -0
- odflows/_version.py +1 -0
- odflows/cli.py +340 -0
- odflows/clustering.py +286 -0
- odflows/errors.py +11 -0
- odflows/filtering.py +131 -0
- odflows/indicators.py +119 -0
- odflows/io.py +326 -0
- odflows/lines.py +163 -0
- odflows/matrix.py +245 -0
- odflows/pandas_ext.py +43 -0
- odflows/py.typed +0 -0
- odflows/regions.py +178 -0
- odflows/zones.py +198 -0
- odflows-0.1.0.dist-info/METADATA +256 -0
- odflows-0.1.0.dist-info/RECORD +21 -0
- odflows-0.1.0.dist-info/WHEEL +5 -0
- odflows-0.1.0.dist-info/entry_points.txt +2 -0
- odflows-0.1.0.dist-info/licenses/LICENSE +21 -0
- odflows-0.1.0.dist-info/top_level.txt +1 -0
odflows/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""odflows: origin-destination flow toolkit for mobility and commuting analysis.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from odflows import ODMatrix, desire_lines, intramax, zone_summary
|
|
6
|
+
|
|
7
|
+
od = ODMatrix.from_long([("A", "B", 120), ("B", "A", 40), ("A", "A", 300)])
|
|
8
|
+
print(zone_summary(od))
|
|
9
|
+
fc = desire_lines(od, {"A": (0.0, 0.0), "B": (1000.0, 0.0)}, style="curved")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
from .clustering import (
|
|
14
|
+
FlowSet,
|
|
15
|
+
bundle_flows,
|
|
16
|
+
flow_dbscan,
|
|
17
|
+
flow_distance_matrix,
|
|
18
|
+
flow_neighbour_counts,
|
|
19
|
+
hierarchical_clusters,
|
|
20
|
+
)
|
|
21
|
+
from .errors import MissingDependencyError, ODFlowsError
|
|
22
|
+
from .filtering import (
|
|
23
|
+
dominant_flows,
|
|
24
|
+
filter_cumulative,
|
|
25
|
+
filter_min_flow,
|
|
26
|
+
filter_share,
|
|
27
|
+
filter_top_k,
|
|
28
|
+
)
|
|
29
|
+
from .indicators import (
|
|
30
|
+
degree,
|
|
31
|
+
job_ratio,
|
|
32
|
+
matrix_summary,
|
|
33
|
+
net_balance,
|
|
34
|
+
self_containment,
|
|
35
|
+
zone_summary,
|
|
36
|
+
)
|
|
37
|
+
from .io import (
|
|
38
|
+
centroids_from_geojson,
|
|
39
|
+
geometry_centroid,
|
|
40
|
+
load_geojson,
|
|
41
|
+
read_centroids_csv,
|
|
42
|
+
read_matrix_csv,
|
|
43
|
+
read_od_csv,
|
|
44
|
+
write_flowmap_blue,
|
|
45
|
+
write_geojson,
|
|
46
|
+
write_kepler_csv,
|
|
47
|
+
write_matrix_csv,
|
|
48
|
+
write_od_csv,
|
|
49
|
+
)
|
|
50
|
+
from .lines import desire_lines, euclidean, haversine_km, line_coordinates
|
|
51
|
+
from .matrix import ODMatrix
|
|
52
|
+
from .pandas_ext import from_dataframe, to_dataframe
|
|
53
|
+
from .regions import IntramaxResult, MergeStep, intramax, region_summary
|
|
54
|
+
from .zones import ZoneLookup, aggregate, reaggregate
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"FlowSet",
|
|
58
|
+
"IntramaxResult",
|
|
59
|
+
"MergeStep",
|
|
60
|
+
"MissingDependencyError",
|
|
61
|
+
"ODFlowsError",
|
|
62
|
+
"ODMatrix",
|
|
63
|
+
"ZoneLookup",
|
|
64
|
+
"__version__",
|
|
65
|
+
"aggregate",
|
|
66
|
+
"bundle_flows",
|
|
67
|
+
"centroids_from_geojson",
|
|
68
|
+
"degree",
|
|
69
|
+
"desire_lines",
|
|
70
|
+
"dominant_flows",
|
|
71
|
+
"euclidean",
|
|
72
|
+
"filter_cumulative",
|
|
73
|
+
"filter_min_flow",
|
|
74
|
+
"filter_share",
|
|
75
|
+
"filter_top_k",
|
|
76
|
+
"flow_dbscan",
|
|
77
|
+
"flow_distance_matrix",
|
|
78
|
+
"flow_neighbour_counts",
|
|
79
|
+
"from_dataframe",
|
|
80
|
+
"geometry_centroid",
|
|
81
|
+
"haversine_km",
|
|
82
|
+
"hierarchical_clusters",
|
|
83
|
+
"intramax",
|
|
84
|
+
"job_ratio",
|
|
85
|
+
"line_coordinates",
|
|
86
|
+
"load_geojson",
|
|
87
|
+
"matrix_summary",
|
|
88
|
+
"net_balance",
|
|
89
|
+
"read_centroids_csv",
|
|
90
|
+
"read_matrix_csv",
|
|
91
|
+
"read_od_csv",
|
|
92
|
+
"reaggregate",
|
|
93
|
+
"region_summary",
|
|
94
|
+
"self_containment",
|
|
95
|
+
"to_dataframe",
|
|
96
|
+
"write_flowmap_blue",
|
|
97
|
+
"write_geojson",
|
|
98
|
+
"write_kepler_csv",
|
|
99
|
+
"write_matrix_csv",
|
|
100
|
+
"write_od_csv",
|
|
101
|
+
"zone_summary",
|
|
102
|
+
]
|
odflows/__main__.py
ADDED
odflows/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
odflows/cli.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Command-line interface: ``odflows summary | lines | filter | transform | aggregate |
|
|
2
|
+
regions | cluster | export``."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import csv
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from ._version import __version__
|
|
14
|
+
from .clustering import (
|
|
15
|
+
FlowSet,
|
|
16
|
+
bundle_flows,
|
|
17
|
+
flow_dbscan,
|
|
18
|
+
flow_distance_matrix,
|
|
19
|
+
hierarchical_clusters,
|
|
20
|
+
)
|
|
21
|
+
from .errors import MissingDependencyError, ODFlowsError
|
|
22
|
+
from .filtering import filter_cumulative, filter_min_flow, filter_share, filter_top_k
|
|
23
|
+
from .indicators import matrix_summary, zone_summary
|
|
24
|
+
from .io import (
|
|
25
|
+
Centroids,
|
|
26
|
+
centroids_from_geojson,
|
|
27
|
+
read_centroids_csv,
|
|
28
|
+
read_matrix_csv,
|
|
29
|
+
read_od_csv,
|
|
30
|
+
write_flowmap_blue,
|
|
31
|
+
write_geojson,
|
|
32
|
+
write_kepler_csv,
|
|
33
|
+
write_matrix_csv,
|
|
34
|
+
write_od_csv,
|
|
35
|
+
)
|
|
36
|
+
from .lines import desire_lines
|
|
37
|
+
from .matrix import ODMatrix
|
|
38
|
+
from .regions import intramax, region_summary
|
|
39
|
+
from .zones import ZoneLookup, reaggregate
|
|
40
|
+
|
|
41
|
+
EXIT_OK = 0
|
|
42
|
+
EXIT_ERROR = 1
|
|
43
|
+
EXIT_USAGE = 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _add_input(p: argparse.ArgumentParser) -> None:
|
|
47
|
+
p.add_argument("input", help="OD table (CSV, long form unless --matrix)")
|
|
48
|
+
p.add_argument("--matrix", action="store_true", help="input is a square matrix CSV")
|
|
49
|
+
p.add_argument("--origin-col", default="origin", help="origin column (long form)")
|
|
50
|
+
p.add_argument("--dest-col", default="destination", help="destination column (long form)")
|
|
51
|
+
p.add_argument("--flow-col", default="flow", help="flow column (long form)")
|
|
52
|
+
p.add_argument("--delimiter", default=None, help="CSV delimiter (default: sniffed)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _add_centroids(p: argparse.ArgumentParser) -> None:
|
|
56
|
+
p.add_argument(
|
|
57
|
+
"--centroids", required=True, help="zones as GeoJSON (points or polygons) or CSV"
|
|
58
|
+
)
|
|
59
|
+
p.add_argument("--id-field", default=None, help="zone id property / CSV column")
|
|
60
|
+
p.add_argument("--x-col", default="lon", help="x column of a centroid CSV")
|
|
61
|
+
p.add_argument("--y-col", default="lat", help="y column of a centroid CSV")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _read_od(args: argparse.Namespace) -> ODMatrix:
|
|
65
|
+
if args.matrix:
|
|
66
|
+
return read_matrix_csv(args.input, delimiter=args.delimiter)
|
|
67
|
+
return read_od_csv(
|
|
68
|
+
args.input,
|
|
69
|
+
origin=args.origin_col,
|
|
70
|
+
destination=args.dest_col,
|
|
71
|
+
flow=args.flow_col,
|
|
72
|
+
delimiter=args.delimiter,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_centroids(args: argparse.Namespace) -> Centroids:
|
|
77
|
+
path = Path(args.centroids)
|
|
78
|
+
if path.suffix.lower() in (".csv", ".txt", ".tsv"):
|
|
79
|
+
return read_centroids_csv(
|
|
80
|
+
path, id_col=args.id_field or "id", x_col=args.x_col, y_col=args.y_col
|
|
81
|
+
)
|
|
82
|
+
return centroids_from_geojson(path, id_field=args.id_field)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_rows(rows: List[Dict[str, Any]], out: Optional[str]) -> None:
|
|
86
|
+
if not rows:
|
|
87
|
+
return
|
|
88
|
+
fh = open(out, "w", encoding="utf-8", newline="") if out else sys.stdout
|
|
89
|
+
try:
|
|
90
|
+
w = csv.DictWriter(fh, fieldnames=list(rows[0]), lineterminator="\n")
|
|
91
|
+
w.writeheader()
|
|
92
|
+
for r in rows:
|
|
93
|
+
w.writerow({k: (f"{v:.6g}" if isinstance(v, float) else v) for k, v in r.items()})
|
|
94
|
+
finally:
|
|
95
|
+
if out:
|
|
96
|
+
fh.close()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _write_od(od: ODMatrix, args: argparse.Namespace) -> None:
|
|
100
|
+
if getattr(args, "output_matrix", False):
|
|
101
|
+
write_matrix_csv(od, args.output)
|
|
102
|
+
else:
|
|
103
|
+
write_od_csv(od, args.output)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _cmd_summary(args: argparse.Namespace) -> int:
|
|
107
|
+
od = _read_od(args)
|
|
108
|
+
if args.json:
|
|
109
|
+
print(json.dumps(matrix_summary(od), indent=2))
|
|
110
|
+
else:
|
|
111
|
+
_write_rows(zone_summary(od), args.output)
|
|
112
|
+
return EXIT_OK
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cmd_lines(args: argparse.Namespace) -> int:
|
|
116
|
+
od = _read_od(args)
|
|
117
|
+
fc = desire_lines(
|
|
118
|
+
od,
|
|
119
|
+
_read_centroids(args),
|
|
120
|
+
min_flow=args.min_flow,
|
|
121
|
+
flows=args.flows,
|
|
122
|
+
style=args.style,
|
|
123
|
+
bend=args.bend,
|
|
124
|
+
)
|
|
125
|
+
write_geojson(fc, args.output)
|
|
126
|
+
print(f"{len(fc['features'])} desire lines -> {args.output}", file=sys.stderr)
|
|
127
|
+
return EXIT_OK
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _cmd_filter(args: argparse.Namespace) -> int:
|
|
131
|
+
od = _read_od(args)
|
|
132
|
+
chosen = [x is not None for x in (args.top_k, args.min_flow, args.min_share, args.cumulative)]
|
|
133
|
+
if sum(chosen) != 1:
|
|
134
|
+
raise ODFlowsError("give exactly one of --top-k, --min-flow, --min-share, --cumulative")
|
|
135
|
+
keep_diag = args.include_intrazonal
|
|
136
|
+
if args.top_k is not None:
|
|
137
|
+
res = filter_top_k(od, args.top_k, include_intrazonal=keep_diag)
|
|
138
|
+
elif args.min_flow is not None:
|
|
139
|
+
res = filter_min_flow(od, args.min_flow, include_intrazonal=keep_diag)
|
|
140
|
+
elif args.min_share is not None:
|
|
141
|
+
res = filter_share(od, args.min_share, basis=args.basis, include_intrazonal=keep_diag)
|
|
142
|
+
else:
|
|
143
|
+
res = filter_cumulative(od, args.cumulative, include_intrazonal=keep_diag)
|
|
144
|
+
if args.drop_intrazonal:
|
|
145
|
+
res = res.without_intrazonal()
|
|
146
|
+
_write_od(res, args)
|
|
147
|
+
return EXIT_OK
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _cmd_transform(args: argparse.Namespace) -> int:
|
|
151
|
+
od = _read_od(args)
|
|
152
|
+
ops = {
|
|
153
|
+
"net": od.net,
|
|
154
|
+
"gross": od.gross,
|
|
155
|
+
"symmetric": od.symmetric,
|
|
156
|
+
"transpose": od.transpose,
|
|
157
|
+
"no-intrazonal": od.without_intrazonal,
|
|
158
|
+
}
|
|
159
|
+
res = ops[args.op]()
|
|
160
|
+
if args.op == "gross" and not args.output_matrix:
|
|
161
|
+
with open(args.output, "w", encoding="utf-8", newline="") as fh:
|
|
162
|
+
w = csv.writer(fh, lineterminator="\n")
|
|
163
|
+
w.writerow(["origin", "destination", "flow"])
|
|
164
|
+
for o, d, f in res.iter_flows(upper_triangle=True):
|
|
165
|
+
w.writerow([o, d, f"{f:g}"])
|
|
166
|
+
return EXIT_OK
|
|
167
|
+
_write_od(res, args)
|
|
168
|
+
return EXIT_OK
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cmd_aggregate(args: argparse.Namespace) -> int:
|
|
172
|
+
od = _read_od(args)
|
|
173
|
+
lookup = ZoneLookup.from_csv(
|
|
174
|
+
args.lookup, source=args.source_col, target=args.target_col, weight=args.weight_col
|
|
175
|
+
)
|
|
176
|
+
_write_od(reaggregate(od, lookup), args)
|
|
177
|
+
return EXIT_OK
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _cmd_regions(args: argparse.Namespace) -> int:
|
|
181
|
+
od = _read_od(args)
|
|
182
|
+
adjacency: Optional[Dict[str, List[str]]] = None
|
|
183
|
+
if args.adjacency:
|
|
184
|
+
adjacency = {}
|
|
185
|
+
with open(args.adjacency, encoding="utf-8-sig", newline="") as fh:
|
|
186
|
+
for row in csv.reader(fh):
|
|
187
|
+
if len(row) >= 2 and row[0].strip() and row[0].strip().lower() != "zone":
|
|
188
|
+
adjacency.setdefault(row[0].strip(), []).append(row[1].strip())
|
|
189
|
+
res = intramax(od, adjacency=adjacency, stop_at=args.n_regions)
|
|
190
|
+
if res.min_regions > args.n_regions:
|
|
191
|
+
print(
|
|
192
|
+
f"warning: stopped at {res.min_regions} regions (no linked pairs left)",
|
|
193
|
+
file=sys.stderr,
|
|
194
|
+
)
|
|
195
|
+
member = res.membership(args.n_regions)
|
|
196
|
+
_write_rows([{"zone": z, "region": r} for z, r in member.items()], args.output)
|
|
197
|
+
if args.summary:
|
|
198
|
+
_write_rows(region_summary(od, member), args.summary)
|
|
199
|
+
return EXIT_OK
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _cmd_cluster(args: argparse.Namespace) -> int:
|
|
203
|
+
od = _read_od(args)
|
|
204
|
+
flows = FlowSet.from_od(od, _read_centroids(args), min_flow=args.min_flow)
|
|
205
|
+
dist = flow_distance_matrix(
|
|
206
|
+
flows, alpha=args.alpha, beta=2.0 - args.alpha, normalized=args.normalized
|
|
207
|
+
)
|
|
208
|
+
if args.method == "dbscan":
|
|
209
|
+
if args.eps is None:
|
|
210
|
+
raise ODFlowsError("--eps is required for dbscan")
|
|
211
|
+
labels = flow_dbscan(
|
|
212
|
+
dist, args.eps, args.min_samples, weights=flows.weights if args.weighted else None
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
labels = hierarchical_clusters(
|
|
216
|
+
dist, n_clusters=args.n_clusters, threshold=args.eps, linkage=args.linkage
|
|
217
|
+
)
|
|
218
|
+
fc = bundle_flows(flows, labels, include_noise=args.include_noise)
|
|
219
|
+
write_geojson(fc, args.output)
|
|
220
|
+
print(f"{len(flows)} flows -> {len(fc['features'])} bundles -> {args.output}", file=sys.stderr)
|
|
221
|
+
return EXIT_OK
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
225
|
+
od = _read_od(args)
|
|
226
|
+
cents = _read_centroids(args)
|
|
227
|
+
if args.format == "flowmap":
|
|
228
|
+
base = Path(args.output)
|
|
229
|
+
stem = base.with_suffix("") if base.suffix.lower() == ".csv" else base
|
|
230
|
+
loc = Path(f"{stem}-locations.csv")
|
|
231
|
+
flo = Path(f"{stem}-flows.csv")
|
|
232
|
+
write_flowmap_blue(od, cents, loc, flo)
|
|
233
|
+
print(f"wrote {loc} and {flo}", file=sys.stderr)
|
|
234
|
+
elif args.format == "kepler":
|
|
235
|
+
write_kepler_csv(od, cents, args.output)
|
|
236
|
+
else:
|
|
237
|
+
write_geojson(desire_lines(od, cents), args.output)
|
|
238
|
+
return EXIT_OK
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
242
|
+
"""The ``odflows`` argument parser."""
|
|
243
|
+
parser = argparse.ArgumentParser(
|
|
244
|
+
prog="odflows", description="Origin-destination flow toolkit for mobility and commuting."
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument("--version", action="version", version=f"odflows {__version__}")
|
|
247
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
248
|
+
|
|
249
|
+
p = sub.add_parser("summary", help="per-zone indicators (CSV) or matrix statistics (--json)")
|
|
250
|
+
_add_input(p)
|
|
251
|
+
p.add_argument("-o", "--output", help="output CSV (default: stdout)")
|
|
252
|
+
p.add_argument("--json", action="store_true", help="print matrix-level statistics as JSON")
|
|
253
|
+
p.set_defaults(func=_cmd_summary)
|
|
254
|
+
|
|
255
|
+
p = sub.add_parser("lines", help="desire lines as GeoJSON")
|
|
256
|
+
_add_input(p)
|
|
257
|
+
_add_centroids(p)
|
|
258
|
+
p.add_argument("-o", "--output", required=True)
|
|
259
|
+
p.add_argument("--flows", choices=["directed", "net", "gross"], default="directed")
|
|
260
|
+
p.add_argument("--style", choices=["straight", "curved", "offset"], default="straight")
|
|
261
|
+
p.add_argument("--bend", type=float, default=0.15, help="curvature / offset (x length)")
|
|
262
|
+
p.add_argument("--min-flow", type=float, default=0.0)
|
|
263
|
+
p.set_defaults(func=_cmd_lines)
|
|
264
|
+
|
|
265
|
+
p = sub.add_parser("filter", help="keep the important flows")
|
|
266
|
+
_add_input(p)
|
|
267
|
+
p.add_argument("-o", "--output", required=True)
|
|
268
|
+
p.add_argument("--output-matrix", action="store_true", help="write a square matrix")
|
|
269
|
+
p.add_argument("--top-k", type=int)
|
|
270
|
+
p.add_argument("--min-flow", type=float)
|
|
271
|
+
p.add_argument("--min-share", type=float)
|
|
272
|
+
p.add_argument(
|
|
273
|
+
"--basis", choices=["origin", "destination", "either", "total"], default="origin"
|
|
274
|
+
)
|
|
275
|
+
p.add_argument("--cumulative", type=float, help="keep flows carrying this share of total")
|
|
276
|
+
p.add_argument(
|
|
277
|
+
"--include-intrazonal", action="store_true", help="let intrazonal flows compete too"
|
|
278
|
+
)
|
|
279
|
+
p.add_argument("--drop-intrazonal", action="store_true", help="remove intrazonal flows")
|
|
280
|
+
p.set_defaults(func=_cmd_filter)
|
|
281
|
+
|
|
282
|
+
p = sub.add_parser("transform", help="net, gross, symmetric, transpose, no-intrazonal")
|
|
283
|
+
_add_input(p)
|
|
284
|
+
p.add_argument("op", choices=["net", "gross", "symmetric", "transpose", "no-intrazonal"])
|
|
285
|
+
p.add_argument("-o", "--output", required=True)
|
|
286
|
+
p.add_argument("--output-matrix", action="store_true")
|
|
287
|
+
p.set_defaults(func=_cmd_transform)
|
|
288
|
+
|
|
289
|
+
p = sub.add_parser("aggregate", help="move flows to another zone system via a lookup")
|
|
290
|
+
_add_input(p)
|
|
291
|
+
p.add_argument("--lookup", required=True, help="CSV source,target[,weight]")
|
|
292
|
+
p.add_argument("--source-col", default="source")
|
|
293
|
+
p.add_argument("--target-col", default="target")
|
|
294
|
+
p.add_argument("--weight-col", default="weight")
|
|
295
|
+
p.add_argument("-o", "--output", required=True)
|
|
296
|
+
p.add_argument("--output-matrix", action="store_true")
|
|
297
|
+
p.set_defaults(func=_cmd_aggregate)
|
|
298
|
+
|
|
299
|
+
p = sub.add_parser("regions", help="functional regions with Intramax")
|
|
300
|
+
_add_input(p)
|
|
301
|
+
p.add_argument("-n", "--n-regions", type=int, required=True)
|
|
302
|
+
p.add_argument("--adjacency", help="CSV of zone,neighbour pairs (contiguity constraint)")
|
|
303
|
+
p.add_argument("-o", "--output", help="zone,region CSV (default: stdout)")
|
|
304
|
+
p.add_argument("--summary", help="also write per-region self-containment CSV")
|
|
305
|
+
p.set_defaults(func=_cmd_regions)
|
|
306
|
+
|
|
307
|
+
p = sub.add_parser("cluster", help="cluster similar flows and bundle them")
|
|
308
|
+
_add_input(p)
|
|
309
|
+
_add_centroids(p)
|
|
310
|
+
p.add_argument("-o", "--output", required=True, help="bundled flows GeoJSON")
|
|
311
|
+
p.add_argument("--method", choices=["hierarchical", "dbscan"], default="hierarchical")
|
|
312
|
+
p.add_argument("--n-clusters", type=int)
|
|
313
|
+
p.add_argument("--eps", type=float, help="distance threshold (hierarchical) / eps (dbscan)")
|
|
314
|
+
p.add_argument("--min-samples", type=float, default=2)
|
|
315
|
+
p.add_argument("--weighted", action="store_true", help="dbscan: count volume, not flows")
|
|
316
|
+
p.add_argument("--linkage", choices=["single", "complete", "average"], default="average")
|
|
317
|
+
p.add_argument("--alpha", type=float, default=1.0, help="origin weight (beta = 2 - alpha)")
|
|
318
|
+
p.add_argument("--normalized", action="store_true", help="use length-normalised FDS")
|
|
319
|
+
p.add_argument("--min-flow", type=float, default=0.0)
|
|
320
|
+
p.add_argument("--include-noise", action="store_true")
|
|
321
|
+
p.set_defaults(func=_cmd_cluster)
|
|
322
|
+
|
|
323
|
+
p = sub.add_parser("export", help="flowmap.blue CSVs, kepler.gl CSV or GeoJSON")
|
|
324
|
+
_add_input(p)
|
|
325
|
+
_add_centroids(p)
|
|
326
|
+
p.add_argument("--format", choices=["flowmap", "kepler", "geojson"], required=True)
|
|
327
|
+
p.add_argument("-o", "--output", required=True, help="file (flowmap: prefix)")
|
|
328
|
+
p.set_defaults(func=_cmd_export)
|
|
329
|
+
return parser
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
333
|
+
"""Entry point for the ``odflows`` command."""
|
|
334
|
+
parser = build_parser()
|
|
335
|
+
args = parser.parse_args(argv)
|
|
336
|
+
try:
|
|
337
|
+
return int(args.func(args))
|
|
338
|
+
except (ODFlowsError, MissingDependencyError, OSError) as exc:
|
|
339
|
+
print(f"odflows: error: {exc}", file=sys.stderr)
|
|
340
|
+
return EXIT_ERROR
|