nkde 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.
- nkde-0.1.0/CHANGELOG.md +31 -0
- nkde-0.1.0/CITATION.cff +39 -0
- nkde-0.1.0/LICENSE +21 -0
- nkde-0.1.0/MANIFEST.in +9 -0
- nkde-0.1.0/PKG-INFO +345 -0
- nkde-0.1.0/README.md +303 -0
- nkde-0.1.0/examples/benchmark.py +65 -0
- nkde-0.1.0/examples/quickstart.py +80 -0
- nkde-0.1.0/pyproject.toml +128 -0
- nkde-0.1.0/setup.cfg +4 -0
- nkde-0.1.0/src/nkde/__init__.py +56 -0
- nkde-0.1.0/src/nkde/__main__.py +7 -0
- nkde-0.1.0/src/nkde/cli.py +225 -0
- nkde-0.1.0/src/nkde/estimators.py +543 -0
- nkde-0.1.0/src/nkde/hotspots.py +164 -0
- nkde-0.1.0/src/nkde/interop.py +135 -0
- nkde-0.1.0/src/nkde/io.py +257 -0
- nkde-0.1.0/src/nkde/kernels.py +124 -0
- nkde-0.1.0/src/nkde/kfunction.py +148 -0
- nkde-0.1.0/src/nkde/network.py +705 -0
- nkde-0.1.0/src/nkde/planar.py +73 -0
- nkde-0.1.0/src/nkde/projection.py +134 -0
- nkde-0.1.0/src/nkde/py.typed +0 -0
- nkde-0.1.0/src/nkde.egg-info/PKG-INFO +345 -0
- nkde-0.1.0/src/nkde.egg-info/SOURCES.txt +36 -0
- nkde-0.1.0/src/nkde.egg-info/dependency_links.txt +1 -0
- nkde-0.1.0/src/nkde.egg-info/entry_points.txt +2 -0
- nkde-0.1.0/src/nkde.egg-info/requires.txt +16 -0
- nkde-0.1.0/src/nkde.egg-info/top_level.txt +1 -0
- nkde-0.1.0/tests/conftest.py +47 -0
- nkde-0.1.0/tests/test_estimators.py +332 -0
- nkde-0.1.0/tests/test_interop.py +72 -0
- nkde-0.1.0/tests/test_io_cli.py +297 -0
- nkde-0.1.0/tests/test_kernels.py +53 -0
- nkde-0.1.0/tests/test_kfunction.py +64 -0
- nkde-0.1.0/tests/test_network.py +167 -0
- nkde-0.1.0/tests/test_planar_hotspots.py +75 -0
- nkde-0.1.0/tests/test_projection.py +45 -0
nkde-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
- **Network model** (`Network`): build from polylines (junctions at shared vertices, optional
|
|
10
|
+
planarisation with `split_at_intersections`), from node/edge lists, from GeoJSON,
|
|
11
|
+
GeoDataFrames or networkx graphs. Bounded multi-source Dijkstra, dead-end detection.
|
|
12
|
+
- **Lixelisation** (`Network.lixelize`): every edge split into equal lixels no longer than `L`.
|
|
13
|
+
- **Event snapping** (`Network.snap`): nearest edge through a uniform grid index, with
|
|
14
|
+
offsets, snap distances, optional times, weights and a `max_distance` filter.
|
|
15
|
+
- **Kernels**: quartic, triangular, Epanechnikov, Gaussian (truncated, renormalised) and
|
|
16
|
+
uniform, in 1D (network, time) and 2D (planar), each with unit mass.
|
|
17
|
+
- **Estimators** (`nkde`): `simple`, `discontinuous` (Okabe, Satoh & Sugihara 2009 equal
|
|
18
|
+
split) and `continuous` (Okabe & Sugihara 2012 unbiased continuous kernel). Exact branch
|
|
19
|
+
enumeration with merging of equivalent branches, vectorised accumulation, chunked
|
|
20
|
+
memory use, optional `max_depth` and `min_weight` truncation.
|
|
21
|
+
- **Adaptive bandwidths**: Abramson (1982) square-root law from a fixed-bandwidth pilot,
|
|
22
|
+
capped by `trim_bw`.
|
|
23
|
+
- **TNKDE**: spatio-temporal product kernel evaluated on a time grid.
|
|
24
|
+
- **Planar KDE** (`planar_kde`) for comparison, with a scipy KD-tree or numpy back end.
|
|
25
|
+
- **Network K-function** (`network_k_function`, Okabe & Yamada 2001) with Monte Carlo CSR
|
|
26
|
+
envelopes (min/max or quantile).
|
|
27
|
+
- **Hot spots**: `rank_hotspots`, Monte Carlo p-values per lixel, `hotspot_clusters`
|
|
28
|
+
(connected hot lixels).
|
|
29
|
+
- **I/O**: GeoJSON lines and points in, GeoJSON lixels with density out; dependency-free
|
|
30
|
+
WGS84 to UTM projection (`lonlat_to_utm`, `utm_to_lonlat`, `utm_transformers`).
|
|
31
|
+
- **CLI**: `nkde run` and `nkde kfunction`.
|
nkde-0.1.0/CITATION.cff
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "nkde: network kernel density estimation along street networks"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/nkde_sdk"
|
|
13
|
+
url: "https://pypi.org/project/nkde/"
|
|
14
|
+
abstract: "Python library for network kernel density estimation (simple, discontinuous and continuous NKDE, adaptive bandwidths, spatio-temporal TNKDE), planar KDE, the network K-function and hot-spot ranking along street networks."
|
|
15
|
+
keywords:
|
|
16
|
+
- "network kernel density estimation"
|
|
17
|
+
- "spatial analysis along networks"
|
|
18
|
+
- "road safety"
|
|
19
|
+
- "crime analysis"
|
|
20
|
+
references:
|
|
21
|
+
- type: book
|
|
22
|
+
title: "Spatial Analysis Along Networks: Statistical and Computational Methods"
|
|
23
|
+
authors:
|
|
24
|
+
- family-names: "Okabe"
|
|
25
|
+
given-names: "Atsuyuki"
|
|
26
|
+
- family-names: "Sugihara"
|
|
27
|
+
given-names: "Kokichi"
|
|
28
|
+
year: 2012
|
|
29
|
+
publisher:
|
|
30
|
+
name: "Wiley"
|
|
31
|
+
- type: article
|
|
32
|
+
title: "spNetwork: A Package for Network Kernel Density Estimation"
|
|
33
|
+
authors:
|
|
34
|
+
- family-names: "Gelb"
|
|
35
|
+
given-names: "Jérémy"
|
|
36
|
+
journal: "The R Journal"
|
|
37
|
+
year: 2021
|
|
38
|
+
volume: 13
|
|
39
|
+
issue: 2
|
nkde-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.
|
nkde-0.1.0/MANIFEST.in
ADDED
nkde-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nkde
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Network kernel density estimation (NKDE / TNKDE) along street networks: simple, discontinuous and continuous kernels, network K-function and hot spots.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/nkde_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/nkde_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/nkde_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/nkde_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: kernel-density,nkde,tnkde,network-kde,spatial-analysis,street-network,road-safety,crash-hotspots,crime-analysis,retail,lixel,k-function,hotspot,gis
|
|
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: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Classifier: Intended Audience :: Science/Research
|
|
23
|
+
Classifier: Intended Audience :: Developers
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: numpy>=1.20.0
|
|
29
|
+
Provides-Extra: fast
|
|
30
|
+
Requires-Dist: scipy>=1.7.0; extra == "fast"
|
|
31
|
+
Provides-Extra: geo
|
|
32
|
+
Requires-Dist: scipy>=1.7.0; extra == "geo"
|
|
33
|
+
Requires-Dist: shapely>=2.0.0; extra == "geo"
|
|
34
|
+
Requires-Dist: geopandas>=0.12.0; extra == "geo"
|
|
35
|
+
Requires-Dist: networkx>=2.8.0; extra == "geo"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# nkde
|
|
44
|
+
|
|
45
|
+
**Network kernel density estimation along street networks, in Python.**
|
|
46
|
+
|
|
47
|
+
Crashes, crimes, shops and bus stops happen *on* streets. A planar (2D) kernel
|
|
48
|
+
density spreads every event over a disc, so it puts density on the street behind a
|
|
49
|
+
block that is 100 m away as the crow flies but 300 m away on foot or by car. It also
|
|
50
|
+
counts every parallel street that the disc touches. Network kernel density
|
|
51
|
+
estimation (NKDE) spreads each event *along the network* using shortest-path or
|
|
52
|
+
branch distances, so the result is a density per metre of street.
|
|
53
|
+
|
|
54
|
+
`nkde` provides:
|
|
55
|
+
|
|
56
|
+
- the **three NKDE estimators** described by Okabe & Sugihara (2012) and implemented in
|
|
57
|
+
the R package spNetwork (Gelb 2021): `simple`, `discontinuous` and `continuous`
|
|
58
|
+
- **lixelisation** (edges split into lixels of length *L*) and **event snapping** to
|
|
59
|
+
the nearest edge, with offsets
|
|
60
|
+
- five kernels: quartic, triangular, Epanechnikov, Gaussian and uniform; a **fixed**
|
|
61
|
+
or **adaptive** (Abramson) bandwidth; event weights
|
|
62
|
+
- **TNKDE**: a spatio-temporal density on a time grid
|
|
63
|
+
- **planar KDE** for comparison, the **network K-function** with Monte Carlo
|
|
64
|
+
envelopes, and **hot-spot ranking and clustering** of lixels
|
|
65
|
+
- GeoJSON in and out, a dependency-free lon/lat to UTM helper, optional
|
|
66
|
+
geopandas/networkx bridges, and a command line tool
|
|
67
|
+
|
|
68
|
+
The only required dependency is numpy. scipy (optional) speeds up the planar KDE.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install nkde # numpy only
|
|
74
|
+
pip install "nkde[fast]" # + scipy (planar KDE through a KD-tree)
|
|
75
|
+
pip install "nkde[geo]" # + scipy, shapely, geopandas, networkx
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Python 3.9 to 3.13.
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
### Command line
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
nkde run roads.geojson events.geojson --bw 300 --lixel 10 --method continuous -o density.geojson
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Inputs are GeoJSON LineString/MultiLineString roads and Point/MultiPoint events in
|
|
89
|
+
projected metres. With `--lonlat`, WGS84 inputs are projected to their UTM zone and
|
|
90
|
+
the output is written back in lon/lat. Other options:
|
|
91
|
+
|
|
92
|
+
| option | meaning |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `--kernel quartic\|triangular\|epanechnikov\|gaussian\|uniform` | kernel (default quartic) |
|
|
95
|
+
| `--method simple\|discontinuous\|continuous` | estimator (default continuous) |
|
|
96
|
+
| `--adaptive`, `--trim-bw M` | Abramson adaptive bandwidth, capped at `M` (default 3 x bw) |
|
|
97
|
+
| `--weight-field F` | event weights, for example casualties |
|
|
98
|
+
| `--time-field F --times 0:365:7 --time-bw 30` | TNKDE (numbers or ISO dates; ISO dates become days) |
|
|
99
|
+
| `--max-snap M` | drop events farther than `M` metres from any road |
|
|
100
|
+
| `--split-crossings` | add junctions where roads cross without a shared vertex |
|
|
101
|
+
| `--max-depth N` | limit branch enumeration to N junctions |
|
|
102
|
+
| `--top N` | print the N highest-density lixels |
|
|
103
|
+
|
|
104
|
+
`nkde kfunction roads.geojson events.geojson --tmax 1000 --step 50 --nsim 99 -o k.csv`
|
|
105
|
+
writes the network K-function with its simulation envelope.
|
|
106
|
+
|
|
107
|
+
The output is one LineString per lixel with properties `lixel`, `edge`, `source_id`,
|
|
108
|
+
`length`, `density` (per metre, integrates to 1 over the network) and `intensity`
|
|
109
|
+
(events per metre, integrates to the number of events or the total weight). TNKDE
|
|
110
|
+
output has `density_t<j>` / `intensity_t<j>` columns, and the times are stored in
|
|
111
|
+
the collection's `nkde` member.
|
|
112
|
+
|
|
113
|
+
### Python
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import nkde
|
|
117
|
+
|
|
118
|
+
net = nkde.network_from_geojson("roads.geojson") # projected metres
|
|
119
|
+
events = nkde.events_from_geojson("crashes.geojson", net, max_distance=25)
|
|
120
|
+
|
|
121
|
+
res = nkde.nkde(net, events, bw=300, lixel_length=10, method="continuous")
|
|
122
|
+
res.to_geojson("density.geojson")
|
|
123
|
+
|
|
124
|
+
top = nkde.rank_hotspots(res, top=20) # highest-density lixels
|
|
125
|
+
clusters = nkde.hotspot_clusters(res.lixels, res.density >= 0.002)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Lon/lat data:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
lines, _ = nkde.read_lines("roads_wgs84.geojson")
|
|
132
|
+
import numpy as np
|
|
133
|
+
allc = np.vstack(lines)
|
|
134
|
+
fwd, inv, zone = nkde.utm_transformers(allc[:, 0], allc[:, 1]) # zone.epsg -> 326xx
|
|
135
|
+
net = nkde.network_from_geojson("roads_wgs84.geojson", transform=fwd)
|
|
136
|
+
events = nkde.events_from_geojson("events_wgs84.geojson", net, transform=fwd)
|
|
137
|
+
res = nkde.nkde(net, events, 300, lixel_length=10)
|
|
138
|
+
res.to_geojson("density_wgs84.geojson", inverse_transform=inv)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Spatio-temporal density, comparison with a planar KDE, and K-function:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
ev = net.snap(xy, times=days) # times in any unit
|
|
145
|
+
tres = nkde.nkde(net, ev, 300, lixel_length=10, times=range(0, 365, 7), time_bw=30)
|
|
146
|
+
tres.density.shape # (n_lixels, n_times)
|
|
147
|
+
|
|
148
|
+
planar = nkde.planar_kde(res.lixels, events, 300) # per square metre
|
|
149
|
+
|
|
150
|
+
k = nkde.network_k_function(net, events, distances=range(50, 1001, 50), n_simulations=99)
|
|
151
|
+
k.clustered() # observed K above the envelope
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`examples/quickstart.py` builds a small network and writes GeoJSON outputs.
|
|
155
|
+
`examples/benchmark.py` reproduces the timings below.
|
|
156
|
+
|
|
157
|
+
## Methods
|
|
158
|
+
|
|
159
|
+
### Lixels and events
|
|
160
|
+
|
|
161
|
+
Each edge of length `l` is cut into `ceil(l / L)` equal lixels, so no lixel is longer
|
|
162
|
+
than `L`. The density is evaluated at lixel mid points. Each event is snapped to the
|
|
163
|
+
nearest point on the nearest edge and is stored as `(edge, offset)`, where the offset
|
|
164
|
+
is the distance in metres from the start of the edge. Junctions are the end points of
|
|
165
|
+
lines and the vertices shared by several lines. `split_at_intersections=True`
|
|
166
|
+
(`--split-crossings`) also adds junctions where lines cross without a shared vertex.
|
|
167
|
+
|
|
168
|
+
### Kernels
|
|
169
|
+
|
|
170
|
+
All kernels have support `[-h, h]` and unit mass in 1D (`u = d / h`):
|
|
171
|
+
|
|
172
|
+
| kernel | `k(d)` |
|
|
173
|
+
|---|---|
|
|
174
|
+
| quartic | `15/16 (1 - u^2)^2 / h` |
|
|
175
|
+
| triangular | `(1 - |u|) / h` |
|
|
176
|
+
| Epanechnikov | `3/4 (1 - u^2) / h` |
|
|
177
|
+
| Gaussian | `exp(-u^2 / (2 (1/3)^2))`, truncated at `h` and renormalised (sigma = h/3) |
|
|
178
|
+
| uniform | `1 / (2h)` |
|
|
179
|
+
|
|
180
|
+
The planar KDE uses the 2D versions of these kernels, which have unit mass over the disc
|
|
181
|
+
of radius `h`. Note that spNetwork's Gaussian kernel uses sigma = h without
|
|
182
|
+
truncation, so Gaussian results differ from spNetwork by construction.
|
|
183
|
+
|
|
184
|
+
### The three estimators
|
|
185
|
+
|
|
186
|
+
Take an event `e` and a point `x` on the network.
|
|
187
|
+
|
|
188
|
+
* **simple**: `f(x) = k(d(e, x))`, where `d` is the shortest-path distance. It is
|
|
189
|
+
computed by a multi-source Dijkstra from the two ends of the event's edge, stopped at
|
|
190
|
+
`h`. At a junction of degree `n` the full kernel continues on all `n - 1` other edges,
|
|
191
|
+
so one event has a mass greater than 1 near junctions (2.3 to 3.6 on a city grid, see
|
|
192
|
+
below). Dead ends lose mass. This is the classical "NKDE with network distance".
|
|
193
|
+
It can over-weight dense networks, but its ranking of lixels is often still useful.
|
|
194
|
+
* **discontinuous** (Okabe, Satoh & Sugihara 2009, equal split): the kernel spreads
|
|
195
|
+
from the event in both directions. At a node of degree `n` each branch splits into
|
|
196
|
+
`n - 1` branches with weight `w / (n - 1)`. Each branch stops when its distance
|
|
197
|
+
reaches `h`. Mass is preserved exactly, except at dead ends. The density jumps at
|
|
198
|
+
junctions.
|
|
199
|
+
* **continuous** (Sugihara, Satoh & Okabe 2010; Okabe & Sugihara 2012): at a node of
|
|
200
|
+
degree `n` a branch continues on each other edge with weight `2w/n` and is reflected
|
|
201
|
+
back on its own edge with weight `(2/n - 1) w`. The density is continuous at nodes,
|
|
202
|
+
unbiased, and mass is preserved exactly. A dead end (`n = 1`) reflects the whole
|
|
203
|
+
branch, so mass is preserved even at dead ends.
|
|
204
|
+
|
|
205
|
+
As in spNetwork, the discontinuous and continuous kernels follow **every walk**
|
|
206
|
+
shorter than `h`, not only shortest paths, so a branch can go around a block and come
|
|
207
|
+
back. The number of walks grows exponentially with `h / block length`. `nkde` merges
|
|
208
|
+
branches that enter the same directed edge at the same distance, because their future
|
|
209
|
+
contributions are identical. On regular street grids this keeps the cost polynomial.
|
|
210
|
+
`max_depth` (number of junctions crossed) and `min_weight` truncate the enumeration if
|
|
211
|
+
needed. Both default to the exact estimator.
|
|
212
|
+
|
|
213
|
+
Implementation: each event produces a list of *traversals* `(edge, entry end, start
|
|
214
|
+
distance, weight)` with a priority queue ordered by distance (the bounded Dijkstra
|
|
215
|
+
for `simple`). The traversals of thousands of events are then expanded into
|
|
216
|
+
`(lixel, value)` pairs in one vectorised numpy pass and summed with `bincount`.
|
|
217
|
+
Only lixels within `h` are touched. Events are processed in chunks (`chunk_size`),
|
|
218
|
+
which bounds memory use.
|
|
219
|
+
|
|
220
|
+
### Adaptive bandwidth
|
|
221
|
+
|
|
222
|
+
With `adaptive=True`, a fixed-bandwidth pilot `f~` is evaluated at the lixel of each event,
|
|
223
|
+
and `h_i = h0 * sqrt(g / f~(x_i))`, where `g` is the geometric mean of the pilot values
|
|
224
|
+
(Abramson 1982). `h_i` is capped at `trim_bw` (default `3 h0`), as with spNetwork's `trim_bw`.
|
|
225
|
+
|
|
226
|
+
### TNKDE
|
|
227
|
+
|
|
228
|
+
`f(x, t) = (1 / W) sum_i w_i k_net(x; e_i) k_time(t - t_i)`, where both kernels have
|
|
229
|
+
unit mass. It is evaluated at the lixels for each requested time.
|
|
230
|
+
`NKDEResult.integral()` integrates over space and time.
|
|
231
|
+
|
|
232
|
+
### K-function and hot spots
|
|
233
|
+
|
|
234
|
+
The global network K-function (Okabe & Yamada 2001) is
|
|
235
|
+
`K(t) = L / (n (n - 1)) * #{ordered pairs i != j : d(i, j) <= t}`, where `L` is the
|
|
236
|
+
network length. The envelope comes from `n_simulations` patterns of `n` uniform random
|
|
237
|
+
points on the network (CSR on the network). Pointwise min/max or quantiles are available.
|
|
238
|
+
`rank_hotspots` orders lixels by density and flags those above a quantile.
|
|
239
|
+
`monte_carlo_pvalues` reruns the estimator on CSR patterns to give a p-value per lixel.
|
|
240
|
+
`hotspot_clusters` labels connected groups of hot lixels.
|
|
241
|
+
|
|
242
|
+
## Validation
|
|
243
|
+
|
|
244
|
+
The test suite (110 tests) checks:
|
|
245
|
+
|
|
246
|
+
* **Unit mass of every kernel** in 1D and 2D, by numerical integration.
|
|
247
|
+
* **Hand-checkable cases.** On a single line every estimator equals `k(|x - e|)`
|
|
248
|
+
exactly. On a 3-arm star the value on another arm is `k`, `k/2` and `2k/3` for the
|
|
249
|
+
simple, discontinuous and continuous kernels. Between the event and the centre the
|
|
250
|
+
continuous kernel is `k(d) - k(d')/3` because of the reflected branch. At a dead end
|
|
251
|
+
the continuous kernel adds the full mirror image.
|
|
252
|
+
* **Agreement where the methods must coincide.** With no junction within `h`, or with
|
|
253
|
+
only degree-2 junctions, all three methods agree to 1e-15.
|
|
254
|
+
* **Symmetry.** The density at lixel *b* from an event at *a* equals the density at *a*
|
|
255
|
+
from an event at *b*, for all three methods on a jittered grid. A centred event on a
|
|
256
|
+
regular grid gives a mirror-symmetric density.
|
|
257
|
+
* **Continuity.** The continuous kernel has the same value on every edge next to a
|
|
258
|
+
node. The discontinuous kernel does not.
|
|
259
|
+
* **Mass conservation**, integral over lixels with the midpoint rule and 1 m lixels.
|
|
260
|
+
The table below uses 50 random events on a jittered 6 x 6 grid (no dead ends; `h`
|
|
261
|
+
up to 4 block lengths, so branches loop around blocks):
|
|
262
|
+
|
|
263
|
+
| method | h = 60 m | h = 230 m | h = 420 m |
|
|
264
|
+
|---|---|---|---|
|
|
265
|
+
| simple | 1.000 to 1.98 | 1.29 to 3.20 | 1.89 to 5.04 |
|
|
266
|
+
| discontinuous | 1 +/- 1e-6 | 1 +/- 1e-6 | 1 +/- 1e-6 |
|
|
267
|
+
| continuous | 1 +/- 1e-6 | 1 +/- 1e-6 | 1 +/- 1e-6 |
|
|
268
|
+
|
|
269
|
+
On a star with two dead ends within reach, the mass is 1.065 for simple, 0.895 for
|
|
270
|
+
discontinuous and 1.000000 for continuous. Per kernel (continuous, h = 230 m) the
|
|
271
|
+
mass error is 1e-8 for quartic, 8e-7 for triangular, 2e-6 for Epanechnikov, 4e-5 for
|
|
272
|
+
Gaussian and 2e-3 for uniform. The last two errors come from the midpoint rule at the
|
|
273
|
+
kernel's jump at `h`, not from the estimator.
|
|
274
|
+
* **TNKDE** integrates to the number of events over space and time. The K-function is
|
|
275
|
+
checked against hand counts, and clustered patterns lie above the CSR envelope. UTM
|
|
276
|
+
coordinates match PROJ within 1 mm, and snapping matches a brute-force search.
|
|
277
|
+
|
|
278
|
+
**Cross-check against spNetwork.** No numeric comparison with published spNetwork
|
|
279
|
+
outputs was made, because no reference values were available that could be reproduced
|
|
280
|
+
reliably. The estimators follow the definitions in Okabe & Sugihara (2012) and in the
|
|
281
|
+
spNetwork documentation (Gelb 2021): equal split by `n - 1`, continuous factors `2/n`
|
|
282
|
+
and `2/n - 1`, and enumeration of all walks within the bandwidth. The hand-checkable
|
|
283
|
+
cases above test those definitions directly. The known difference is the Gaussian
|
|
284
|
+
kernel convention (see Kernels). spNetwork may also place lixel sample points or
|
|
285
|
+
handle the last, shorter lixel of each edge differently.
|
|
286
|
+
|
|
287
|
+
## Performance
|
|
288
|
+
|
|
289
|
+
These timings come from `python examples/benchmark.py` on a laptop (AMD Ryzen 5 4600H,
|
|
290
|
+
Python 3.12, numpy 2.5, single thread). The network is a jittered 80 x 80 street grid with
|
|
291
|
+
100 m blocks: 6,400 nodes, 12,640 edges and 1,272 km of street, cut into **133,462 lixels**
|
|
292
|
+
of 10 m. There are **10,000 random events**, the bandwidth is 300 m and the kernel is quartic.
|
|
293
|
+
|
|
294
|
+
| step | time | mass per event |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| snap 10,000 points | 1.3 s | |
|
|
297
|
+
| simple NKDE | 1.5 s | 3.57 |
|
|
298
|
+
| discontinuous NKDE | 2.6 s | 1.000000 |
|
|
299
|
+
| continuous NKDE | 5.9 s | 1.000000 |
|
|
300
|
+
| continuous, adaptive (pilot + final) | 12.2 s | |
|
|
301
|
+
| planar KDE at lixel mid points (scipy) | 1.1 s | |
|
|
302
|
+
| K-function, 1,000 events, 20 distances, 19 simulations | 3.6 s | |
|
|
303
|
+
|
|
304
|
+
## API overview
|
|
305
|
+
|
|
306
|
+
| object | purpose |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `Network.from_lines`, `Network.from_edges`, `network_from_geojson` | build the network |
|
|
309
|
+
| `Network.lixelize(L)` -> `Lixels` | lixels (`edge`, `start`, `end`, `mid`, `length`, `midpoints_xy()`, `locate()`) |
|
|
310
|
+
| `Network.snap(xy, times=, weights=, max_distance=)` -> `Events` | snap points; `events_on_edges`, `random_events` |
|
|
311
|
+
| `nkde(network_or_lixels, events, bw, ...)` -> `NKDEResult` | NKDE / TNKDE (`intensity`, `density`, `bandwidths`, `integral()`, `to_geojson()`) |
|
|
312
|
+
| `event_kernel`, `abramson_bandwidths` | one event's kernel; adaptive bandwidths |
|
|
313
|
+
| `planar_kde(targets, events, bw)` | Euclidean KDE |
|
|
314
|
+
| `network_k_function`, `pairwise_network_distances` | K-function and envelopes |
|
|
315
|
+
| `rank_hotspots`, `monte_carlo_pvalues`, `hotspot_clusters` | hot spots |
|
|
316
|
+
| `read_lines`, `read_points`, `events_from_geojson`, `result_to_geojson` | GeoJSON I/O |
|
|
317
|
+
| `lonlat_to_utm`, `utm_to_lonlat`, `utm_transformers`, `UTMZone` | projection |
|
|
318
|
+
| `nkde.interop` | `network_from_geodataframe`, `events_from_geodataframe`, `result_to_geodataframe`, `network_from_networkx`, `network_to_networkx` |
|
|
319
|
+
|
|
320
|
+
## References
|
|
321
|
+
|
|
322
|
+
- Abramson, I. S. (1982). On bandwidth variation in kernel estimates: a square root law.
|
|
323
|
+
*The Annals of Statistics*, 10(4), 1217-1223.
|
|
324
|
+
- Gelb, J. (2021). spNetwork: a package for network kernel density estimation.
|
|
325
|
+
*The R Journal*, 13(2), 528-544.
|
|
326
|
+
- Okabe, A., & Sugihara, K. (2012). *Spatial Analysis Along Networks: Statistical and
|
|
327
|
+
Computational Methods*. Wiley.
|
|
328
|
+
- Okabe, A., Satoh, T., & Sugihara, K. (2009). A kernel density estimation method for
|
|
329
|
+
networks, its computational method and a GIS-based tool. *International Journal of
|
|
330
|
+
Geographical Information Science*, 23(1), 7-32.
|
|
331
|
+
- Okabe, A., & Yamada, I. (2001). The K-function method on a network and its
|
|
332
|
+
computational implementation. *Geographical Analysis*, 33(3), 271-290.
|
|
333
|
+
- Sugihara, K., Satoh, T., & Okabe, A. (2010). Simple and unbiased kernel function for
|
|
334
|
+
network analysis. *Proceedings of the 10th International Symposium on Communications
|
|
335
|
+
and Information Technologies (ISCIT)*, 827-832.
|
|
336
|
+
- Xie, Z., & Yan, J. (2008). Kernel density estimation of traffic accidents in a network
|
|
337
|
+
space. *Computers, Environment and Urban Systems*, 32(5), 396-406.
|
|
338
|
+
|
|
339
|
+
## Citation
|
|
340
|
+
|
|
341
|
+
See `CITATION.cff`.
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT (c) 2026 Yusuf Eminoğlu
|