Skip to content

Commit f3967d0

Browse files
sjsreyknaaptimeknaaptimeljwolfknaaptime
authored
ENH: Add decay and taper arguments to normalize kernel in distance-based weights (#791)
* Allow for normalize=True for gaussian kernel as pdf, False K(0)=1 * bool not binary for type * Add tests for normalize kw in Kernel * numpy testing * Correct reference for kernel defs * prototyping new _kernels.py * triangular (#29) Co-authored-by: knaaptime <knaaptime@Mac.attlocal.net> * move primative kernels out to libpysal._kernels.py * renaming kernels.py * populate self-weight with K(0) * modify imports * roll back graph kernels * Add tests and illustrative nb * Correct Gaussian kernel function * Update tests for gaussian correction * ruff format rules and remove test nbs * ruff tests * Relocate kernels from graph to libpysal for reuse and centralization * move taper/decay logic out of graph module (#30) * replay levi's change to not remove self-weight (#32) Co-authored-by: knaaptime <knaaptime@magneto.local> * gaussian constant term * update tests for trim * import proper kernel.py in graph * Add exclude_loops=True for kernels on graph * Fix gaussian kernel. * The coplanar tests assume we don't taper. In main this could be a bug? * clean up doc strings * update tests for correct Gaussian kernel * update tests for gaussian kernel * Do not decay boxcar kernel * Update reference for kernels * cast boxcar kernel to float * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * formatting and import sorting * skip tests if pyproj not available * Spelling * np.in1d is deprecated, using np.isin instead * Don't assign to unused variable in pytest * Docs for kernels * ruff formatting * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff take 2 * more narrative on kernels * Pass decay and trim kw args into graph kernels * add decay to graph/_kernel.py * If k is specified we need the bandwith set to max distance * Handle taper appropriately * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ruff * Propagate decay and taper kwargs * ruff * ruff * use lps kernel * Propagate decay and taper to triangulation builders * update build_triangulation * pass decay and taper in triangulation functions --------- Co-authored-by: eli knaap <ek@knaaptime.com> Co-authored-by: knaaptime <knaaptime@Mac.attlocal.net> Co-authored-by: Levi John Wolf <levi.john.wolf@gmail.com> Co-authored-by: knaaptime <knaaptime@magneto.local> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: James Gaboardi <jgaboardi@gmail.com>
1 parent d222c9b commit f3967d0

16 files changed

Lines changed: 1036 additions & 130 deletions

‎docs/api.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,17 @@ io
255255
libpysal.io.fileio.FileIO
256256

257257

258+
kernels
259+
-------
260+
261+
262+
.. autosummary::
263+
:toctree: generated/
264+
265+
libpysal.kernels.kernel
266+
267+
268+
258269
examples
259270
--------
260271

‎docs/index.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
libpysal: Python Spatial Analysis Library Core
44
==============================================
55

6-
.. image:: https://github.com/pysal/libpysal/workflows/.github/workflows/unittests.yml/badge.svg
7-
:target: https://github.com/pysal/libpysal/actions?query=workflow%3A.github%2Fworkflows%2Funittests.yml
8-
6+
.. image:: https://github.com/pysal/libpysal/actions/workflows/.github/workflows/unittests.yml/badge.svg
7+
:target: https://github.com/pysal/libpysal/actions/workflows/unittests.yml
8+
99
.. image:: https://img.shields.io/badge/Discord-join%20chat-7289da?style=flat&logo=discord&logoColor=cccccc
1010
:target: https://discord.gg/BxFTEPFFZn
1111

‎docs/migration.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Members common to W and Graph
1818

1919

2020
+-----------------------------------------------------------------------------------------+------------------+
21-
| Member | Typee |
21+
| Member | Type |
2222
+=========================================================================================+==================+
2323
| `asymmetry <generated/libpysal.graph.Graph.html#libpysal.graph.Graph.asymmetry>`_ | builtins.method |
2424
+-----------------------------------------------------------------------------------------+------------------+

‎docs/user-guide/intro.rst‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ This user guide covers essential features of libpysal, mostly in the form of int
77
- work with built-in example spatial data sets
88
- how to use spatial weights
99
- how to use spatial graphs
10+
- how to use kernels
1011

1112
Notebooks cover just a small selection of functions as an illustration of
12-
principles. For a full overview of momepy capabilities, head to the `API <../api.rst>`_.
13+
principles. For a full overview of libpysal's capabilities, head to the `API <../api.rst>`_.
1314

1415

1516
.. toctree::
@@ -18,3 +19,4 @@ principles. For a full overview of momepy capabilities, head to the `API <../api
1819
Datasets <data/examples>
1920
Weights <weights/intro>
2021
Graphs <graph/intro>
22+
Kernels <kernels>

‎docs/user-guide/kernels.ipynb‎

Lines changed: 368 additions & 0 deletions
Large diffs are not rendered by default.

‎libpysal/graph/_kernel.py‎

Lines changed: 23 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import pandas
33
from scipy import optimize, sparse, spatial, stats
44

5+
from libpysal.kernels import _kernel_functions
6+
from libpysal.kernels import kernel as _lps_kernel
7+
58
from ._utils import (
69
CoplanarError,
710
_build_coplanarity_lookup,
@@ -22,59 +25,6 @@
2225
_VALID_GEOMETRY_TYPES = ["Point"]
2326

2427

25-
def _triangular(distances, bandwidth):
26-
u = numpy.clip(distances / bandwidth, 0, 1)
27-
return 1 - u
28-
29-
30-
def _parabolic(distances, bandwidth):
31-
u = numpy.clip(distances / bandwidth, 0, 1)
32-
return 0.75 * (1 - u**2)
33-
34-
35-
def _gaussian(distances, bandwidth):
36-
u = distances / bandwidth
37-
return numpy.exp(-((u / 2) ** 2)) / (numpy.sqrt(2 * numpy.pi))
38-
39-
40-
def _bisquare(distances, bandwidth):
41-
u = numpy.clip(distances / bandwidth, 0, 1)
42-
return (15 / 16) * (1 - u**2) ** 2
43-
44-
45-
def _cosine(distances, bandwidth):
46-
u = numpy.clip(distances / bandwidth, 0, 1)
47-
return (numpy.pi / 4) * numpy.cos(numpy.pi / 2 * u)
48-
49-
50-
def _exponential(distances, bandwidth):
51-
u = distances / bandwidth
52-
return numpy.exp(-u)
53-
54-
55-
def _boxcar(distances, bandwidth):
56-
r = (distances < bandwidth).astype(int)
57-
return r
58-
59-
60-
def _identity(distances, _):
61-
return distances
62-
63-
64-
_kernel_functions = {
65-
"triangular": _triangular,
66-
"parabolic": _parabolic,
67-
"gaussian": _gaussian,
68-
"bisquare": _bisquare,
69-
"cosine": _cosine,
70-
"boxcar": _boxcar,
71-
"discrete": _boxcar,
72-
"exponential": _exponential,
73-
"identity": _identity,
74-
None: _identity,
75-
}
76-
77-
7828
def _kernel(
7929
coordinates,
8030
bandwidth=None,
@@ -84,8 +34,10 @@ def _kernel(
8434
ids=None,
8535
p=2,
8636
taper=True,
37+
decay=False,
8738
coplanar="raise",
8839
resolve_isolates=True,
40+
exclude_self_weights=True,
8941
):
9042
"""
9143
Compute a kernel function over a distance matrix.
@@ -132,8 +84,18 @@ def _kernel(
13284
parameter for minkowski metric, ignored if metric != "minkowski".
13385
taper : bool (default: True)
13486
remove links with a weight equal to zero
87+
decay : bool (default: False)
88+
whether to calculate the kernel using the decay formulation.
89+
In the decay form, a kernel measures the distance decay in
90+
similarity between observations. It varies from from maximal
91+
similarity (1) at a distance of zero to minimal similarity (0
92+
or negative) at some very large (possibly infinite) distance.
93+
Otherwise, kernel functions are treated as proper
94+
volume-preserving probability distributions.
13595
resolve_isolates : bool
13696
Try to resolve isolates. Can be disabled if we are dealing with cliques later.
97+
exclude_self_weights : bool (default: True)
98+
Remove self-weights
13799
"""
138100
if metric != "precomputed":
139101
coordinates, ids, _ = _validate_geometry_input(
@@ -188,16 +150,17 @@ def _kernel(
188150
data = sq.flatten()
189151
i = numpy.tile(numpy.arange(sq.shape[0]), sq.shape[0])
190152
j = numpy.repeat(numpy.arange(sq.shape[0]), sq.shape[0])
191-
# remove diagonal
192-
data = numpy.delete(data, numpy.arange(0, data.size, sq.shape[0] + 1))
193-
i = numpy.delete(i, numpy.arange(0, i.size, sq.shape[0] + 1))
194-
j = numpy.delete(j, numpy.arange(0, j.size, sq.shape[0] + 1))
195-
# construct sparse
153+
154+
if exclude_self_weights:
155+
data = numpy.delete(data, numpy.arange(0, data.size, sq.shape[0] + 1))
156+
i = numpy.delete(i, numpy.arange(0, i.size, sq.shape[0] + 1))
157+
j = numpy.delete(j, numpy.arange(0, j.size, sq.shape[0] + 1))
158+
196159
d = sparse.csc_array((data, (i, j)))
197160
else:
198161
d = sparse.csc_array(coordinates)
199162
if bandwidth is None:
200-
bandwidth = numpy.percentile(d.data, 25)
163+
bandwidth = numpy.percentile(d.data, 25) if k is None else d.data.max()
201164
elif bandwidth == "auto":
202165
if (kernel == "identity") or (kernel is None):
203166
bandwidth = numpy.nan # ignored by identity
@@ -206,11 +169,9 @@ def _kernel(
206169
if callable(kernel):
207170
d.data = kernel(d.data, bandwidth)
208171
else:
209-
d.data = _kernel_functions[kernel](d.data, bandwidth)
210-
172+
d.data = _lps_kernel(d.data, bandwidth, kernel=kernel, taper=taper, decay=decay)
211173
if taper:
212174
d.eliminate_zeros()
213-
214175
return _sparse_to_arrays(d, ids=ids, resolve_isolates=resolve_isolates)
215176

216177

‎libpysal/graph/_network.py‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import numpy as np
22

3-
from ._kernel import _kernel_functions
3+
from ._kernel import _lps_kernel
44
from ._utils import _induce_cliques, _validate_geometry_input
55

66

@@ -62,7 +62,9 @@ def pdna_to_adj(origins, network, node_ids, threshold):
6262
return adj
6363

6464

65-
def build_travel_graph(df, network, threshold, mapping_distance, kernel=None):
65+
def build_travel_graph(
66+
df, network, threshold, mapping_distance, kernel=None, taper=True, decay=False
67+
):
6668
"""Compute the shortest path between gdf centroids via a pandana.Network
6769
and return an adjacency list with weight=cost. Note unlike distance_band,
6870
:math:`G_{ij}` and :math:`G_{ji}` are often different because travel networks
@@ -87,6 +89,16 @@ def build_travel_graph(df, network, threshold, mapping_distance, kernel=None):
8789
libpysal.graph.Graph.build_kernel for more information on kernel
8890
transformation options. Default is None, in which case the Graph weight
8991
is pure distance between focal and neighbor
92+
taper : bool (default: True)
93+
remove links with a weight equal to zero
94+
decay : bool (default: False)
95+
whether to calculate the kernel using the decay formulation.
96+
In the decay form, a kernel measures the distance decay in
97+
similarity between observations. It varies from from maximal
98+
similarity (1) at a distance of zero to minimal similarity (0
99+
or negative) at some very large (possibly infinite) distance.
100+
Otherwise, kernel functions are treated as proper
101+
volume-preserving probability distributions.
90102
91103
Returns
92104
-------
@@ -125,10 +137,16 @@ def build_travel_graph(df, network, threshold, mapping_distance, kernel=None):
125137
)
126138

127139
if callable(kernel):
128-
adj_cliques["weight"] = kernel(adj_cliques["weight"], threshold)
140+
adj_cliques["weight"] = kernel(
141+
adj_cliques["weight"], threshold, decay=decay, taper=taper
142+
)
129143
else:
130-
adj_cliques["weight"] = _kernel_functions[kernel](
131-
adj_cliques["weight"], threshold
144+
adj_cliques["weight"] = _lps_kernel(
145+
adj_cliques["weight"].values,
146+
kernel=kernel,
147+
bandwidth=threshold,
148+
decay=decay,
149+
taper=taper,
132150
)
133151

134152
return adj_cliques

‎libpysal/graph/_triangulation.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def tri_with_validation(
5050
kernel=None,
5151
bandwidth=None,
5252
seed=None,
53+
decay=False,
54+
taper=False,
5355
**kwargs,
5456
):
5557
if coplanar not in ["raise", "jitter", "clique"]:
@@ -85,8 +87,9 @@ def tri_with_validation(
8587
metric="precomputed",
8688
kernel=kernel,
8789
bandwidth=bandwidth,
88-
taper=False,
90+
taper=taper,
8991
resolve_isolates=False, # no isolates in triangulation
92+
decay=decay,
9093
)
9194
# create adjacency
9295
adjtable = pandas.DataFrame.from_dict(

0 commit comments

Comments
 (0)