Skip to content

Commit 8286c8f

Browse files
authored
Merge pull request #496 from sjsrey/from_WSP
Implements W.from_WSP
2 parents 6415fbf + 2a2d9ff commit 8286c8f

File tree

5 files changed

+172
-32
lines changed

5 files changed

+172
-32
lines changed

.github/workflows/unittests.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
matrix:
2121
os: ['ubuntu-latest']
2222
environment-file: [
23-
ci/38-minimal.yaml,
2423
ci/38.yaml,
2524
ci/39.yaml,
2625
ci/310.yaml,

ci/38-minimal.yaml

Lines changed: 0 additions & 17 deletions
This file was deleted.

libpysal/weights/tests/test_weights.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import tempfile
33

44
import unittest
5+
import pytest
56
from ..weights import W, WSP
67
from .. import util
78
from ..util import WSP2W, lat2W
@@ -10,6 +11,7 @@
1011
from ... import examples
1112
from ..distance import KNN
1213
import numpy as np
14+
import scipy.sparse
1315

1416
NPTA3E = np.testing.assert_array_almost_equal
1517

@@ -44,6 +46,7 @@ def setUp(self):
4446
}
4547

4648
self.w3x3 = util.lat2W(3, 3)
49+
self.w_islands = W({0: [1], 1: [0, 2], 2: [1], 3: []})
4750

4851
def test_W(self):
4952
w = W(self.neighbors, self.weights, silence_warnings=True)
@@ -355,6 +358,33 @@ def test_roundtrip_write(self):
355358
new = W.from_file(path)
356359
np.testing.assert_array_equal(self.w.sparse.toarray(), new.sparse.toarray())
357360

361+
def test_to_sparse(self):
362+
sparse = self.w_islands.to_sparse()
363+
np.testing.assert_array_equal(sparse.data, [1, 1, 1, 1, 0])
364+
np.testing.assert_array_equal(sparse.row, [0, 1, 1, 2, 3])
365+
np.testing.assert_array_equal(sparse.col, [1, 0, 2, 1, 3])
366+
sparse = self.w_islands.to_sparse("bsr")
367+
self.assertIsInstance(sparse, scipy.sparse._arrays.bsr_array)
368+
sparse = self.w_islands.to_sparse("csr")
369+
self.assertIsInstance(sparse, scipy.sparse._arrays.csr_array)
370+
sparse = self.w_islands.to_sparse("coo")
371+
self.assertIsInstance(sparse, scipy.sparse._arrays.coo_array)
372+
sparse = self.w_islands.to_sparse("csc")
373+
self.assertIsInstance(sparse, scipy.sparse._arrays.csc_array)
374+
sparse = self.w_islands.to_sparse()
375+
self.assertIsInstance(sparse, scipy.sparse._arrays.coo_array)
376+
377+
def test_sparse_fmt(self):
378+
with pytest.raises(ValueError) as exc_info:
379+
sparse = self.w_islands.to_sparse("dog")
380+
381+
def test_from_sparse(self):
382+
sparse = self.w_islands.to_sparse()
383+
w = W.from_sparse(sparse)
384+
self.assertEqual(w.n, 4)
385+
self.assertEqual(len(w.islands), 0)
386+
self.assertEqual(w.neighbors[3], [3])
387+
358388

359389
class Test_WSP_Back_To_W(unittest.TestCase):
360390
# Test to make sure we get back to the same W functionality
@@ -689,6 +719,11 @@ def test_trcWtW_WW(self):
689719
def test_s0(self):
690720
self.assertEqual(self.w3x3.s0, 24.0)
691721

722+
def test_from_WSP(self):
723+
w = W.from_WSP(self.wsp)
724+
self.assertEqual(w.n, 100)
725+
self.assertEqual(w.pct_nonzero, 4.62)
726+
692727

693728
if __name__ == "__main__":
694729
unittest.main()

libpysal/weights/util.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
try:
2626
from shapely.geometry.base import BaseGeometry
27+
2728
HAS_SHAPELY = True
2829
except ImportError:
2930
HAS_SHAPELY = False
@@ -534,17 +535,17 @@ def higher_order_sp(
534535
)
535536

536537
if lower_order:
537-
wk = sum(map(lambda x: w ** x, range(2, k + 1)))
538+
wk = sum(map(lambda x: w**x, range(2, k + 1)))
538539
shortest_path = False
539540
else:
540-
wk = w ** k
541+
wk = w**k
541542

542543
rk, ck = wk.nonzero()
543544
sk = set(zip(rk, ck))
544545

545546
if shortest_path:
546547
for j in range(1, k):
547-
wj = w ** j
548+
wj = w**j
548549
rj, cj = wj.nonzero()
549550
sj = set(zip(rj, cj))
550551
sk.difference_update(sj)
@@ -826,14 +827,12 @@ def WSP2W(wsp, **kwargs):
826827
827828
828829
"""
829-
wsp.sparse
830-
indices = wsp.sparse.indices
831830
data = wsp.sparse.data
832831
indptr = wsp.sparse.indptr
833832
id_order = wsp.id_order
834833
if id_order:
835834
# replace indices with user IDs
836-
indices = [id_order[i] for i in indices]
835+
indices = [id_order[i] for i in wsp.sparse.indices]
837836
else:
838837
id_order = list(range(wsp.n))
839838
neighbors, weights = {}, {}
@@ -1086,12 +1085,7 @@ def get_points_array(iterable):
10861085
]
10871086
)
10881087
else:
1089-
data = np.vstack(
1090-
[
1091-
np.array(shape.centroid)
1092-
for shape in first_choice
1093-
]
1094-
)
1088+
data = np.vstack([np.array(shape.centroid) for shape in first_choice])
10951089
except AttributeError:
10961090
data = np.vstack([shape for shape in backup])
10971091
return data

libpysal/weights/weights.py

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import numpy as np
1111
import scipy.sparse
1212
from scipy.sparse.csgraph import connected_components
13+
from sklearn import preprocessing
14+
from collections import defaultdict
1315

1416
# from .util import full, WSP2W resolve import cycle by
1517
# forcing these into methods
@@ -232,7 +234,67 @@ def from_shapefile(cls, *args, **kwargs):
232234

233235
@classmethod
234236
def from_WSP(cls, WSP, silence_warnings=True):
235-
return WSP2W(WSP, silence_warnings=silence_warnings)
237+
"""Create a pysal W from a pysal WSP object (thin weights matrix).
238+
239+
Parameters
240+
----------
241+
wsp : WSP
242+
PySAL sparse weights object
243+
244+
silence_warnings : bool
245+
By default ``libpysal`` will print a warning if the dataset contains
246+
any disconnected components or islands. To silence this warning set this
247+
parameter to ``True``.
248+
249+
250+
Returns
251+
-------
252+
w : W
253+
PySAL weights object
254+
255+
Examples
256+
--------
257+
>>> from libpysal.weights import lat2W, WSP, W
258+
259+
Build a 10x10 scipy.sparse matrix for a rectangular 2x5 region of cells
260+
(rook contiguity), then construct a PySAL sparse weights object (wsp).
261+
262+
>>> sp = lat2SW(2, 5)
263+
>>> wsp = WSP(sp)
264+
>>> wsp.n
265+
10
266+
>>> wsp.sparse[0].todense()
267+
matrix([[0, 1, 0, 0, 0, 1, 0, 0, 0, 0]], dtype=int8)
268+
269+
Create a standard PySAL W from this sparse weights object.
270+
271+
>>> w = W.from_WSP(wsp)
272+
>>> w.n
273+
10
274+
>>> print(w.full()[0][0])
275+
[0 1 0 0 0 1 0 0 0 0]
276+
"""
277+
data = WSP.sparse.data
278+
indptr = WSP.sparse.indptr
279+
id_order = WSP.id_order
280+
if id_order:
281+
# replace indices with user IDs
282+
indices = [id_order[i] for i in WSP.sparse.indices]
283+
else:
284+
id_order = list(range(WSP.n))
285+
neighbors, weights = {}, {}
286+
start = indptr[0]
287+
for i in range(WSP.n):
288+
oid = id_order[i]
289+
end = indptr[i + 1]
290+
neighbors[oid] = indices[start:end]
291+
weights[oid] = data[start:end]
292+
start = end
293+
ids = copy.copy(WSP.id_order)
294+
w = W(neighbors, weights, ids, silence_warnings=silence_warnings)
295+
w._sparse = copy.deepcopy(WSP.sparse)
296+
w._cache["sparse"] = w._sparse
297+
return w
236298

237299
@classmethod
238300
def from_adjlist(
@@ -385,6 +447,73 @@ def sparse(self):
385447
self._cache["sparse"] = self._sparse
386448
return self._sparse
387449

450+
@classmethod
451+
def from_sparse(cls, sparse):
452+
"""Convert a ``scipy.sparse`` array to a PySAL ``W`` object.
453+
454+
Parameters
455+
----------
456+
sparse : scipy.sparse array
457+
458+
Returns
459+
-------
460+
w : libpysal.weights.W
461+
A ``W`` object containing the same graph as the ``scipy.sparse`` graph.
462+
463+
464+
Notes
465+
-----
466+
When the sparse array has a zero in its data attribute, and
467+
the corresponding row and column values are equal, the value
468+
for the pysal weight will be 0 for the "loop".
469+
"""
470+
coo = sparse.tocoo()
471+
neighbors = defaultdict(list)
472+
weights = defaultdict(list)
473+
for k, v, w in zip(coo.row, coo.col, coo.data):
474+
neighbors[k].append(v)
475+
weights[k].append(w)
476+
return W(neighbors=neighbors, weights=weights)
477+
478+
def to_sparse(self, fmt="coo"):
479+
"""Generate a ``scipy.sparse`` array object from a pysal W.
480+
481+
Parameters
482+
----------
483+
fmt : {'bsr', 'coo', 'csc', 'csr'}
484+
scipy.sparse format
485+
486+
Returns
487+
-------
488+
scipy.sparse array
489+
A scipy.sparse array with a format of fmt.
490+
491+
Notes
492+
-----
493+
The keys of the w.neighbors are encoded
494+
to determine row,col in the sparse array.
495+
496+
"""
497+
disp = {}
498+
disp["bsr"] = scipy.sparse.bsr_array
499+
disp["coo"] = scipy.sparse.coo_array
500+
disp["csc"] = scipy.sparse.csc_array
501+
disp["csr"] = scipy.sparse.csr_array
502+
fmt_l = fmt.lower()
503+
if fmt_l in disp:
504+
adj_list = self.to_adjlist(drop_islands=False)
505+
data = adj_list.weight
506+
row = adj_list.focal
507+
col = adj_list.neighbor
508+
le = preprocessing.LabelEncoder()
509+
le.fit(row)
510+
row = le.transform(row)
511+
col = le.transform(col)
512+
n = self.n
513+
return disp[fmt_l]((data, (row, col)), shape=(n, n))
514+
else:
515+
raise ValueError(f"unsupported sparse format: {fmt}")
516+
388517
@property
389518
def n_components(self):
390519
"""Store whether the adjacency matrix is fully connected."""
@@ -1306,7 +1435,7 @@ def plot(
13061435
ax.scatter(
13071436
gdf.centroid.apply(lambda p: p.x),
13081437
gdf.centroid.apply(lambda p: p.y),
1309-
**node_kws
1438+
**node_kws,
13101439
)
13111440
return f, ax
13121441

0 commit comments

Comments
 (0)