nkde 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.
- nkde/__init__.py +56 -0
- nkde/__main__.py +7 -0
- nkde/cli.py +225 -0
- nkde/estimators.py +543 -0
- nkde/hotspots.py +164 -0
- nkde/interop.py +135 -0
- nkde/io.py +257 -0
- nkde/kernels.py +124 -0
- nkde/kfunction.py +148 -0
- nkde/network.py +705 -0
- nkde/planar.py +73 -0
- nkde/projection.py +134 -0
- nkde/py.typed +0 -0
- nkde-0.1.0.dist-info/METADATA +345 -0
- nkde-0.1.0.dist-info/RECORD +19 -0
- nkde-0.1.0.dist-info/WHEEL +5 -0
- nkde-0.1.0.dist-info/entry_points.txt +2 -0
- nkde-0.1.0.dist-info/licenses/LICENSE +21 -0
- nkde-0.1.0.dist-info/top_level.txt +1 -0
nkde/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""nkde: network kernel density estimation along street networks.
|
|
2
|
+
|
|
3
|
+
Estimate the density of events (crashes, crimes, shops, ...) *along* a street
|
|
4
|
+
network rather than across the plane: lixelisation, event snapping, the simple,
|
|
5
|
+
discontinuous and continuous network KDE of Okabe & Sugihara, adaptive
|
|
6
|
+
bandwidths, spatio-temporal TNKDE, planar KDE for comparison, the network
|
|
7
|
+
K-function and hot-spot ranking.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .estimators import METHODS, NKDEResult, abramson_bandwidths, event_kernel, nkde
|
|
11
|
+
from .hotspots import HotspotRanking, hotspot_clusters, monte_carlo_pvalues, rank_hotspots
|
|
12
|
+
from .io import (
|
|
13
|
+
events_from_geojson,
|
|
14
|
+
network_from_geojson,
|
|
15
|
+
read_lines,
|
|
16
|
+
read_points,
|
|
17
|
+
result_to_geojson,
|
|
18
|
+
)
|
|
19
|
+
from .kernels import KERNELS, get_kernel
|
|
20
|
+
from .kfunction import KFunctionResult, network_k_function, pairwise_network_distances
|
|
21
|
+
from .network import Events, Lixels, Network
|
|
22
|
+
from .planar import planar_kde
|
|
23
|
+
from .projection import UTMZone, lonlat_to_utm, utm_to_lonlat, utm_transformers
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"KERNELS",
|
|
29
|
+
"METHODS",
|
|
30
|
+
"Events",
|
|
31
|
+
"HotspotRanking",
|
|
32
|
+
"KFunctionResult",
|
|
33
|
+
"Lixels",
|
|
34
|
+
"NKDEResult",
|
|
35
|
+
"Network",
|
|
36
|
+
"UTMZone",
|
|
37
|
+
"__version__",
|
|
38
|
+
"abramson_bandwidths",
|
|
39
|
+
"event_kernel",
|
|
40
|
+
"events_from_geojson",
|
|
41
|
+
"get_kernel",
|
|
42
|
+
"hotspot_clusters",
|
|
43
|
+
"lonlat_to_utm",
|
|
44
|
+
"monte_carlo_pvalues",
|
|
45
|
+
"network_from_geojson",
|
|
46
|
+
"network_k_function",
|
|
47
|
+
"nkde",
|
|
48
|
+
"pairwise_network_distances",
|
|
49
|
+
"planar_kde",
|
|
50
|
+
"rank_hotspots",
|
|
51
|
+
"read_lines",
|
|
52
|
+
"read_points",
|
|
53
|
+
"result_to_geojson",
|
|
54
|
+
"utm_to_lonlat",
|
|
55
|
+
"utm_transformers",
|
|
56
|
+
]
|
nkde/__main__.py
ADDED
nkde/cli.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Command line interface: ``nkde run`` and ``nkde kfunction``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from typing import List, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .estimators import METHODS, nkde
|
|
15
|
+
from .hotspots import rank_hotspots
|
|
16
|
+
from .io import network_from_geojson, read_lines, read_points
|
|
17
|
+
from .kernels import KERNELS
|
|
18
|
+
from .kfunction import network_k_function
|
|
19
|
+
from .projection import utm_transformers
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_times(text: str) -> np.ndarray:
|
|
23
|
+
"""``start:stop:step`` (stop inclusive) or a comma separated list."""
|
|
24
|
+
if ":" in text:
|
|
25
|
+
parts = [float(p) for p in text.split(":")]
|
|
26
|
+
if len(parts) != 3 or parts[2] <= 0:
|
|
27
|
+
raise argparse.ArgumentTypeError("use start:stop:step with a positive step")
|
|
28
|
+
start, stop, step = parts
|
|
29
|
+
n = int(np.floor((stop - start) / step + 1e-9)) + 1
|
|
30
|
+
return start + step * np.arange(max(n, 1))
|
|
31
|
+
return np.array([float(p) for p in text.split(",") if p.strip()])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load(args: argparse.Namespace): # type: ignore[no-untyped-def]
|
|
35
|
+
forward = inverse = None
|
|
36
|
+
if args.lonlat:
|
|
37
|
+
lines, _ = read_lines(args.roads)
|
|
38
|
+
allc = np.vstack(lines)
|
|
39
|
+
forward, inverse, zone = utm_transformers(allc[:, 0], allc[:, 1])
|
|
40
|
+
print(
|
|
41
|
+
f"projected to UTM zone {zone.zone}{'S' if zone.south else 'N'} (EPSG:{zone.epsg})",
|
|
42
|
+
file=sys.stderr,
|
|
43
|
+
)
|
|
44
|
+
net = network_from_geojson(
|
|
45
|
+
args.roads,
|
|
46
|
+
transform=forward,
|
|
47
|
+
id_field=args.id_field,
|
|
48
|
+
split_at_intersections=args.split_crossings,
|
|
49
|
+
)
|
|
50
|
+
xy, t, w, _ = read_points(
|
|
51
|
+
args.events,
|
|
52
|
+
forward,
|
|
53
|
+
time_field=getattr(args, "time_field", None),
|
|
54
|
+
weight_field=getattr(args, "weight_field", None),
|
|
55
|
+
)
|
|
56
|
+
events = net.snap(xy, times=t, weights=w, max_distance=args.max_snap)
|
|
57
|
+
dropped = len(xy) - len(events)
|
|
58
|
+
print(
|
|
59
|
+
f"network: {net.n_nodes} nodes, {net.n_edges} edges, {net.total_length:.1f} m; "
|
|
60
|
+
f"events: {len(events)} snapped"
|
|
61
|
+
+ (f", {dropped} dropped (> --max-snap)" if dropped else ""),
|
|
62
|
+
file=sys.stderr,
|
|
63
|
+
)
|
|
64
|
+
return net, events, inverse
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cmd_run(args: argparse.Namespace) -> int:
|
|
68
|
+
net, events, inverse = _load(args)
|
|
69
|
+
if len(events) == 0:
|
|
70
|
+
print("error: no events left after snapping", file=sys.stderr)
|
|
71
|
+
return 2
|
|
72
|
+
times = None
|
|
73
|
+
if args.times is not None:
|
|
74
|
+
times = _parse_times(args.times)
|
|
75
|
+
t0 = time.perf_counter()
|
|
76
|
+
res = nkde(
|
|
77
|
+
net,
|
|
78
|
+
events,
|
|
79
|
+
args.bw,
|
|
80
|
+
lixel_length=args.lixel,
|
|
81
|
+
kernel=args.kernel,
|
|
82
|
+
method=args.method,
|
|
83
|
+
adaptive=args.adaptive,
|
|
84
|
+
trim_bw=args.trim_bw,
|
|
85
|
+
max_depth=args.max_depth,
|
|
86
|
+
times=times,
|
|
87
|
+
time_bw=args.time_bw,
|
|
88
|
+
)
|
|
89
|
+
elapsed = time.perf_counter() - t0
|
|
90
|
+
res.to_geojson(args.output, inverse_transform=inverse, precision=args.precision)
|
|
91
|
+
print(
|
|
92
|
+
f"{args.method} NKDE ({args.kernel}, bw={args.bw:g} m"
|
|
93
|
+
f"{', adaptive' if args.adaptive else ''}) on {len(res.lixels)} lixels "
|
|
94
|
+
f"in {elapsed:.2f} s -> {args.output}",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
if args.top:
|
|
98
|
+
rk = rank_hotspots(res, top=args.top)
|
|
99
|
+
print("rank\tlixel\tedge\tdensity")
|
|
100
|
+
for r, li, d in zip(rk.rank.tolist(), rk.lixel.tolist(), rk.density.tolist()):
|
|
101
|
+
print(f"{r}\t{li}\t{int(res.lixels.edge[li])}\t{d:.6g}")
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _cmd_kfunction(args: argparse.Namespace) -> int:
|
|
106
|
+
net, events, _ = _load(args)
|
|
107
|
+
t = np.arange(args.step, args.tmax + 1e-9, args.step)
|
|
108
|
+
res = network_k_function(net, events, t, n_simulations=args.nsim, seed=args.seed)
|
|
109
|
+
rows = []
|
|
110
|
+
for i in range(len(t)):
|
|
111
|
+
row = {"t": float(t[i]), "k": float(res.k[i])}
|
|
112
|
+
if res.lower is not None and res.upper is not None and res.mean is not None:
|
|
113
|
+
row.update(
|
|
114
|
+
lower=float(res.lower[i]), upper=float(res.upper[i]), mean=float(res.mean[i])
|
|
115
|
+
)
|
|
116
|
+
rows.append(row)
|
|
117
|
+
text: str
|
|
118
|
+
if args.output and args.output.lower().endswith(".json"):
|
|
119
|
+
text = json.dumps(rows, indent=1)
|
|
120
|
+
else:
|
|
121
|
+
cols = list(rows[0].keys())
|
|
122
|
+
text = "\n".join([",".join(cols)] + [",".join(f"{r[c]:.6g}" for c in cols) for r in rows])
|
|
123
|
+
if args.output:
|
|
124
|
+
with open(args.output, "w", encoding="utf-8") as fh:
|
|
125
|
+
fh.write(text + "\n")
|
|
126
|
+
print(f"K-function at {len(t)} distances -> {args.output}", file=sys.stderr)
|
|
127
|
+
else:
|
|
128
|
+
print(text)
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _common(p: argparse.ArgumentParser) -> None:
|
|
133
|
+
p.add_argument("roads", help="GeoJSON line layer (projected metres unless --lonlat)")
|
|
134
|
+
p.add_argument("events", help="GeoJSON point layer")
|
|
135
|
+
p.add_argument(
|
|
136
|
+
"--lonlat",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help="inputs are WGS84 lon/lat: project to UTM (output written back in lon/lat)",
|
|
139
|
+
)
|
|
140
|
+
p.add_argument(
|
|
141
|
+
"--max-snap",
|
|
142
|
+
type=float,
|
|
143
|
+
default=None,
|
|
144
|
+
help="drop events farther than this from the network (m)",
|
|
145
|
+
)
|
|
146
|
+
p.add_argument(
|
|
147
|
+
"--id-field", default=None, help="road property copied to the output as source_id"
|
|
148
|
+
)
|
|
149
|
+
p.add_argument(
|
|
150
|
+
"--split-crossings",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help="planarise: add junctions where road lines cross without a shared vertex",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
157
|
+
parser = argparse.ArgumentParser(
|
|
158
|
+
prog="nkde", description="Network kernel density estimation along street networks."
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument("--version", action="version", version=f"nkde {__version__}")
|
|
161
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
162
|
+
|
|
163
|
+
run = sub.add_parser("run", help="compute a (temporal) network KDE on lixels")
|
|
164
|
+
_common(run)
|
|
165
|
+
run.add_argument("--bw", type=float, required=True, help="bandwidth in metres")
|
|
166
|
+
run.add_argument(
|
|
167
|
+
"--lixel", type=float, default=10.0, help="lixel length in metres (default 10)"
|
|
168
|
+
)
|
|
169
|
+
run.add_argument("--method", choices=METHODS, default="continuous")
|
|
170
|
+
run.add_argument("--kernel", choices=KERNELS, default="quartic")
|
|
171
|
+
run.add_argument("--adaptive", action="store_true", help="Abramson adaptive bandwidths")
|
|
172
|
+
run.add_argument(
|
|
173
|
+
"--trim-bw", type=float, default=None, help="cap for adaptive bandwidths (default 3*bw)"
|
|
174
|
+
)
|
|
175
|
+
run.add_argument("--max-depth", type=int, default=None, help="max junctions crossed per branch")
|
|
176
|
+
run.add_argument("--weight-field", default=None, help="event property holding weights")
|
|
177
|
+
run.add_argument(
|
|
178
|
+
"--time-field", default=None, help="event property holding times (number or ISO date)"
|
|
179
|
+
)
|
|
180
|
+
run.add_argument(
|
|
181
|
+
"--time-bw",
|
|
182
|
+
type=float,
|
|
183
|
+
default=None,
|
|
184
|
+
help="temporal bandwidth (same unit as times; days for ISO dates)",
|
|
185
|
+
)
|
|
186
|
+
run.add_argument(
|
|
187
|
+
"--times", default=None, help="evaluation times: start:stop:step or comma list"
|
|
188
|
+
)
|
|
189
|
+
run.add_argument("--precision", type=int, default=3, help="decimals of output coordinates")
|
|
190
|
+
run.add_argument("--top", type=int, default=0, help="print the N highest-density lixels")
|
|
191
|
+
run.add_argument("-o", "--output", required=True, help="output GeoJSON path")
|
|
192
|
+
run.set_defaults(func=_cmd_run)
|
|
193
|
+
|
|
194
|
+
kf = sub.add_parser("kfunction", help="global network K-function with CSR envelopes")
|
|
195
|
+
_common(kf)
|
|
196
|
+
kf.add_argument("--tmax", type=float, required=True, help="largest distance (m)")
|
|
197
|
+
kf.add_argument("--step", type=float, required=True, help="distance step (m)")
|
|
198
|
+
kf.add_argument("--nsim", type=int, default=99, help="number of CSR simulations")
|
|
199
|
+
kf.add_argument("--seed", type=int, default=None)
|
|
200
|
+
kf.add_argument("-o", "--output", default=None, help="CSV or .json path (stdout if omitted)")
|
|
201
|
+
kf.set_defaults(func=_cmd_kfunction)
|
|
202
|
+
return parser
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
206
|
+
parser = build_parser()
|
|
207
|
+
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
208
|
+
if args.command == "run":
|
|
209
|
+
if (args.times is None) != (args.time_field is None) or (
|
|
210
|
+
args.times is not None and args.time_bw is None
|
|
211
|
+
):
|
|
212
|
+
parser.error("temporal NKDE needs --time-field, --times and --time-bw together")
|
|
213
|
+
try:
|
|
214
|
+
code: int = args.func(args)
|
|
215
|
+
except (ValueError, OSError) as exc:
|
|
216
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
217
|
+
return 1
|
|
218
|
+
return code
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__": # pragma: no cover
|
|
222
|
+
sys.exit(main())
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
__all__: List[str] = ["main", "build_parser"]
|