From d3c0d3582ddef625befd9811743bc0e0de364ff5 Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 11:14:33 -0500 Subject: [PATCH 1/8] stash --- spopt/region/ward.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spopt/region/ward.py b/spopt/region/ward.py index 32baca61..8edb694e 100644 --- a/spopt/region/ward.py +++ b/spopt/region/ward.py @@ -1,27 +1,26 @@ -from ..BaseClass import BaseSpOptHeuristicSolver from sklearn.cluster import AgglomerativeClustering +from ..BaseClass import BaseSpOptHeuristicSolver + class WardSpatial(BaseSpOptHeuristicSolver): - """Agglomerative clustering using Ward linkage with a spatial connectivity constraint. + """Agglomerative clustering using Ward + linkage with a spatial connectivity constraint. Parameters ---------- - gdf : geopandas.GeoDataFrame, required - Geodataframe containing original data - - w : libpysal.weights.W, required - Weights object created from given data - - attrs_name : list, required + gdf : geopandas.GeoDataFrame + Geodataframe containing original data. + w : libpysal.weights.W + Weights object created from given data. + attrs_name : list Strings for attribute names (cols of ``geopandas.GeoDataFrame``). - - n_clusters : int, optional, default: 5 + n_clusters : int (default 5) The number of clusters to form. - - clustering_kwds: dictionary, optional, default: dict() - Other parameters about clustering could be used in sklearn.cluster.AgglometariveClustering. + clustering_kwds: dict + Other parameters about clustering could be used in + ``sklearn.cluster.AgglometariveClustering.`` Returns ------- @@ -33,13 +32,14 @@ class WardSpatial(BaseSpOptHeuristicSolver): Examples -------- - >>> import numpy as np + >>> import geopandas >>> import libpysal - >>> import geopandas as gpd + >>> import numpy >>> from spopt.region import WardSpatial Read the data. + >>> libpysal.examples.load_example('AirBnB') >>> pth = libpysal.examples.get_path('airbnb_Chicago 2015.shp') >>> chicago = gpd.read_file(pth) @@ -49,7 +49,7 @@ class WardSpatial(BaseSpOptHeuristicSolver): >>> attrs_name = ['num_spots'] >>> n_clusters = 8 - Run the skater algorithm. + Run the ``WardSpatial`` algorithm. >>> model = WardSpatial(chicago, w, attrs_name, n_clusters) >>> model.solve() From 857603aae9ba60d24431232ca097133d3bf58bab Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 15:22:45 -0500 Subject: [PATCH 2/8] random_region docs, etc. --- spopt/region/random_region.py | 292 +++++++++++++++------------------- 1 file changed, 130 insertions(+), 162 deletions(-) diff --git a/spopt/region/random_region.py b/spopt/region/random_region.py index c9774d18..b00132f2 100644 --- a/spopt/region/random_region.py +++ b/spopt/region/random_region.py @@ -1,14 +1,16 @@ """ -Generate random regions +Generate random regions. -Randomly form regions given various types of constraints on cardinality and -composition. +Randomly form regions given various types of constraints +on cardinality and composition. """ __author__ = "David Folch David.Folch@nau.edu, Serge Rey sergio.rey@ucr.edu" -import numpy as np import copy + +import numpy as np + from .components import check_contiguity __all__ = ["RandomRegions", "RandomRegion"] @@ -20,125 +22,109 @@ class RandomRegions: Parameters ---------- - area_ids : list - IDs indexing the areas to be grouped into regions (must - be in the same order as spatial weights matrix if this - is provided) - - num_regions : integer - number of regions to generate (if None then this is - chosen randomly from 2 to n where n is the number of - areas) - - cardinality : list - list containing the number of areas to assign to regions - (if num_regions is also provided then len(cardinality) - must equal num_regions; if cardinality=None then a list - of length num_regions will be generated randomly) - - contiguity : W - spatial weights object (if None then contiguity will be - ignored) - - maxiter : int - maximum number attempts (for each permutation) at finding - a feasible solution (only affects contiguity constrained - regions) - - compact : boolean - attempt to build compact regions, note (only affects - contiguity constrained regions) - - max_swaps : int - maximum number of swaps to find a feasible solution - (only affects contiguity constrained regions) - - permutations : int - number of RandomRegion instances to generate + area_ids : list + The IDs indexing the areas to be grouped into regions (must be in + the same order as spatial weights matrix if this is provided). + num_regions : int (default None) + The number of regions to generate (if ``None`` then this is chosen + randomly from 2 to :math:`n` where :math:`n` is the number of areas). + cardinality : list (default None) + A list containing the number of areas to assign to regions + (if ``num_regions`` is also provided then ``len(cardinality)`` + must equal ``num_regions``; if ``None`` then a list of length + ``num_regions`` will be generated randomly). + contiguity : libpysal.weights.W (default None) + A spatial weights object (if ``None`` then contiguity will be ignored). + maxiter : int (default 100) + The maximum number attempts (for each permutation) at finding + a feasible solution (only affects contiguity constrained regions). + compact : bool (default False) + Attempt to build compact regions (only affects contiguity constrained regions). + max_swaps : int (default 1000000) + The maximum number of swaps to find a feasible solution + (only affects contiguity constrained regions). + permutations : int (default 99) + The number of ``RandomRegion`` instances to generate. Attributes ---------- - solutions : list - list of length permutations containing all RandomRegion instances generated - - solutions_feas : list - list of the RandomRegion instances that resulted in feasible solutions + solutions : list + A list of length ``permutations`` containing all + ``RandomRegion`` instances generated. + solutions_feas : list + A list of the ``RandomRegion`` instances that resulted in feasible solutions. Examples -------- - Setup the data + Setup the data. - >>> import random - >>> import numpy as np >>> import libpysal + >>> import numpy + >>> from spopt.region import RandomRegions, RandomRegion >>> nregs = 13 - >>> cards = range(2,14) + [10] - >>> w = liblibpysal.weights.lat2W(10,10,rook=True) + >>> cards = list(range(2,14)) + [10] + >>> w = libpysal.weights.lat2W(10,10,rook=True) >>> ids = w.id_order - Unconstrained + Unconstrained: - >>> random.seed(10) - >>> np.random.seed(10) - >>> t0 = spopt.region.RandomRegions(ids, permutations=2) + >>> numpy.random.seed(10) + >>> t0 = RandomRegions(ids, permutations=2) >>> t0.solutions[0].regions[0] [19, 14, 43, 37, 66, 3, 79, 41, 38, 68, 2, 1, 60] - Cardinality and contiguity constrained (num_regions implied) + Cardinality and contiguity constrained (``num_regions`` implied): - >>> random.seed(60) - >>> np.random.seed(60) - >>> t1 = spopt.region.RandomRegions(ids, num_regions=nregs, cardinality=cards, contiguity=w, permutations=2) + >>> numpy.random.seed(60) + >>> t1 = RandomRegions( + ... ids, num_regions=nregs, cardinality=cards, contiguity=w, permutations=2 + ... ) >>> t1.solutions[0].regions[0] [62, 61, 81, 71, 64, 90, 72, 51, 80, 63, 50, 73, 52] - Cardinality constrained (num_regions implied) + Cardinality constrained (``num_regions`` implied): - >>> random.seed(100) - >>> np.random.seed(100) - >>> t2 = spopt.region.RandomRegions(ids, num_regions=nregs, cardinality=cards, permutations=2) + >>> numpy.random.seed(100) + >>> t2 = RandomRegions( + ... ids, num_regions=nregs, cardinality=cards, permutations=2 + ... ) >>> t2.solutions[0].regions[0] [37, 62] - Number of regions and contiguity constrained + Number of regions and contiguity constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t3 = spopt.region.RandomRegions(ids, num_regions=nregs, contiguity=w, permutations=2) + >>> numpy.random.seed(100) + >>> t3 = RandomRegions(ids, num_regions=nregs, contiguity=w, permutations=2) >>> t3.solutions[0].regions[1] [62, 52, 51, 63, 61, 73, 41, 53, 60, 83, 42, 31, 32] - Cardinality and contiguity constrained + Cardinality and contiguity constrained: - >>> random.seed(60) - >>> np.random.seed(60) - >>> t4 = spopt.region.RandomRegions(ids, cardinality=cards, contiguity=w, permutations=2) + >>> numpy.random.seed(60) + >>> t4 = RandomRegions(ids, cardinality=cards, contiguity=w, permutations=2) >>> t4.solutions[0].regions[0] [62, 61, 81, 71, 64, 90, 72, 51, 80, 63, 50, 73, 52] - Number of regions constrained + Number of regions constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t5 = spopt.region.RandomRegions(ids, num_regions=nregs, permutations=2) + >>> numpy.random.seed(100) + >>> t5 = RandomRegions(ids, num_regions=nregs, permutations=2) >>> t5.solutions[0].regions[0] [37, 62, 26, 41, 35, 25, 36] - Cardinality constrained + Cardinality constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t6 = spopt.region.RandomRegions(ids, cardinality=cards, permutations=2) + >>> numpy.random.seed(100) + >>> t6 = RandomRegions(ids, cardinality=cards, permutations=2) >>> t6.solutions[0].regions[0] [37, 62] - Contiguity constrained + Contiguity constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t7 = spopt.region.RandomRegions(ids, contiguity=w, permutations=2) + >>> numpy.random.seed(100) + >>> t7 = RandomRegions(ids, contiguity=w, permutations=2) >>> t7.solutions[0].regions[1] [62, 61, 71, 63] @@ -172,134 +158,113 @@ def __init__( self.solutions = solutions self.solutions_feas = [] for i in solutions: - if i.feasible == True: + if i.feasible: self.solutions_feas.append(i) class RandomRegion: - """Randomly combine a given set of areas into two or more regions based - on various constraints. - + """Randomly combine a given set of areas into two + or more regions based on various constraints. Parameters ---------- - area_ids : list - IDs indexing the areas to be grouped into regions (must - be in the same order as spatial weights matrix if this - is provided) - - num_regions : integer - number of regions to generate (if None then this is - chosen randomly from 2 to n where n is the number of - areas) - - cardinality : list - list containing the number of areas to assign to regions - (if num_regions is also provided then len(cardinality) - must equal num_regions; if cardinality=None then a list - of length num_regions will be generated randomly) - - contiguity : W - spatial weights object (if None then contiguity will be - ignored) - - maxiter : int - maximum number attempts at finding a feasible solution - (only affects contiguity constrained regions) - - compact : boolean - attempt to build compact regions (only affects - contiguity constrained regions) - - max_swaps : int - maximum number of swaps to find a feasible solution - (only affects contiguity constrained regions) + area_ids : list + The IDs indexing the areas to be grouped into regions (must be in + the same order as spatial weights matrix if this is provided). + num_regions : int (default None) + The number of regions to generate (if ``None`` then this is chosen + randomly from 2 to :math:`n` where :math:`n` is the number of areas). + cardinality : list (default None) + A list containing the number of areas to assign to regions + (if ``num_regions`` is also provided then ``len(cardinality)`` + must equal ``num_regions``; if ``None`` then a list of length + ``num_regions`` will be generated randomly). + contiguity : libpysal.weights.W (default None) + A spatial weights object (if ``None`` then contiguity will be ignored). + maxiter : int (default 100) + The maximum number attempts (for each permutation) at finding + a feasible solution (only affects contiguity constrained regions). + compact : bool (default False) + Attempt to build compact regions (only affects contiguity constrained regions). + max_swaps : int (default 1000000) + The maximum number of swaps to find a feasible solution + (only affects contiguity constrained regions). Attributes ---------- - feasible : boolean - if True then solution was found - - regions : list - list of lists of regions (each list has the ids of areas - in that region) + feasible : bool + If ``True`` then solution was found. + regions : list + A list of lists of regions where each list has the IDs of areas in that region. Examples -------- - Setup the data + Setup the data. - >>> import random - >>> import numpy as np >>> import libpysal + >>> import numpy + >>> from spopt.region import RandomRegions, RandomRegion >>> nregs = 13 - >>> cards = range(2,14) + [10] + >>> cards = list(range(2,14)) + [10] >>> w = libpysal.weights.lat2W(10,10,rook=True) >>> ids = w.id_order - Unconstrained + Unconstrained: - >>> random.seed(10) - >>> np.random.seed(10) - >>> t0 = spopt.region.RandomRegion(ids) + >>> numpy.random.seed(10) + >>> t0 = RandomRegion(ids) >>> t0.regions[0] [19, 14, 43, 37, 66, 3, 79, 41, 38, 68, 2, 1, 60] - Cardinality and contiguity constrained (num_regions implied) + Cardinality and contiguity constrained (``num_regions`` implied): - >>> random.seed(60) - >>> np.random.seed(60) - >>> t1 = spopt.region.RandomRegion(ids, num_regions=nregs, cardinality=cards, contiguity=w) + >>> numpy.random.seed(60) + >>> t1 = RandomRegion(ids, num_regions=nregs, cardinality=cards, contiguity=w) >>> t1.regions[0] [62, 61, 81, 71, 64, 90, 72, 51, 80, 63, 50, 73, 52] - Cardinality constrained (num_regions implied) + Cardinality constrained (``num_regions`` implied): - >>> random.seed(100) - >>> np.random.seed(100) - >>> t2 = spopt.region.RandomRegion(ids, num_regions=nregs, cardinality=cards) + >>> numpy.random.seed(100) + >>> t2 = RandomRegion(ids, num_regions=nregs, cardinality=cards) >>> t2.regions[0] [37, 62] - Number of regions and contiguity constrained + Number of regions and contiguity constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t3 = spopt.region.RandomRegion(ids, num_regions=nregs, contiguity=w) + >>> numpy.random.seed(100) + >>> t3 = RandomRegion(ids, num_regions=nregs, contiguity=w) >>> t3.regions[1] [62, 52, 51, 63, 61, 73, 41, 53, 60, 83, 42, 31, 32] - Cardinality and contiguity constrained + Cardinality and contiguity constrained: - >>> random.seed(60) - >>> np.random.seed(60) - >>> t4 = spopt.region.RandomRegion(ids, cardinality=cards, contiguity=w) + >>> numpy.random.seed(60) + >>> t4 = RandomRegion(ids, cardinality=cards, contiguity=w) >>> t4.regions[0] [62, 61, 81, 71, 64, 90, 72, 51, 80, 63, 50, 73, 52] - Number of regions constrained + Number of regions constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t5 = spopt.region.RandomRegion(ids, num_regions=nregs) + >>> numpy.random.seed(100) + >>> t5 = RandomRegion(ids, num_regions=nregs) >>> t5.regions[0] [37, 62, 26, 41, 35, 25, 36] - Cardinality constrained + Cardinality constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t6 = spopt.region.RandomRegion(ids, cardinality=cards) + >>> numpy.random.seed(100) + >>> t6 = RandomRegion(ids, cardinality=cards) >>> t6.regions[0] [37, 62] - Contiguity constrained + Contiguity constrained: - >>> random.seed(100) - >>> np.random.seed(100) - >>> t7 = spopt.region.RandomRegion(ids, contiguity=w) + >>> numpy.random.seed(100) + >>> t7 = RandomRegion(ids, contiguity=w) >>> t7.regions[0] [37, 36, 38, 39] @@ -494,14 +459,15 @@ def build_contig_regions( if counter == len(candidates): # start swapping - # swapping simply changes the candidate list + # -- swapping simply changes the candidate list swap_in = None # area to become new candidate while swap_in is None: # PEP8 E711 swap_count += 1 swap_out = candidates.pop(0) # area to remove from candidates swap_neighs = copy.copy(w.neighbors[swap_out]) swap_neighs = list(np.random.permutation(swap_neighs)) - # select area to add to candidates (i.e. remove from an existing region) + # select area to add to candidates + # -- (i.e. remove from an existing region) for i in swap_neighs: if i not in candidates: join = i # area linking swap_in to swap_out @@ -509,7 +475,8 @@ def build_contig_regions( swap_region = regions[swap_index] swap_region = list(np.random.permutation(swap_region)) for j in swap_region: - # test to ensure region connectivity after removing area + # test to ensure region + # connectivity after removing area swap_region_test = swap_region[:] + [swap_out] if check_contiguity(w, swap_region_test, j): swap_in = j @@ -556,7 +523,8 @@ def build_contig_regions( regions.append(region) region_index = len(regions) - 1 for i in region: - area2region[i] = region_index # area2region needed for swapping + # area2region needed for swapping + area2region[i] = region_index # handling of regionalization result if len(regions) < num_regions: # regionalization failed From d6a1949ef81fdb87639fe9d18b0a1c078871b05b Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 15:23:03 -0500 Subject: [PATCH 3/8] ward docs, etc. --- spopt/region/base.py | 32 ++++++++++++++++---------------- spopt/region/ward.py | 19 +++++++++++-------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/spopt/region/base.py b/spopt/region/base.py index 8fc60c86..9f55a82c 100644 --- a/spopt/region/base.py +++ b/spopt/region/base.py @@ -1,4 +1,4 @@ -"""Base classes for spopt/region""" +"""Base classes and functions for spopt/region""" import libpysal import numpy @@ -205,7 +205,7 @@ def _closest(data, centroids): def _seeds(areas, k, seed): - """Randomly select `k` seeds from a sequence of areas. + """Randomly select ``k`` seeds from a sequence of areas. Parameters ---------- @@ -237,7 +237,7 @@ def is_neighbor(area, region, w): ---------- area : int, numpy.int64 - Area label + The area label. region : list Region members. w : libpysal.weights.W @@ -265,16 +265,16 @@ def infeasible_components(gdf, w, threshold_var, threshold): Parameters ---------- - gdf : geopandas.GeoDataFrame, required + gdf : geopandas.GeoDataFrame Geodataframe containing original data. - w : libpysal.weights.W, required + w : libpysal.weights.W Weights object created from given data. - attrs_name : list, required + attrs_name : list Strings for attribute names to measure similarity (cols of ``geopandas.GeoDataFrame``). - threshold_var : string, requied + threshold_var : str The name of the spatial extensive attribute variable. - threshold : {int, float}, required + threshold : {int, float} The threshold value. Returns @@ -334,8 +334,8 @@ def modify_components(gdf, w, threshold_var, threshold, policy="single"): The name of the spatial extensive attribute variable. threshold : {int, float} The threshold value. - policy: str - ``'single'`` (default) will attach an infeasible component to a feasible + policy: str (default 'single') + ``'single'`` will attach an infeasible component to a feasible component based on a single join using the minimum nearest neighbor distance between areas of infeasible components and areas in the largest component. ``'multiple'`` will form a join @@ -393,12 +393,12 @@ def form_single_component(gdf, w, linkage="single"): w : libysal.weights.W PySAL weights object. linkage : str - `single`: a small component will be joined with the largest - component by adding a single join based on minimum nearest - neighbor relation between areas in the small component and - the largest component. 'multiple': joins are added between each - area in the small component and their nearest neighbor in the - largest component. + `single`: a small component will be joined with the largest + component by adding a single join based on minimum nearest + neighbor relation between areas in the small component and + the largest component. 'multiple': joins are added between each + area in the small component and their nearest neighbor in the + largest component. Returns ------- diff --git a/spopt/region/ward.py b/spopt/region/ward.py index 8edb694e..b9ba5c6d 100644 --- a/spopt/region/ward.py +++ b/spopt/region/ward.py @@ -54,14 +54,17 @@ class WardSpatial(BaseSpOptHeuristicSolver): >>> model = WardSpatial(chicago, w, attrs_name, n_clusters) >>> model.solve() - Get the region IDs for unit areas. - - >>> model.labels_ - - Show the clustering results. - - >>> chicago['ward_new'] = model.labels_ - >>> chicago.plot(column='ward_new', categorical=True, figsize=(12,8), edgecolor='w') + Get the counts of region IDs for unit areas. + + >>> numpy.array(numpy.unique(model.labels_, return_counts=True)).T + array([[ 0, 62], + [ 1, 6], + [ 2, 3], + [ 3, 1], + [ 4, 2], + [ 5, 1], + [ 6, 1], + [ 7, 1]]) """ From 13e341c4282c6e0d58989cc5b1327dd251d3ff8f Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 18:44:37 -0500 Subject: [PATCH 4/8] region/util [1] docwork --- spopt/region/util.py | 49 ++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/spopt/region/util.py b/spopt/region/util.py index fc05f202..46b7e666 100755 --- a/spopt/region/util.py +++ b/spopt/region/util.py @@ -810,21 +810,21 @@ def assert_feasible(solution, adj, n_regions=None): Parameters ---------- - solution : :class:`numpy.ndarray` - Array of region labels. - adj : :class:`scipy.sparse.csr_matrix` + solution : numpy.ndarray + An array of region labels. + adj : scipy.sparse.csr_matrix Adjacency matrix representing the contiguity relation. - n_regions : `int` or `None` - An `int` represents the desired number of regions. - If `None`, then the number of regions is not checked. + n_regions : int (default None) + An ``int`` represents the desired number of regions. + If ``None``, then the number of regions is not checked. Raises ------ - exc : `ValueError` - A `ValueError` is raised if clustering is not spatially contiguous. - Given the `n_regions` argument is not `None`, a `ValueError` is raised - also if the number of regions is not equal to the `n_regions` argument. + exc : ValueError + A ``ValueError`` is raised if clustering is not spatially contiguous. + Given the ``n_regions`` argument is not ``None``, a ``ValueError`` is raised + also if the number of regions is not equal to the ``n_regions`` argument. """ if n_regions is not None: @@ -892,6 +892,8 @@ def separate_components(adj, labels): Examples -------- + >>> import networkx + >>> import numpy >>> edges_island1 = [(0, 1), (1, 2), # 0 | 1 | 2 ... (0, 3), (1, 4), (2, 5), # --------- ... (3, 4), (4,5)] # 3 | 4 | 5 @@ -900,21 +902,21 @@ def separate_components(adj, labels): ... (6, 8), (7, 9), # ----- ... (8, 9)] # 8 | 9 - >>> graph = nx.Graph(edges_island1 + edges_island2) - >>> adj = nx.to_scipy_sparse_matrix(graph) + >>> graph = networkx.Graph(edges_island1 + edges_island2) + >>> adj = networkx.to_scipy_sparse_matrix(graph) >>> # island 1: island divided into regions 0, 1, and 2 >>> sol_island1 = [area%3 for area in range(6)] >>> # island 2: all areas are in region 3 >>> sol_island2 = [3 for area in range(6, 10)] - >>> labels = np.array(sol_island1 + sol_island2) + >>> labels = numpy.array(sol_island1 + sol_island2) >>> yielded = list(separate_components(adj, labels)) >>> yielded.sort(key=lambda arr: arr[0], reverse=True) - >>> (yielded[0] == np.array([0, 1, 2, 0, 1, 2, -1, -1, -1, -1])).all() + >>> (yielded[0] == numpy.array([0, 1, 2, 0, 1, 2, -1, -1, -1, -1])).all() True - >>> (yielded[1] == np.array([-1, -1, -1, -1, -1, -1, 3, 3, 3, 3])).all() + >>> (yielded[1] == numpy.array([-1, -1, -1, -1, -1, -1, 3, 3, 3, 3])).all() True """ @@ -937,24 +939,27 @@ def pop_randomly_from(lst): def count(arr, el): - """ + """Return the count occurence of a specific value in an array. + Parameters ---------- - arr : :class:`numpy.ndarray` - - el : object + arr : numpy.ndarray + The array from which count occurence values. + el : int, float, str + The value to count. Returns ------- - result : :class:`numpy.ndarray` - The number of occurences of `el` in `arr`. + result : int + The number of occurences of ``el`` in ``arr``. Examples -------- - >>> arr = np.array([0, 0, 0, 1, 1]) + >>> import numpy + >>> arr = numpy.array([0, 0, 0, 1, 1]) >>> count(arr, 0) 3 From d73e20d69d545aad987629ff7376bf80c4a50462 Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 21:15:59 -0500 Subject: [PATCH 5/8] region/util [2] docwork --- spopt/region/util.py | 312 ++++++++++++++++++++++--------------------- 1 file changed, 159 insertions(+), 153 deletions(-) diff --git a/spopt/region/util.py b/spopt/region/util.py index 46b7e666..5e55acb1 100755 --- a/spopt/region/util.py +++ b/spopt/region/util.py @@ -4,15 +4,15 @@ import random import types -import scipy.sparse.csgraph as csg -from sklearn.metrics.pairwise import distance_metrics -from scipy.sparse import dok_matrix -import numpy as np import networkx as nx -from libpysal import weights +import numpy as np import pulp +import scipy.sparse.csgraph as csg +from libpysal import weights +from scipy.sparse import dok_matrix +from sklearn.metrics.pairwise import distance_metrics -from spopt.region.csgraph_utils import sub_adj_matrix, is_connected +from spopt.region.csgraph_utils import is_connected, sub_adj_matrix Move = collections.namedtuple("move", "area old_region new_region") "A named tuple representing a move from `old_region` to `new_region`." # sphinx @@ -20,30 +20,31 @@ def array_from_dict_values(dct, sorted_keys=None, flat_output=False, dtype=float): """ - Return values of the dictionary passed as `dct` argument as an numpy array. - The values in the returned array are sorted by the keys of `dct`. + Return values of a dictionary as an array. The values in the returned + array are sorted by the keys of the input dictionary. Parameters ---------- dct : dict - - sorted_keys : iterable, optional + The input dictionary. + sorted_keys : iterable (default None) If passed, then the elements of the returned array will be sorted by this argument. Thus, this argument can be passed to suppress the sorting, or for getting a subset of the dictionary's values or to get repeated values. - flat_output : bool, default: False - If True, the returned array will be one-dimensional. - If False, the returned array will be two-dimensional with one row per - key in `dct`. - dtype : default: np.float64 - The `dtype` of the returned array. + flat_output : bool (default False) + If ``True``, the returned array will be one-dimensional. + If ``False``, the returned array will be two-dimensional + with one row per key in ``dct``. + dtype : (default float) + The ``dtype`` of the returned array. Returns ------- - array : :class:`numpy.ndarray` + array : numpy.ndarray + The resultant array. Examples -------- @@ -51,8 +52,7 @@ def array_from_dict_values(dct, sorted_keys=None, flat_output=False, dtype=float >>> dict_flat = {0: 0, 1: 10} >>> dict_it = {0: [0], 1: [10]} >>> desired_flat = np.array([0, 10]) - >>> desired_2d = np.array([[0], - ... [10]]) + >>> desired_2d = np.array([[0],[10]]) >>> flat_flat = array_from_dict_values(dict_flat, flat_output=True) >>> (flat_flat == desired_flat).all() True @@ -96,14 +96,21 @@ def scipy_sparse_matrix_from_dict(neighbors): Returns ------- - adj : :class:`scipy.sparse.csr_matrix` + adj : scipy.sparse.csr_matrix Adjacency matrix representing the areas' contiguity relation. Examples -------- - >>> neighbors = {0: {1, 3}, 1: {0, 2, 4}, 2: {1, 5}, - ... 3: {0, 4}, 4: {1, 3, 5}, 5: {2, 4}} + >>> import numpy + >>> neighbors = { + ... 0: {1, 3}, + ... 1: {0, 2, 4}, + ... 2: {1, 5}, + ... 3: {0, 4}, + ... 4: {1, 3, 5}, + ... 5: {2, 4} + ... } >>> obtained = scipy_sparse_matrix_from_dict(neighbors) >>> desired = np.array([[0, 1, 0, 1, 0, 0], ... [1, 0, 1, 0, 1, 0], @@ -114,13 +121,15 @@ def scipy_sparse_matrix_from_dict(neighbors): >>> (obtained.todense() == desired).all() True - >>> neighbors = {"left": {"middle"}, - ... "middle": {"left", "right"}, - ... "right": {"middle"}} + >>> neighbors = { + ... 'left': {'middle'}, 'middle': {'left', 'right'}, 'right': {'middle'} + ... } >>> obtained = scipy_sparse_matrix_from_dict(neighbors) - >>> desired = np.array([[0, 1, 0], - ... [1, 0, 1], - ... [0, 1, 0]]) + >>> desired = np.array( + ... [[0, 1, 0], + ... [1, 0, 1], + ... [0, 1, 0]] + ... ) >>> (obtained.todense() == desired).all() True @@ -140,25 +149,26 @@ def scipy_sparse_matrix_from_w(w): Parameters ---------- - w : :class:`libpysal.weights.weights.W` - A W object representing the areas' contiguity relation. + w : libpysal.weights.weights.W + A *W* object representing the areas' contiguity relation. Returns ------- - adj : :class:`scipy.sparse.csr_matrix` + adj : scipy.sparse.csr_matrix Adjacency matrix representing the areas' contiguity relation. Examples -------- >>> from libpysal import weights + >>> import numpy >>> neighbor_dict = {0: {1}, 1: {0, 2}, 2: {1}} >>> w = weights.W(neighbor_dict) >>> obtained = scipy_sparse_matrix_from_w(w) - >>> desired = np.array([[0., 1., 0.], - ... [1., 0., 1.], - ... [0., 1., 0.]]) + >>> desired = numpy.array( + ... [[0., 1., 0.], [1., 0., 1.], [0., 1., 0.]] + ... ) >>> obtained.todense().all() == desired.all() True @@ -168,19 +178,20 @@ def scipy_sparse_matrix_from_w(w): def dict_from_graph_attr(graph, attr, array_values=False): """ + Parameters ---------- graph : networkx.Graph - + Graph to convert to dictionary. attr : str, iterable, or dict - If str, then it specifies the an attribute of the graph's nodes. - If iterable of strings, then multiple attributes of the graph's nodes - are specified. - If dict, then each key is a node and each value the corresponding - attribute value. (This format is also this function's return format.) - array_values : bool, default: False - If True, then each value is transformed into a :class:`numpy.ndarray`. + If ``str``, then it specifies the an attribute of the graph's nodes. + If ``iterable`` of strings, then multiple attributes of the graph's nodes + are specified. If ``dict``, then each key is a node and each value the + corresponding attribute value. + (This format is also this function's return format.) + array_values : bool (default False) + If ``True``, then each value is transformed into a ``numpy.ndarray``. Returns ------- @@ -196,18 +207,18 @@ def dict_from_graph_attr(graph, attr, array_values=False): Examples -------- - >>> import networkx as nx + >>> import networkx >>> edges = [(0, 1), (1, 2), # 0 | 1 | 2 ... (0, 3), (1, 4), (2, 5), # --------- - ... (3, 4), (4,5)] # 3 | 4 | 5 - >>> graph = nx.Graph(edges) + ... (3, 4), (4, 5)] # 3 | 4 | 5 + >>> graph = networkx.Graph(edges) >>> data_dict = {node: 10*node for node in graph} - >>> nx.set_node_attributes(graph, data_dict, "test_data") + >>> networkx.set_node_attributes(graph, data_dict, 'test_data') >>> desired = {key: [value] for key, value in data_dict.items()} - >>> dict_from_graph_attr(graph, "test_data") == desired + >>> dict_from_graph_attr(graph, 'test_data') == desired True - >>> dict_from_graph_attr(graph, ["test_data"]) == desired + >>> dict_from_graph_attr(graph, ['test_data']) == desired True """ @@ -232,42 +243,38 @@ def array_from_graph(graph, attr): ---------- graph : networkx.Graph - + Graph to convert to array. attr : str or iterable - If str, then it specifies the an attribute of the graph's nodes. - If iterable of strings, then multiple attributes of the graph's nodes + If ``str``, then it specifies the an attribute of the graph's nodes. + If ``iterable`` of strings, then multiple attributes of the graph's nodes are specified. Returns ------- - array : :class:`numpy.ndarray` - Array with one row for each node in `graph`. + array : numpy.ndarray + Array with one row for each node in ``graph``. Examples -------- - >>> import networkx as nx + >>> import networkx + >>> import numpy >>> edges = [(0, 1), (1, 2), # 0 | 1 | 2 ... (0, 3), (1, 4), (2, 5), # --------- - ... (3, 4), (4,5)] # 3 | 4 | 5 - >>> graph = nx.Graph(edges) + ... (3, 4), (4, 5)] # 3 | 4 | 5 + >>> graph = networkx.Graph(edges) >>> data_dict = {node: 10*node for node in graph} - >>> nx.set_node_attributes(graph, data_dict, "test_data") - >>> desired = np.array([[0], - ... [10], - ... [20], - ... [30], - ... [40], - ... [50]]) - >>> (array_from_graph(graph, "test_data") == desired).all() + >>> networkx.set_node_attributes(graph, data_dict, 'test_data') + >>> desired = np.array([[0], [10], [20], [30], [40], [50]]) + >>> (array_from_graph(graph, 'test_data') == desired).all() True - >>> (array_from_graph(graph, ["test_data"]) == desired).all() + >>> (array_from_graph(graph, ['test_data']) == desired).all() True - >>> (array_from_graph(graph, ["test_data", "test_data"]) == - ... np.hstack((desired, desired))).all() + >>> (array_from_graph(graph, ['test_data', 'test_data']) == + ... numpy.hstack((desired, desired))).all() True """ @@ -292,21 +299,21 @@ def array_from_region_list(region_list): Parameters ---------- - region_list : `list` + region_list : list Each list element is an iterable of a region's areas. Returns ------- - labels : :class:`numpy.ndarray` + labels : numpy.ndarray Each element specifies the region of the corresponding area. Examples -------- - >>> import numpy as np + >>> import numpy >>> obtained = array_from_region_list([{0, 1, 2, 5}, {3, 4}]) - >>> desired = np.array([ 0, 0, 0, 1, 1, 0]) + >>> desired = numpy.array([ 0, 0, 0, 1, 1, 0]) >>> (obtained == desired).all() True @@ -321,51 +328,47 @@ def array_from_region_list(region_list): def array_from_df_col(df, attr): """ - Extract one or more columns from a DataFrame as numpy array. + Extract one or more columns from a DataFrame as a ``numpy.array``. Parameters ---------- df : Union[DataFrame, GeoDataFrame] - + Dataframe to convert to dictionary. attr : Union[str, Sequence[str]] The columns' names to extract. Returns ------- - col : :class:`numpy.ndarray` + col : numpy.ndarray The specified column(s) of the array. Examples -------- - >>> import pandas as pd - >>> df = pd.DataFrame({"col1": [1, 2, 3], - ... "col2": [7, 8, 9]}) - >>> (array_from_df_col(df, "col1") == np.array([[1], - ... [2], - ... [3]])).all() + >>> import pandas + >>> import numpy + >>> df = pandas.DataFrame({'col1': [1, 2, 3], 'col2': [7, 8, 9]}) + >>> array = numpy.array([[1], [2], [3]]) + >>> (array_from_df_col(df, 'col1') == array).all() True - >>> (array_from_df_col(df, ["col1"]) == np.array([[1], - ... [2], - ... [3]])).all() + >>> (array_from_df_col(df, ['col1']) == array).all() True - >>> (array_from_df_col(df, ["col1", "col2"]) == np.array([[1, 7], - ... [2, 8], - ... [3, 9]])).all() + >>> array = numpy.array([[1, 7], [2, 8], [3, 9]]) + >>> (array_from_df_col(df, ['col1', 'col2']) == array).all() True """ value_error = ValueError( - "The attr argument has to be of one of the " - "following types: str or a sequence of strings." + "The `attr` argument has to be of one of the " + "following types: `str` or a sequence of strings." ) if isinstance(attr, str): attr = [attr] - elif isinstance(attr, collections.Sequence): + elif isinstance(attr, collections.abc.Sequence): if not all(isinstance(el, str) for el in attr): raise value_error else: @@ -375,21 +378,22 @@ def array_from_df_col(df, attr): def w_from_gdf(gdf, contiguity): """ - Get a `W` object from a GeoDataFrame. + Get a *W* object from a GeoDataFrame. Parameters ---------- gdf : GeoDataFrame - - contiguity : {"rook", "queen"} + GeoDataframe to convert to ``libpysal.weights.W``. + contiguity : str + Either ``'rook'`` or ``'queen'``. Returns ------- - cweights : `W` - The contiguity information contained in the `gdf` argument in the form - of a W object. + cweights : libpysal.weights.W + The contiguity information contained in the ``gdf`` + argument in the form of a *W* object. """ if not isinstance(contiguity, str) or contiguity.lower() not in ["rook", "queen"]: @@ -410,36 +414,35 @@ def dataframe_to_dict(df, cols): Parameters ---------- - df : Union[:class:`pandas.DataFrame`, :class:`geopandas.GeoDataFrame`] - - cols : Union[`str`, `list`] - If `str`, then it is the name of a column of `df`. - If `list`, then it is a list of strings. Each string is the name of a - column of `df`. + df : Union[pandas.DataFrame, geopandas.GeoDataFrame] + Dataframe to convert to dictionary. + cols : Union[str, list] + If ``str``, then it is the name of a column of ``df``. + If ``list``, then it is a list of strings. Each string is the name of a + column of ``df``. Returns ------- result : dict The keys are the elements of the DataFrame's index. - Each value is a :class:`numpy.ndarray` holding the corresponding values - in the columns specified by `cols`. + Each value is a ``numpy.ndarray`` holding the corresponding values + in the columns specified by ``cols``. Examples -------- - >>> import pandas as pd - >>> df = pd.DataFrame({"data": [100, 120, 115]}) + >>> import pandas + >>> df = pandas.DataFrame({'data': [100, 120, 115]}) >>> result = dataframe_to_dict(df, "data") >>> result == {0: 100, 1: 120, 2: 115} True - >>> import numpy as np - >>> df = pd.DataFrame({"data": [100, 120], - ... "other": [1, 2]}) - >>> actual = dataframe_to_dict(df, ["data", "other"]) - >>> desired = {0: np.array([100, 1]), 1: np.array([120, 2])} - >>> all(np.array_equal(actual[i], desired[i]) for i in desired) + >>> import numpy + >>> df = pandas.DataFrame({'data': [100, 120], 'other': [1, 2]}) + >>> actual = dataframe_to_dict(df, ['data', 'other']) + >>> desired = {0: numpy.array([100, 1]), 1: numpy.array([120, 2])} + >>> all(numpy.array_equal(actual[i], desired[i]) for i in desired) True """ @@ -452,26 +455,25 @@ def find_sublist_containing(el, lst, index=False): Parameters ---------- - el : + el : int The element to search for in the sublists of `lst`. lst : collections.Sequence A sequence of sequences or sets. - index : bool, default: False - If False (default), the subsequence or subset containing `el` is - returned. - If True, the index of the subsequence or subset in `lst` is returned. + index : bool (default False) + If ``False``, the subsequence or subset containing ``el`` is returned. + If ``True``, the index of the subsequence or subset in ``lst`` is returned. Returns ------- - result : collections.Sequence, collections.Set or int + result : collections.Sequence, collections.Set, or int See the `index` argument for more information. Raises ------ exc : LookupError - If `el` is not in any of the elements of `lst`. + If ``el`` is not in any of the elements of ``lst``. Examples -------- @@ -501,19 +503,19 @@ def get_metric_function(metric=None): Parameters ---------- - metric : str or function or None, default: None - Using None is equivalent to using "euclidean". + metric : str or function (default None) + Using None is equivalent to using ``'euclidean'``. If str, then this string specifies the distance metric (from scikit-learn) to use for calculating the objective function. Possible values are: - * "cityblock" for sklearn.metrics.pairwise.manhattan_distances - * "cosine" for sklearn.metrics.pairwise.cosine_distances - * "euclidean" for sklearn.metrics.pairwise.euclidean_distances - * "l1" for sklearn.metrics.pairwise.manhattan_distances - * "l2" for sklearn.metrics.pairwise.euclidean_distances - * "manhattan" for sklearn.metrics.pairwise.manhattan_distances + * ``'cityblock'`` for ``sklearn.metrics.pairwise.manhattan_distances`` + * ``'cosine'`` for ``sklearn.metrics.pairwise.cosine_distances`` + * ``'euclidean'`` for ``sklearn.metrics.pairwise.euclidean_distances`` + * ``'l1'`` for ``sklearn.metrics.pairwise.manhattan_distances`` + * ``'l2'`` for ``sklearn.metrics.pairwise.euclidean_distances`` + * ``'manhattan'`` for ``sklearn.metrics.pairwise.manhattan_distances`` If function, then this function should take two arguments and return a scalar value. Furthermore, the following conditions must be fulfilled: @@ -527,9 +529,9 @@ def get_metric_function(metric=None): ------- metric_func : function - If the `metric` argument is a function, it is returned. - If the `metric` argument is a string, then the corresponding distance - metric function from `sklearn.metrics.pairwise` is returned. + If the ``metric`` argument is a function, it is returned. + If the ``metric`` argument is a string, then the corresponding distance + metric function from ``sklearn.metrics.pairwise`` is returned. """ if metric is None: @@ -570,26 +572,26 @@ def raise_distance_metric_not_set(x, y): def make_move(moving_area, new_label, labels): """ - Modify the `labels` argument in place (no return value!) such that the - area `moving_area` has the new region label `new_label`. + Modify the ``labels`` argument in place (no return value!) such that the + area ``moving_area`` has the new region label ``new_label``. Parameters ---------- - moving_area : + moving_area : int The area to be moved (assigned to a new region). - new_label : `int` - The new region label of area `moving_area`. - labels : :class:`numpy.ndarray` + new_label : int + The new region label of area ``moving_area``. + labels : numpy.ndarray Each element is a region label of the area corresponding array index. Examples -------- - >>> import numpy as np - >>> labels = np.array([0, 0, 0, 0, 1, 1]) + >>> import numpy + >>> labels = numpy.array([0, 0, 0, 0, 1, 1]) >>> make_move(3, 1, labels) - >>> (labels == np.array([0, 0, 0, 1, 1, 1])).all() + >>> (labels == numpy.array([0, 0, 0, 1, 1, 1])).all() True """ @@ -615,6 +617,7 @@ def distribute_regions_among_components(component_labels, n_regions): `-------´ `---´ n_regions : int + The desired number of clusters. Must be > 0 and <= number of nodes. Returns ------- @@ -653,14 +656,15 @@ def generate_initial_sol(adj, n_regions): Parameters ---------- - adj : :class:`scipy.sparse.csr_matrix` - + adj : scipy.sparse.csr_matrix + Adjacency matrix. n_regions : int + The desired number of clusters. Must be > 0 and <= number of nodes. Yields ------ - region_labels : :class:`numpy.ndarray` + region_labels : numpy.ndarray An array with -1 for areas which are not part of the yielded component and an integer >= 0 specifying the region of areas within the yielded component. @@ -710,12 +714,12 @@ def generate_initial_sol(adj, n_regions): def _randomly_divide_connected_graph(adj, n_regions): """ - Divide the provided connected graph into `n_regions` regions. + Divide the provided connected graph into ``n_regions`` regions. Parameters ---------- - adj : :class:`scipy.sparse.csr_matrix` + adj : scipy.sparse.csr_matrix Adjacency matrix. n_regions : int The desired number of clusters. Must be > 0 and <= number of nodes. @@ -723,8 +727,8 @@ def _randomly_divide_connected_graph(adj, n_regions): Returns ------- - labels : :class:`numpy.ndarray` - Each element (an integer in {0, ..., `n_regions` - 1}) specifies the + labels : numpy.ndarray + Each element (an integer in ``{0, ..., ``n_regions - 1}``) specifies the region an area (defined by the index in the array) belongs to. Examples @@ -790,7 +794,7 @@ def copy_func(f): ------- g : function - Copy of `f`. + Copy of ``f``. """ g = types.FunctionType( @@ -877,26 +881,28 @@ def separate_components(adj, labels): Parameters ---------- - adj : :class:`scipy.sparse.csr_matrix` + adj : scipy.sparse.csr_matrix Adjacency matrix representing the contiguity relation. - labels : :class:`numpy.ndarray` + labels : numpy.ndarray + An array of area labels. Yields ------ - comp_dict : :class:`numpy.ndarray` - Each yielded dict represents one connected component of the graph - specified by the `adj` argument. In a yielded dict, each key is an area - and each value is the corresponding region-ID. + comp_dict : numpy.ndarray + Each yielded ``dict`` represents one connected component of the + graph specified by the ``adj`` argument. In a yielded ``dict``, + each key is an area and each value is the corresponding region ID. Examples -------- >>> import networkx >>> import numpy + >>> edges_island1 = [(0, 1), (1, 2), # 0 | 1 | 2 ... (0, 3), (1, 4), (2, 5), # --------- - ... (3, 4), (4,5)] # 3 | 4 | 5 + ... (3, 4), (4, 5)] # 3 | 4 | 5 >>> edges_island2 = [(6, 7), # 6 | 7 ... (6, 8), (7, 9), # ----- From e56f4d5c7f69e0d1883070f52345293e3c369f51 Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 21:41:12 -0500 Subject: [PATCH 6/8] f-strings region/util --- spopt/region/util.py | 66 +++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/spopt/region/util.py b/spopt/region/util.py index 5e55acb1..db4f50c7 100755 --- a/spopt/region/util.py +++ b/spopt/region/util.py @@ -289,8 +289,8 @@ def array_from_graph_or_dict(graph, attr): return array_from_dict_values(attr) else: raise ValueError( - "The `attr` argument must be a string, a list of " - "strings or a dictionary." + f"The `attr` argument was set to `{attr}`, but must be " + "a string, a list of strings or a dictionary." ) @@ -363,8 +363,8 @@ def array_from_df_col(df, attr): """ value_error = ValueError( - "The `attr` argument has to be of one of the " - "following types: `str` or a sequence of strings." + f"The `attr` argument was set to `{attr}`, but has to be " + "one of the following types: `str` or a sequence of strings." ) if isinstance(attr, str): attr = [attr] @@ -398,9 +398,8 @@ def w_from_gdf(gdf, contiguity): """ if not isinstance(contiguity, str) or contiguity.lower() not in ["rook", "queen"]: raise ValueError( - "The contiguity argument must be either None " - "or one of the following strings: " - '"rook" or"queen".' + f"The contiguity argument was set to `{contiguity}`, but it must " + "be either `None` or one of the following strings: 'rook' or 'queen'." ) if contiguity.lower() == "rook": cweights = weights.Rook.from_dataframe(gdf) @@ -495,7 +494,7 @@ def find_sublist_containing(el, lst, index=False): for idx, sublst in enumerate(lst): if el in sublst: return idx if index else sublst - raise LookupError("{} not found in any of the sublists of {}".format(el, lst)) + raise LookupError(f"{el} not found in any of the sublists of {lst}.") def get_metric_function(metric=None): @@ -541,24 +540,19 @@ def get_metric_function(metric=None): try: return distance_metrics()[metric] except KeyError: + accetpable_names = tuple( + name for name in distance_metrics().keys() if name != "precomputed" + ) raise ValueError( - "{} is not a known metric. Please use rather one of the " - "following metrics: {}".format( - metric, - tuple( - name - for name in distance_metrics().keys() - if name != "precomputed" - ), - ) + f"'{metric}' is not a known metric. Please use one " + f"of the following metrics: {accetpable_names}." ) elif callable(metric): return metric else: raise ValueError( - "A {} was passed as `metric` argument. " - "Please pass a string or a function " - "instead.".format(type(metric)) + f"A {type(metric)} was passed as `metric` argument. " + "Please pass a string or a function instead." ) @@ -676,9 +670,8 @@ def generate_initial_sol(adj, n_regions): raise ValueError("There must be at least one area.") if n_areas < n_regions: raise ValueError( - "The number of regions ({}) must be " - "less than or equal to the number of areas " - "({}).".format(n_regions, n_areas) + f"The number of regions ({n_regions}) must be less than " + f"or equal to the number of areas ({n_areas})." ) if n_regions == 1: yield {area: 0 for area in range(n_areas)} @@ -687,9 +680,8 @@ def generate_initial_sol(adj, n_regions): n_comps, comp_labels = csg.connected_components(adj) if n_comps > n_regions: raise ValueError( - "The number of regions ({}) must not be " - "less than the number of connected components " - "({}).".format(n_regions, n_comps) + f"The number of regions ({n_regions}) must not be less than " + f"the number of connected components ({n_comps})." ) n_regions_per_comp = distribute_regions_among_components(comp_labels, n_regions) @@ -728,7 +720,7 @@ def _randomly_divide_connected_graph(adj, n_regions): ------- labels : numpy.ndarray - Each element (an integer in ``{0, ..., ``n_regions - 1}``) specifies the + Each element (an integer in ``{0, ..., n_regions - 1}``) specifies the region an area (defined by the index in the array) belongs to. Examples @@ -747,15 +739,13 @@ def _randomly_divide_connected_graph(adj, n_regions): """ if not n_regions > 0: - msg = "n_regions is {} but must be positive.".format(n_regions) - raise ValueError(msg) + raise ValueError(f"`n_regions` is {n_regions} but must be positive.") n_areas = adj.shape[0] if not n_regions <= n_areas: - msg = ( - "n_regions is {} but must less than or equal to " - + "the number of nodes which is {}".format(n_regions, n_areas) + raise ValueError( + f"`n_regions` is {n_regions} but must less than or " + f"equal to the number of nodes which is {n_areas}." ) - raise ValueError(msg) mst = csg.minimum_spanning_tree(adj) for _ in range(n_regions - 1): # try different links to cut and pick the one leading to the most @@ -834,8 +824,7 @@ def assert_feasible(solution, adj, n_regions=None): if n_regions is not None: if len(set(solution)) != n_regions: raise ValueError( - "The number of regions is {} but " - "should be {}".format(len(solution), n_regions) + f"The number of regions is {len(solution)} but should be {n_regions}." ) for region_label in set(solution): @@ -843,9 +832,7 @@ def assert_feasible(solution, adj, n_regions=None): # check right contiguity if not is_connected(aux): - raise ValueError( - "Region {} is not spatially " "contiguous.".format(region_label) - ) + raise ValueError(f"Region {region_label} is not spatially contiguous.") def boolean_assert_feasible(solution, adj, n_regions=None): @@ -855,8 +842,7 @@ def boolean_assert_feasible(solution, adj, n_regions=None): if n_regions is not None: if len(set(solution)) != n_regions: raise ValueError( - "The number of regions is {} but " - "should be {}".format(len(solution), n_regions) + f"The number of regions is {len(solution)} but should be {n_regions}." ) for region_label in set(solution): From 7139df8da866835f7f11d1f613da06908dc7d8f1 Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 21:50:04 -0500 Subject: [PATCH 7/8] f-strings region/random_region --- spopt/region/random_region.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/spopt/region/random_region.py b/spopt/region/random_region.py index b00132f2..1eb8de66 100644 --- a/spopt/region/random_region.py +++ b/spopt/region/random_region.py @@ -292,20 +292,24 @@ def __init__( if cardinality: if self.n != sum(cardinality): self.feasible = False - msg = f"Number of areas ({self.n}) does not match " - msg += f"'cardinality' ({sum(cardinality)})." - raise ValueError(msg) + raise ValueError( + f"Number of areas ({self.n}) does not match " + f"`cardinality` ({sum(cardinality)})." + ) if contiguity: if area_ids != contiguity.id_order: self.feasible = False - msg = "Order of 'area_ids' must match order in 'contiguity'." - raise ValueError(msg) + raise ValueError( + "Order of `area_ids` must match order in `contiguity`. Inspect " + "the `area_ids` and `contiguity.id_order` input parameters." + ) if num_regions and cardinality: if num_regions != len(cardinality): self.feasible = False - msg = f"Number of regions ({num_regions}) does not match " - msg += f"'cardinality' ({sum(cardinality)})." - raise ValueError(msg) + raise ValueError( + f"Number of regions ({num_regions}) does not match " + f"`cardinality` ({len(cardinality)})." + ) # dispatches the appropriate algorithm if num_regions and cardinality and contiguity: From 480d12c3daf2ddd8885c75556cb1a86828990e5b Mon Sep 17 00:00:00 2001 From: James Gaboardi Date: Mon, 21 Nov 2022 21:57:46 -0500 Subject: [PATCH 8/8] add actual test change --- spopt/tests/test_random_regions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spopt/tests/test_random_regions.py b/spopt/tests/test_random_regions.py index c8bfdb13..85763a22 100644 --- a/spopt/tests/test_random_regions.py +++ b/spopt/tests/test_random_regions.py @@ -168,7 +168,7 @@ def test_random_regions_error_card(self): RandomRegion([0, 1], cardinality=[4]) def test_random_regions_error_contig(self): - with pytest.raises(ValueError, match="Order of 'area_ids'"): + with pytest.raises(ValueError, match="Order of `area_ids`"): class _shell_w_: def __init__(self):