Skip to content

Commit 3f284d9

Browse files
rongboxujGaboardi
andauthored
Add the k nearest p-median module and the tutorial example for capacitated p-median (#397)
* add capacitated p-median case to the notebook * add the k nearest p median part to the p_median.py Create a new class * add the reference of k nearest p median * modify the KNearestPMedian class * Create test_knearest_p_median.py * add the part that can validate the initial k list * Update test_knearest_p_median.py * update the p_median and test_knearest_p_median.py change the name of k to k_array and add the tests for specific errors * update p-median.ipynb again add the intro about predefined facilities above cell 49, sorry for only adding it above cell 24 in the previous commit * clear all the output in p-median.ipynb * add 'pointpats' to the environment file * update the pointpats part * Update .ci/310.yaml Co-authored-by: James Gaboardi <[email protected]> * Update .ci/311-DEV.yaml Co-authored-by: James Gaboardi <[email protected]> * Update .ci/311.yaml Co-authored-by: James Gaboardi <[email protected]> * Update .ci/39.yaml Co-authored-by: James Gaboardi <[email protected]> * Update pyproject.toml (trigger a CI run) --------- Co-authored-by: James Gaboardi <[email protected]>
1 parent 99762ae commit 3f284d9

11 files changed

+951
-576
lines changed

.ci/310.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies:
1515
- shapely
1616
- spaghetti
1717
- tqdm
18+
- pointpats
1819
# testing
1920
- codecov
2021
- coverage

.ci/311-DEV.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- scipy
1515
- spaghetti
1616
- tqdm
17+
- pointpats
1718
# testing
1819
- codecov
1920
- coverage

.ci/311.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- scipy
1515
- spaghetti
1616
- tqdm
17+
- pointpats
1718
# testing
1819
- codecov
1920
- coverage

.ci/38-MIN.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies:
1313
- scipy=1.7
1414
- shapely=2.0
1515
- spaghetti
16+
- pointpats=2.3
1617
# testing
1718
- codecov
1819
- coverage

.ci/39.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- scipy
1515
- shapely
1616
- spaghetti
17+
- pointpats
1718
# testing
1819
- codecov
1920
- coverage

docs/_static/references.bib

+12
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,18 @@ @article{openshaw1995algorithms
168168
}
169169

170170

171+
@article{richard_2018,
172+
author = {Richard L. Church},
173+
title ={Tobler's Law and Spatial Optimization: Why Bakersfield?},
174+
journal = {International Regional Science Review},
175+
volume = {41},
176+
number = {3},
177+
pages = {287-310},
178+
year = {2018},
179+
doi = {10.1177/0160017616650612}
180+
}
181+
182+
171183
@article{shi_malik_2000,
172184
author={Jianbo Shi and Malik, J.},
173185
journal={{IEEE Transactions on Pattern Analysis and Machine Intelligence}},

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies:
1616
- scipy>=1.3.2
1717
- shapely>=2.0
1818
- tqdm>=4.27.0
19+
- pointpats>=2.3.0
1920

2021
# notebook/binder specific
2122
- folium

notebooks/p-median.ipynb

+283-571
Large diffs are not rendered by default.

pyproject.toml

+2-5
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ maintainers = [{ name = "spopt contributors" }]
1616
license = { text = "BSD 3-Clause" }
1717
description = "Spatial Optimization in PySAL"
1818
keywords = ["spatial optimization"]
19-
readme = { text = """\
20-
Spopt is an open-source Python library for solving optimization problems with spatial data. Originating from the `region` module in `PySAL`_ (Python Spatial Analysis Library), it is under active development for the inclusion of newly proposed models and methods for regionalization, facility location, and transportation-oriented solutions.
21-
22-
.. _PySAL: http://pysal.org
23-
""", content-type = "text/x-rst" }
19+
readme = "README.md"
2420
classifiers = [
2521
"Programming Language :: Python :: 3",
2622
"License :: OSI Approved :: BSD License",
@@ -42,6 +38,7 @@ dependencies = [
4238
"shapely>=2",
4339
"spaghetti",
4440
"tqdm>=4.27.0",
41+
"pointpats>=2.3.0"
4542
]
4643

4744

spopt/locate/p_median.py

+527
Large diffs are not rendered by default.

spopt/tests/test_knearest_p_median.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import numpy
2+
import geopandas
3+
import pandas
4+
import pulp
5+
from shapely.geometry import Point
6+
7+
from spopt.locate.p_median import KNearestPMedian
8+
import os
9+
import pickle
10+
import platform
11+
import pytest
12+
import warnings
13+
14+
15+
class TestKNearestPMedian:
16+
def setup_method(self) -> None:
17+
# Create the test data
18+
k = numpy.array([1, 1])
19+
self.demand_data = {
20+
"ID": [1, 2],
21+
"geometry": [Point(0.5, 1), Point(1.5, 1)],
22+
"demand": [1, 1],
23+
}
24+
self.facility_data = {
25+
"ID": [101, 102, 103],
26+
"geometry": [Point(1, 1), Point(0, 2), Point(2, 0)],
27+
"capacity": [1, 1, 1],
28+
}
29+
gdf_demand = geopandas.GeoDataFrame(self.demand_data, crs="EPSG:4326")
30+
gdf_fac = geopandas.GeoDataFrame(self.facility_data, crs="EPSG:4326")
31+
self.k_nearest_pmedian = KNearestPMedian.from_geodataframe(
32+
gdf_demand,
33+
gdf_fac,
34+
"geometry",
35+
"geometry",
36+
"demand",
37+
p_facilities=2,
38+
facility_capacity_col="capacity",
39+
k_array=k,
40+
)
41+
42+
def test_knearest_p_median_from_geodataframe(self):
43+
result = self.k_nearest_pmedian.solve(pulp.PULP_CBC_CMD(msg=False))
44+
assert isinstance(result, KNearestPMedian)
45+
46+
def test_knearest_p_median_from_geodataframe_no_results(self):
47+
result = self.k_nearest_pmedian.solve(
48+
pulp.PULP_CBC_CMD(msg=False), results=False
49+
)
50+
assert isinstance(result, KNearestPMedian)
51+
52+
with pytest.raises(AttributeError):
53+
result.cli2fac
54+
with pytest.raises(AttributeError):
55+
result.fac2cli
56+
with pytest.raises(AttributeError):
57+
result.mean_dist
58+
59+
def test_solve(self):
60+
solver = pulp.PULP_CBC_CMD(msg=False)
61+
self.k_nearest_pmedian.solve(solver)
62+
assert self.k_nearest_pmedian.problem.status == pulp.LpStatusOptimal
63+
64+
fac2cli_known = [[1], [0], []]
65+
cli2fac_known = [[1], [0]]
66+
mean_dist_known = 0.8090169943749475
67+
assert self.k_nearest_pmedian.fac2cli == fac2cli_known
68+
assert self.k_nearest_pmedian.cli2fac == cli2fac_known
69+
assert self.k_nearest_pmedian.mean_dist == mean_dist_known
70+
71+
def test_error_k_array_non_numpy_array(self):
72+
gdf_demand = geopandas.GeoDataFrame(self.demand_data, crs="EPSG:4326")
73+
gdf_fac = geopandas.GeoDataFrame(self.facility_data, crs="EPSG:4326")
74+
k = [1, 1]
75+
with pytest.raises(TypeError):
76+
KNearestPMedian.from_geodataframe(
77+
gdf_demand,
78+
gdf_fac,
79+
"geometry",
80+
"geometry",
81+
"demand",
82+
p_facilities=2,
83+
facility_capacity_col="capacity",
84+
k_array=k,
85+
)
86+
87+
def test_error_k_array_invalid_value(self):
88+
gdf_demand = geopandas.GeoDataFrame(self.demand_data, crs="EPSG:4326")
89+
gdf_fac = geopandas.GeoDataFrame(self.facility_data, crs="EPSG:4326")
90+
91+
k = numpy.array([1, 4])
92+
with pytest.raises(ValueError):
93+
KNearestPMedian.from_geodataframe(
94+
gdf_demand,
95+
gdf_fac,
96+
"geometry",
97+
"geometry",
98+
"demand",
99+
p_facilities=2,
100+
facility_capacity_col="capacity",
101+
k_array=k,
102+
)
103+
104+
def test_error_geodataframe_crs_mismatch(self):
105+
gdf_demand = geopandas.GeoDataFrame(self.demand_data, crs="EPSG:4326")
106+
gdf_fac = geopandas.GeoDataFrame(
107+
self.facility_data, crs="EPSG:3857"
108+
) # Different CRS
109+
110+
k = numpy.array([1, 1])
111+
with pytest.raises(ValueError):
112+
KNearestPMedian.from_geodataframe(
113+
gdf_demand,
114+
gdf_fac,
115+
"geometry",
116+
"geometry",
117+
"demand",
118+
p_facilities=2,
119+
facility_capacity_col="capacity",
120+
k_array=k,
121+
)

0 commit comments

Comments
 (0)