def fit_from_w(self, w, attr, n_regions, initial_labels=None, objective_func=ObjectiveFunctionPairwise()): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix`. Parameters ---------- w : :class:`libpysal.weights.weights.W` W object representing the contiguity relation. attr : :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. initial_labels : :class:`numpy.ndarray` or None, default: None Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ adj = scipy_sparse_matrix_from_w(w) self.fit_from_scipy_sparse_matrix( adj, attr, n_regions, initial_labels, objective_func=objective_func)
def fit_from_networkx(self, graph, attr, spatially_extensive_attr, threshold, max_it=10, objective_func=ObjectiveFunctionPairwise()): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix:. Parameters ---------- graph : `networkx.Graph` attr : str, list or dict If the clustering criteria are present in the networkx.Graph `graph` as node attributes, then they can be specified as a string (for one criterion) or as a list of strings (for multiple criteria). Alternatively, a dict can be used with each key being a node of the networkx.Graph `graph` and each value being the corresponding clustering criterion (a scalar (e.g. `float` or `int`) or a :class:`numpy.ndarray`). If there are no clustering criteria present in the networkx.Graph `graph` as node attributes, then a dictionary must be used for this argument. Refer to the corresponding argument in :meth:`fit_from_dict` for more details about the expected dict. spatially_extensive_attr : str, list or dict If the spatially extensive attribute is present in the networkx.Graph `graph` as node attributes, then they can be specified as a string (for one attribute) or as a list of strings (for multiple attributes). Alternatively, a dict can be used with each key being a node of the networkx.Graph `graph` and each value being the corresponding spatially extensive attribute (a scalar (e.g. `float` or `int`) or a :class:`numpy.ndarray`). If there are no spatially extensive attributes present in the networkx.Graph `graph` as node attributes, then a dictionary must be used for this argument. Refer to the corresponding argument in :meth:`fit_from_dict` for more details about the expected dict. threshold : numbers.Real or :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. max_it : int, default: 10 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ adj = nx.to_scipy_sparse_matrix(graph) attr = array_from_graph_or_dict(graph, attr) sp_ext_attr = array_from_graph_or_dict(graph, spatially_extensive_attr) self.fit_from_scipy_sparse_matrix(adj, attr, sp_ext_attr, threshold=threshold, max_it=max_it, objective_func=objective_func)
def fit_from_scipy_sparse_matrix( self, adj, attr, n_regions, initial_labels=None, objective_func=ObjectiveFunctionPairwise()): """ Perform the AZP algorithm as described in [OR1995]_. The resulting region labels are assigned to the instance's :attr:`labels_` attribute. Parameters ---------- adj : :class:`scipy.sparse.csr_matrix` Adjacency matrix representing the contiguity relation. attr : :class:`numpy.ndarray` Array (number of areas x number of attributes) of areas' attributes relevant to clustering. n_regions : `int` Number of desired regions. initial_labels : :class:`numpy.ndarray` or None, default: None One-dimensional array of labels at the beginning of the algorithm. If None, then a random initial clustering will be generated. objective_func : :class:`region.objective_function.ObjectiveFunction`, default: ObjectiveFunctionPairwise() The objective function to use. """ if attr.ndim == 1: attr = attr.reshape(adj.shape[0], -1) self.allow_move_strategy.attr_all = attr self.objective_func = objective_func # step 1 if initial_labels is not None: assert_feasible(initial_labels, adj, n_regions) initial_labels_gen = separate_components(adj, initial_labels) else: initial_labels_gen = generate_initial_sol(adj, n_regions) labels = -np.ones(adj.shape[0]) for labels_comp in initial_labels_gen: comp_idx = np.where(labels_comp != -1)[0] adj_comp = sub_adj_matrix(adj, comp_idx) labels_comp = labels_comp[comp_idx] attr_comp = attr[comp_idx] self.allow_move_strategy.start_new_component( labels_comp, attr_comp, self.objective_func, comp_idx) labels_comp = self._azp_connected_component( adj_comp, labels_comp, attr_comp) labels[comp_idx] = labels_comp self.n_regions = n_regions self.labels_ = labels
def fit_from_networkx(self, graph, attr, n_regions, initial_labels=None, objective_func=ObjectiveFunctionPairwise()): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix`. Parameters ---------- graph : `networkx.Graph` Graph representing the contiguity relation. attr : str, list or dict If the clustering criteria are present in the networkx.Graph `graph` as node attributes, then they can be specified as a string (for one criterion) or as a list of strings (for multiple criteria). Alternatively, a dict can be used with each key being a node of the networkx.Graph `graph` and each value being the corresponding clustering criterion (a scalar (e.g. `float` or `int`) or a :class:`numpy.ndarray`). If there are no clustering criteria present in the networkx.Graph `graph` as node attributes, then a dictionary must be used for this argument. Refer to the corresponding argument in :meth:`fit_from_dict` for more details about the expected dict. n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. initial_labels : str or dict or None, default: None If str, then the string names the graph's attribute holding the information about the initial clustering. If dict, then each key is a node and each value is the region the key area is assigned to at the beginning of the algorithm. If None, then a random initial clustering will be generated. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ adj = nx.to_scipy_sparse_matrix(graph) attr = array_from_graph_or_dict(graph, attr) if initial_labels is not None: initial_labels = array_from_graph_or_dict(graph, initial_labels) self.fit_from_scipy_sparse_matrix( adj, attr, n_regions, initial_labels, objective_func=objective_func)
def fit_from_geodataframe(self, gdf, attr, spatially_extensive_attr, threshold, max_it=10, objective_func=ObjectiveFunctionPairwise(), contiguity="rook"): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix:. Parameters ---------- gdf : :class:`geopandas.GeoDataFrame` attr : str or list The clustering criteria (columns of the GeoDataFrame `gdf`) are specified as string (for one column) or list of strings (for multiple columns). spatially_extensive_attr : str or list The name (`str`) or names (`list` of strings) of column(s) in `gdf` containing the spatially extensive attributes. threshold : numbers.Real or :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. max_it : int, default: 10 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. contiguity : {"rook", "queen"}, default: "rook" Defines the contiguity relationship between areas. Possible contiguity definitions are: * "rook" - Rook contiguity. * "queen" - Queen contiguity. """ w = w_from_gdf(gdf, contiguity) attr = array_from_df_col(gdf, attr) spat_ext_attr = array_from_df_col(gdf, spatially_extensive_attr) self.fit_from_w(w, attr, spat_ext_attr, threshold=threshold, max_it=max_it, objective_func=objective_func)
def fit_from_dict(self, neighbor_dict, attr, n_regions, initial_labels=None, objective_func=ObjectiveFunctionPairwise()): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix`. Parameters ---------- neighbor_dict : `dict` Each key is an area and each value is an iterable of the key area's neighbors. attr : `dict` Each key is an area and each value is the corresponding clustering-relevant attribute. n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. initial_labels : `dict` or None, default: None Each key represents an area. Each value represents the region, the corresponding area is assigned to at the beginning of the algorithm. If None, then a random initial clustering will be generated. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ sorted_areas = sorted(neighbor_dict) adj = scipy_sparse_matrix_from_dict(neighbor_dict) attr_arr = array_from_dict_values(attr, sorted_areas) if initial_labels is not None: initial_labels = array_from_dict_values( initial_labels, sorted_areas, flat_output=True, dtype=np.int32) self.fit_from_scipy_sparse_matrix( adj, attr_arr, n_regions, initial_labels, objective_func=objective_func)
def fit_from_geodataframe(self, gdf, attr, n_regions, contiguity="rook", initial_labels=None, cooling_factor=0.85, objective_func=ObjectiveFunctionPairwise()): """ Parameters ---------- gdf : :class:`geopandas.GeoDataFrame` Refer to the corresponding argument in :meth:`AZP.fit_from_geodataframe`. attr : `str` or `list` Refer to the corresponding argument in :meth:`AZP.fit_from_geodataframe`. n_regions : `int` Refer to the corresponding argument in :meth:`AZP.fit_from_geodataframe`. contiguity : `str` Refer to the corresponding argument in :meth:`AZP.fit_from_geodataframe`. initial_labels : :class:`numpy.ndarray` or None, default: None Refer to the corresponding argument in :meth:`AZP.fit_from_geodataframe`. cooling_factor : float, default: 0.85 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ w = w_from_gdf(gdf, contiguity) attr = array_from_df_col(gdf, attr) self.fit_from_w( w, attr, n_regions, initial_labels, cooling_factor=cooling_factor, objective_func=objective_func)
def fit_from_networkx(self, graph, attr, n_regions, initial_labels=None, cooling_factor=0.85, objective_func=ObjectiveFunctionPairwise()): """ Parameters ---------- graph : `networkx.Graph` Refer to the corresponding argument in :meth:`AZP.fit_from_networkx`. attr : str, list or dict Refer to the corresponding argument in :meth:`AZP.fit_from_networkx`. n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. initial_labels : str or dict or None, default: None Refer to the corresponding argument in :meth:`AZP.fit_from_networkx`. cooling_factor : float, default: 0.85 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ adj = nx.to_scipy_sparse_matrix(graph) attr = array_from_graph_or_dict(graph, attr) if initial_labels is not None: initial_labels = array_from_graph_or_dict(graph, initial_labels) self.fit_from_scipy_sparse_matrix( adj, attr, n_regions, initial_labels, cooling_factor=cooling_factor, objective_func=objective_func)
def fit_from_dict(self, neighbor_dict, attr, n_regions, initial_labels=None, cooling_factor=0.85, objective_func=ObjectiveFunctionPairwise()): """ Parameters ---------- neighbor_dict : `dict` Refer to the corresponding argument in :meth:`AZP.fit_from_dict`. attr : `dict` Refer to the corresponding argument in :meth:`AZP.fit_from_dict`. n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. initial_labels : `dict` or None, default: None Refer to the corresponding argument in :meth:`AZP.fit_from_dict`. cooling_factor : float, default: 0.85 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ sorted_areas = sorted(neighbor_dict) adj = scipy_sparse_matrix_from_dict(neighbor_dict) attr_arr = array_from_dict_values(attr, sorted_areas) if initial_labels is not None: initial_labels = array_from_dict_values( initial_labels, sorted_areas, flat_output=True, dtype=np.int32) self.fit_from_scipy_sparse_matrix( adj, attr_arr, n_regions, initial_labels=initial_labels, cooling_factor=cooling_factor, objective_func=objective_func)
def fit_from_w(self, w, attr, spatially_extensive_attr, threshold, max_it=10, objective_func=ObjectiveFunctionPairwise()): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix:. Parameters ---------- w : :class:`libpysal.weights.weights.W` W object representing the contiguity relation. attr : :class:`numpy.ndarray` Each element specifies an area's attribute which is used for calculating the objective function. spatially_extensive_attr : :class:`numpy.ndarray` Each element specifies an area's spatially extensive attribute which is used to ensure that the sum of spatially extensive attributes in each region adds up to a threshold defined by the `threshold` argument. threshold : numbers.Real or :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. max_it : int, default: 10 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ adj = scipy_sparse_matrix_from_w(w) self.fit_from_scipy_sparse_matrix(adj, attr, spatially_extensive_attr, threshold, max_it=max_it, objective_func=objective_func)
def fit_from_geodataframe(self, gdf, attr, n_regions, contiguity="rook", initial_labels=None, objective_func=ObjectiveFunctionPairwise()): """ Alternative API for :meth:`fit_from_scipy_sparse_matrix`. Parameters ---------- gdf : :class:`geopandas.GeoDataFrame` attr : `str` or `list` The clustering-relevant attributes (columns of the GeoDataFrame `gdf`) are specified as string (for one column) or list of strings (for multiple columns). n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. contiguity : {"rook", "queen"}, default: "rook" Defines the contiguity relationship between areas. Possible contiguity definitions are: * "rook" - Rook contiguity. * "queen" - Queen contiguity. initial_labels : :class:`numpy.ndarray` or None, default: None Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ w = w_from_gdf(gdf, contiguity) attr = array_from_df_col(gdf, attr) self.fit_from_w( w, attr, n_regions, initial_labels, objective_func=objective_func)
def fit_from_w(self, w, attr, n_regions, initial_labels=None, cooling_factor=0.85, objective_func=ObjectiveFunctionPairwise()): """ Parameters ---------- w : :class:`libpysal.weights.weights.W` Refer to the corresponding argument in :meth:`AZP.fit_from_w`. attr : :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. n_regions : `int` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. initial_labels : :class:`numpy.ndarray` or None, default: None Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. cooling_factor : float, default: 0.85 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ adj = scipy_sparse_matrix_from_w(w) self.fit_from_scipy_sparse_matrix( adj, attr, n_regions, initial_labels, cooling_factor=cooling_factor, objective_func=objective_func)
def fit_from_scipy_sparse_matrix( self, adj, attr, spatially_extensive_attr, threshold, max_it=10, objective_func=ObjectiveFunctionPairwise()): """ Solve the max-p-regions problem in a heuristic way (see [DAR2012]_). The resulting region labels are assigned to the instance's :attr:`labels_` attribute. Parameters ---------- adj : :class:`scipy.sparse.csr_matrix` Adjacency matrix representing the areas' contiguity relation. attr : :class:`numpy.ndarray` Array (number of areas x number of attributes) of areas' attributes relevant to clustering. spatially_extensive_attr : :class:`numpy.ndarray` Array (number of areas x number of attributes) of areas' attributes relevant to ensuring the threshold condition. threshold : numbers.Real or :class:`numpy.ndarray` The lower bound for a region's sum of spatially extensive attributes. The argument's type is numbers.Real if there is only one spatially extensive attribute per area, otherwise it is a one-dimensional array with as many entries as there are spatially extensive attributes per area. max_it : int, default: 10 The maximum number of partitions produced in the algorithm's construction phase. objective_func : :class:`region.objective_function.ObjectiveFunction`, default: ObjectiveFunctionPairwise() The objective function to use. """ print("f_f_SCIPY got:\n", attr, "\n", spatially_extensive_attr, "\n", threshold, sep="") weights = ps_api.WSP(adj).to_W() areas_dict = weights.neighbors self.metric = objective_func.metric best_partition = None best_obj_value = float("inf") feasible_partitions = [] partitions_before_enclaves_assignment = [] max_p = 0 # maximum number of regions # construction phase # print("constructing") for _ in range(max_it): # print(" ", _) partition, enclaves = self.grow_regions(adj, attr, spatially_extensive_attr, threshold) n_regions = len(partition) if n_regions > max_p: partitions_before_enclaves_assignment = [(partition, enclaves)] max_p = n_regions elif n_regions == max_p: partitions_before_enclaves_assignment.append( (partition, enclaves)) # print("\n" + "assigning enclaves") for partition, enclaves in partitions_before_enclaves_assignment: # print(" cleaning up in partition", partition) feasible_partitions.append( self.assign_enclaves(partition, enclaves, areas_dict, attr)) for partition in feasible_partitions: print(partition, "\n") # local search phase if self.local_search is None: self.local_search = AZP() self.local_search.allow_move_strategy = AllowMoveAZPMaxPRegions( spatially_extensive_attr, threshold, self.local_search.allow_move_strategy) for partition in feasible_partitions: self.local_search.fit_from_scipy_sparse_matrix( adj, attr, max_p, initial_labels=array_from_region_list(partition), objective_func=objective_func) partition = self.local_search.labels_ # print("optimized partition", partition) obj_value = objective_func(partition, attr) if obj_value < best_obj_value: best_obj_value = obj_value best_partition = partition self.labels_ = best_partition
def fit_from_dict(self, neighbors_dict, attr, spatially_extensive_attr, threshold, max_it=10, objective_func=ObjectiveFunctionPairwise()): """ Solve the max-p-regions problem in a heuristic way (see [DAR2012]_). The resulting region labels are assigned to the instance's :attr:`labels_` attribute. Parameters ---------- neighbors_dict : dict Each key represents an area and each value is an iterable of neighbors of this area. attr : dict A dict with the same keys as `neighbors_dict` and values representing the attributes for calculating h**o-/heterogeneity. A value can be scalar (e.g. `float` or `int`) or a :class:`numpy.ndarray`. spatially_extensive_attr : dict A dict with the same keys as `neighbors_dict` and values representing the spatially extensive attribute (scalar or iterable of scalars). In the max-p-regions problem each region's sum of spatially extensive attributes must be greater than a specified threshold. In case of iterables of scalars as dict-values all elements of the iterable have to fulfill the condition. threshold : numbers.Real or :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. max_it : int, default: 10 Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ if not isinstance(neighbors_dict, dict): raise ValueError("The neighbors_dict argument must be dict.") not_same_dict_keys_msg = "The {} argument has to be of type dict " \ "with the same keys as neighbors_dict." if not isinstance(attr, dict) or attr.keys() != neighbors_dict.keys(): raise ValueError(not_same_dict_keys_msg.format("attr")) if not isinstance(spatially_extensive_attr, dict) or \ spatially_extensive_attr.keys() != neighbors_dict.keys(): raise ValueError( not_same_dict_keys_msg.format(spatially_extensive_attr)) adj = scipy_sparse_matrix_from_dict(neighbors_dict) attr_arr = array_from_dict_values(attr) spat_ext_attr_arr = array_from_dict_values(spatially_extensive_attr) self.fit_from_scipy_sparse_matrix(adj, attr_arr, spat_ext_attr_arr, threshold=threshold, max_it=max_it, objective_func=objective_func)
def fit_from_scipy_sparse_matrix( self, adj, attr, n_regions, initial_labels=None, cooling_factor=0.85, objective_func=ObjectiveFunctionPairwise()): """ Parameters ---------- adj : :class:`scipy.sparse.csr_matrix` Refer to the corresponding argument in :meth:`AZP.fit_from_scipy_sparse_matrix`. attr : :class:`numpy.ndarray` Refer to the corresponding argument in :meth:`AZP.fit_from_scipy_sparse_matrix`. n_regions : `int` Refer to the corresponding argument in :meth:`AZP.fit_from_scipy_sparse_matrix`. initial_labels : :class:`numpy.ndarray` or None, default: None Refer to the corresponding argument in :meth:`AZP.fit_from_scipy_sparse_matrix`. cooling_factor : float, default: 0.85 Float :math:`\\in (0, 1)` specifying the cooling factor for the simulated annealing. objective_func : :class:`region.ObjectiveFunction`, default: ObjectiveFunctionPairwise() Refer to the corresponding argument in :meth:`fit_from_scipy_sparse_matrix`. """ if not (0 < cooling_factor < 1): raise ValueError("The cooling_factor argument must be greater " "than 0 and less than 1") if attr.ndim == 1: attr = attr.reshape(adj.shape[0], -1) self.allow_move_strategy = AllowMoveAZPSimulatedAnnealing( init_temperature=self.init_temperature, sa_moves_term=self.sa_moves_term) self.allow_move_strategy.register_sa_moves_term(self.sa_moves_alert) self.allow_move_strategy.register_move_made(self.move_made_alert) self.azp = AZP( allow_move_strategy=self.allow_move_strategy, random_state=self.random_state) # step a t = self.init_temperature nonmoving_steps = 0 # step d: repeat step b and c while nonmoving_steps < self.nonmoving_steps_before_stop: it = 0 self.sa_moves_term_reached = False self.allow_move_strategy.reset() # step b while it < self.maxit and not self.sa_moves_term_reached: it += 1 old_sol = initial_labels self.azp.fit_from_scipy_sparse_matrix( adj, attr, n_regions, initial_labels, objective_func) initial_labels = self.azp.labels_ if old_sol is not None: if (old_sol == initial_labels).all(): break # added termination condition (not in Openshaw & Rao (1995)) if self.visited.count(tuple(initial_labels)) \ >= self.reps_before_termination: break self.visited.append(tuple(initial_labels)) # step c t *= cooling_factor self.allow_move_strategy.update_temperature(t) if self.move_made: self.move_made = False else: nonmoving_steps += 1 self.labels_ = initial_labels