diff --git a/libpysal/weights/contiguity.py b/libpysal/weights/contiguity.py index 9c58db701..2e009f69c 100644 --- a/libpysal/weights/contiguity.py +++ b/libpysal/weights/contiguity.py @@ -1,4 +1,5 @@ import itertools +import warnings import numpy @@ -133,10 +134,17 @@ def from_iterable(cls, iterable, sparse=False, **kwargs): @classmethod def from_dataframe( - cls, df, geom_col=None, idVariable=None, ids=None, id_order=None, **kwargs + cls, + df, + geom_col=None, + idVariable=None, + ids=None, + id_order=None, + use_index=None, + **kwargs, ): """ - Construct a weights object from a pandas dataframe with a geometry + Construct a weights object from a (geo)pandas dataframe with a geometry column. This will cast the polygons to PySAL polygons, then build the W using ids from the dataframe. @@ -149,16 +157,24 @@ def from_dataframe( the name of the column in `df` that contains the geometries. Defaults to active geometry column. idVariable : string + DEPRECATED - use `ids` instead. the name of the column to use as IDs. If nothing is provided, the dataframe index is used - ids : list - a list of ids to use to index the spatial weights object. - Order is not respected from this list. + ids : list-like, string + a list-like of ids to use to index the spatial weights object or + the name of the column to use as IDs. If nothing is + provided, the dataframe index is used if `use_index=True` or + a positional index is used if `use_index=False`. + Order of the resulting W is not respected from this list. id_order : list - an ordered list of ids to use to index the spatial weights + DEPRECATED - argument is deprecated and will be removed. + An ordered list of ids to use to index the spatial weights object. If used, the resulting weights object will iterate over results in the order of the names provided in this - argument. + argument. + use_index : bool + use index of `df` as `ids` to index the spatial weights object. + Defaults to False but in future will default to True. See Also -------- @@ -167,17 +183,62 @@ def from_dataframe( """ if geom_col is None: geom_col = df.geometry.name + if id_order is not None: + warnings.warn( + "`id_order` is deprecated and will be removed in future.", + FutureWarning, + stacklevel=2, + ) if id_order is True and ((idVariable is not None) or (ids is not None)): # if idVariable is None, we want ids. Otherwise, we want the # idVariable column id_order = list(df.get(idVariable, ids)) else: id_order = df.get(id_order, ids) - elif idVariable is not None: - ids = df.get(idVariable).tolist() - elif isinstance(ids, str): - ids = df.get(ids).tolist() + + if idVariable is not None: + if ids is None: + warnings.warn( + "`idVariable` is deprecated and will be removed in future. " + "Use `ids` instead.", + FutureWarning, + stacklevel=2, + ) + ids = idVariable + else: + warnings.warn( + "Both `idVariable` and `ids` passed, using `ids`.", + UserWarning, + stacklevel=2, + ) + + if ids is None: + if use_index is None: + warnings.warn( + "`use_index` defaults to False but will default to True in future. " + "Set True/False directly to control this behavior and silence this " + "warning", + FutureWarning, + stacklevel=2, + ) + use_index = False + if use_index: + ids = df.index.tolist() + + else: + if isinstance(ids, str): + ids = df[ids] + + if not isinstance(ids, list): + ids = ids.tolist() + + if len(ids) != len(df): + raise ValueError("The length of `ids` does not match the length of df.") + + if id_order is None: + id_order = ids + return cls.from_iterable( df[geom_col].tolist(), ids=ids, id_order=id_order, **kwargs ) @@ -227,7 +288,7 @@ def from_xarray( Returns ------- w : libpysal.weights.W/libpysal.weights.WSP - instance of spatial weights class W or WSP with an index attribute + instance of spatial weights class W or WSP with an index attribute Notes ----- @@ -358,9 +419,18 @@ def from_iterable(cls, iterable, sparse=False, **kwargs): return w @classmethod - def from_dataframe(cls, df, geom_col=None, **kwargs): + def from_dataframe( + cls, + df, + geom_col=None, + idVariable=None, + ids=None, + id_order=None, + use_index=None, + **kwargs, + ): """ - Construct a weights object from a pandas dataframe with a geometry + Construct a weights object from a (geo)pandas dataframe with a geometry column. This will cast the polygons to PySAL polygons, then build the W using ids from the dataframe. @@ -371,46 +441,93 @@ def from_dataframe(cls, df, geom_col=None, **kwargs): for spatial weights geom_col : string the name of the column in `df` that contains the - geometries. Defaults to active geometry column + geometries. Defaults to active geometry column. idVariable : string + DEPRECATED - use `ids` instead. the name of the column to use as IDs. If nothing is provided, the dataframe index is used - ids : list - a list of ids to use to index the spatial weights object. - Order is not respected from this list. + ids : list-like, string + a list-like of ids to use to index the spatial weights object or + the name of the column to use as IDs. If nothing is + provided, the dataframe index is used if `use_index=True` or + a positional index is used if `use_index=False`. + Order of the resulting W is not respected from this list. id_order : list - an ordered list of ids to use to index the spatial weights + DEPRECATED - argument is deprecated and will be removed. + An ordered list of ids to use to index the spatial weights object. If used, the resulting weights object will iterate over results in the order of the names provided in this - argument. + argument. + use_index : bool + use index of `df` as `ids` to index the spatial weights object. + Defaults to False but in future will default to True. See Also -------- :class:`libpysal.weights.weights.W` :class:`libpysal.weights.contiguity.Queen` """ - idVariable = kwargs.pop("idVariable", None) - ids = kwargs.pop("ids", None) - id_order = kwargs.pop("id_order", None) if geom_col is None: geom_col = df.geometry.name + if id_order is not None: + warnings.warn( + "`id_order` is deprecated and will be removed in future.", + FutureWarning, + stacklevel=2, + ) if id_order is True and ((idVariable is not None) or (ids is not None)): # if idVariable is None, we want ids. Otherwise, we want the # idVariable column - ids = list(df.get(idVariable, ids)) - id_order = ids - elif isinstance(id_order, str): - ids = df.get(id_order, ids) - id_order = ids - elif idVariable is not None: - ids = df.get(idVariable).tolist() - elif isinstance(ids, str): - ids = df.get(ids).tolist() - w = cls.from_iterable( + id_order = list(df.get(idVariable, ids)) + else: + id_order = df.get(id_order, ids) + + if idVariable is not None: + if ids is None: + warnings.warn( + "`idVariable` is deprecated and will be removed in future. " + "Use `ids` instead.", + FutureWarning, + stacklevel=2, + ) + ids = idVariable + else: + warnings.warn( + "Both `idVariable` and `ids` passed, using `ids`.", + UserWarning, + stacklevel=2, + ) + + if ids is None: + if use_index is None: + warnings.warn( + "`use_index` defaults to False but will default to True in future. " + "Set True/False directly to control this behavior and silence this " + "warning", + FutureWarning, + stacklevel=2, + ) + use_index = False + if use_index: + ids = df.index.tolist() + + else: + if isinstance(ids, str): + ids = df[ids] + + if not isinstance(ids, list): + ids = ids.tolist() + + if len(ids) != len(df): + raise ValueError("The length of `ids` does not match the length of df.") + + if id_order is None: + id_order = ids + + return cls.from_iterable( df[geom_col].tolist(), ids=ids, id_order=id_order, **kwargs ) - return w @classmethod def from_xarray( @@ -457,7 +574,7 @@ def from_xarray( Returns ------- w : libpysal.weights.W/libpysal.weights.WSP - instance of spatial weights class W or WSP with an index attribute + instance of spatial weights class W or WSP with an index attribute Notes ----- @@ -526,17 +643,17 @@ def Voronoi(points, criterion="rook", clip="ahull", **kwargs): def _from_dataframe(df, **kwargs): """ - Construct a voronoi contiguity weight directly from a dataframe. + Construct a voronoi contiguity weight directly from a dataframe. Note that if criterion='rook', this is identical to the delaunay - graph for the points. + graph for the points if no clipping of the voronoi cells is applied. If the input dataframe is of any other geometry type than "Point", - a value error is raised. + a value error is raised. Parameters ---------- df : pandas.DataFrame - dataframe containing point geometries for a + dataframe containing point geometries for a voronoi diagram. Returns @@ -561,14 +678,14 @@ def _from_dataframe(df, **kwargs): def _build(polygons, criterion="rook", ids=None): """ - This is a developer-facing function to construct a spatial weights object. + This is a developer-facing function to construct a spatial weights object. Parameters ---------- polygons : list list of pysal polygons to use to build contiguity criterion : string - option of which kind of contiguity to build. Is either "rook" or "queen" + option of which kind of contiguity to build. Is either "rook" or "queen" ids : list list of ids to use to index the neighbor dictionary @@ -576,12 +693,12 @@ def _build(polygons, criterion="rook", ids=None): ------- tuple containing (neighbors, ids), where neighbors is a dictionary describing contiguity relations and ids is the list of ids used to index - that dictionary. + that dictionary. NOTE: this is different from the prior behavior of buildContiguity, which returned an actual weights object. Since this just dispatches for the classes above, this returns the raw ingredients for a spatial weights - object, not the object itself. + object, not the object itself. """ if ids and len(ids) != len(set(ids)): raise ValueError( @@ -621,7 +738,7 @@ def buildContiguity(polygons, criterion="rook", ids=None): This is a deprecated function. It builds a contiguity W from the polygons provided. As such, it is now - identical to calling the class constructors for Rook or Queen. + identical to calling the class constructors for Rook or Queen. """ # Warn('This function is deprecated. Please use the Rook or Queen classes', # UserWarning) diff --git a/libpysal/weights/distance.py b/libpysal/weights/distance.py index 8c95e9d68..ba35e8548 100644 --- a/libpysal/weights/distance.py +++ b/libpysal/weights/distance.py @@ -83,20 +83,20 @@ class KNN(W): Notes ----- - Ties between neighbors of equal distance are arbitrarily broken. + Ties between neighbors of equal distance are arbitrarily broken. - Further, if many points occupy the same spatial location (i.e. observations are - coincident), then you may need to increase k for those observations to + Further, if many points occupy the same spatial location (i.e. observations are + coincident), then you may need to increase k for those observations to acquire neighbors at different spatial locations. For example, if five points are coincident, then their four nearest neighbors will all occupy the same spatial location; only the fifth nearest neighbor will result in those coincident points becoming connected to the graph as a - whole. + whole. Solutions to this problem include jittering the points (by adding - a small random value to each observation's location) or by adding + a small random value to each observation's location) or by adding higher-k neighbors only to the coincident points, using the - weights.w_sets.w_union() function. + weights.w_sets.w_union() function. See Also -------- @@ -271,7 +271,9 @@ def from_array(cls, array, *args, **kwargs): return cls(array, *args, **kwargs) @classmethod - def from_dataframe(cls, df, geom_col=None, ids=None, *args, **kwargs): + def from_dataframe( + cls, df, geom_col=None, ids=None, use_index=True, *args, **kwargs + ): """ Make KNN weights from a dataframe. @@ -283,10 +285,14 @@ def from_dataframe(cls, df, geom_col=None, ids=None, *args, **kwargs): geom_col : string the name of the column in `df` that contains the geometries. Defaults to active geometry column. - ids : string or iterable - if string, the column name of the indices from the dataframe - if iterable, a list of ids to use for the W - if None, df.index is used. + ids : list-like, string + a list-like of ids to use to index the spatial weights object or + the name of the column to use as IDs. If nothing is + provided, the dataframe index is used if `use_index=True` or + a positional index is used if `use_index=False`. + Order of the resulting W is not respected from this list. + use_index : bool + use index of `df` as `ids` to index the spatial weights object. See Also -------- @@ -295,7 +301,7 @@ def from_dataframe(cls, df, geom_col=None, ids=None, *args, **kwargs): if geom_col is None: geom_col = df.geometry.name pts = get_points_array(df[geom_col]) - if ids is None: + if ids is None and use_index: ids = df.index.tolist() elif isinstance(ids, str): ids = df[ids].tolist() @@ -603,7 +609,7 @@ def from_array(cls, array, **kwargs): return cls(array, **kwargs) @classmethod - def from_dataframe(cls, df, geom_col=None, ids=None, **kwargs): + def from_dataframe(cls, df, geom_col=None, ids=None, use_index=True, **kwargs): """ Make Kernel weights from a dataframe. @@ -615,10 +621,14 @@ def from_dataframe(cls, df, geom_col=None, ids=None, **kwargs): geom_col : string the name of the column in `df` that contains the geometries. Defaults to active geometry column. - ids : string or iterable - if string, the column name of the indices from the dataframe - if iterable, a list of ids to use for the W - if None, df.index is used. + ids : list-like, string + a list-like of ids to use to index the spatial weights object or + the name of the column to use as IDs. If nothing is + provided, the dataframe index is used if `use_index=True` or + a positional index is used if `use_index=False`. + Order of the resulting W is not respected from this list. + use_index : bool + use index of `df` as `ids` to index the spatial weights object. See Also -------- @@ -627,7 +637,7 @@ def from_dataframe(cls, df, geom_col=None, ids=None, **kwargs): if geom_col is None: geom_col = df.geometry.name pts = get_points_array(df[geom_col]) - if ids is None: + if ids is None and use_index: ids = df.index.tolist() elif isinstance(ids, str): ids = df[ids].tolist() @@ -691,13 +701,13 @@ def _eval_kernel(self): elif self.function == "uniform": self.kernel = [np.ones(zi.shape) * 0.5 for zi in zs] elif self.function == "quadratic": - self.kernel = [(3.0 / 4) * (1 - zi ** 2) for zi in zs] + self.kernel = [(3.0 / 4) * (1 - zi**2) for zi in zs] elif self.function == "quartic": - self.kernel = [(15.0 / 16) * (1 - zi ** 2) ** 2 for zi in zs] + self.kernel = [(15.0 / 16) * (1 - zi**2) ** 2 for zi in zs] elif self.function == "gaussian": c = np.pi * 2 c = c ** (-0.5) - self.kernel = [c * np.exp(-(zi ** 2) / 2.0) for zi in zs] + self.kernel = [c * np.exp(-(zi**2) / 2.0) for zi in zs] else: print(("Unsupported kernel function", self.function)) @@ -881,7 +891,9 @@ def from_array(cls, array, threshold, **kwargs): return cls(array, threshold, **kwargs) @classmethod - def from_dataframe(cls, df, threshold, geom_col=None, ids=None, **kwargs): + def from_dataframe( + cls, df, threshold, geom_col=None, ids=None, use_index=True, **kwargs + ): """ Make DistanceBand weights from a dataframe. @@ -894,16 +906,20 @@ def from_dataframe(cls, df, threshold, geom_col=None, ids=None, **kwargs): geom_col : string the name of the column in `df` that contains the geometries. Defaults to active geometry column. - ids : string or iterable - if string, the column name of the indices from the dataframe - if iterable, a list of ids to use for the W - if None, df.index is used. + ids : list-like, string + a list-like of ids to use to index the spatial weights object or + the name of the column to use as IDs. If nothing is + provided, the dataframe index is used if `use_index=True` or + a positional index is used if `use_index=False`. + Order of the resulting W is not respected from this list. + use_index : bool + use index of `df` as `ids` to index the spatial weights object. """ if geom_col is None: geom_col = df.geometry.name pts = get_points_array(df[geom_col]) - if ids is None: + if ids is None and use_index: ids = df.index.tolist() elif isinstance(ids, str): ids = df[ids].tolist() diff --git a/libpysal/weights/gabriel.py b/libpysal/weights/gabriel.py index 4c168f137..cd3276616 100644 --- a/libpysal/weights/gabriel.py +++ b/libpysal/weights/gabriel.py @@ -18,10 +18,10 @@ class Delaunay(W): """ - Constructor of the Delaunay graph of a set of input points. + Constructor of the Delaunay graph of a set of input points. Relies on scipy.spatial.Delaunay and numba to quickly construct a graph from the input set of points. Will be slower without numba, - and will warn if this is missing. + and will warn if this is missing. Parameters ---------- @@ -30,7 +30,7 @@ class Delaunay(W): delaunay triangulation **kwargs : keyword argument list keyword arguments passed directly to weights.W - + Notes ----- The Delaunay triangulation can result in quite a few non-local links among @@ -40,25 +40,35 @@ class Delaunay(W): The weights.Voronoi class builds a voronoi diagram among the points, clips the Voronoi cells, and then constructs an adjacency graph among the clipped cells. This graph among the clipped Voronoi cells generally represents the structure - of local adjacencies better than the "raw" Delaunay graph. + of local adjacencies better than the "raw" Delaunay graph. The weights.gabriel.Gabriel graph constructs a Delaunay graph, but only - includes the "short" links in the Delaunay graph. + includes the "short" links in the Delaunay graph. However, if the unresricted Delaunay triangulation is needed, this class will compute it much more quickly than Voronoi(coordinates, clip=None). """ + def __init__(self, coordinates, **kwargs): try: from numba import njit except ModuleNotFoundError: - warnings.warn("The numba package is used extensively in this module" - " to accelerate the computation of graphs. Without numba," - " these computations may become unduly slow on large data." - ) + warnings.warn( + "The numba package is used extensively in this module" + " to accelerate the computation of graphs. Without numba," + " these computations may become unduly slow on large data." + ) edges, _ = self._voronoi_edges(coordinates) + ids = kwargs.get("ids") + if ids is not None: + ids = numpy.asarray(ids) + edges = numpy.column_stack((ids[edges[:, 0]], ids[edges[:, 1]])) + del kwargs["ids"] + else: + ids = numpy.arange(coordinates.shape[0]) + voronoi_neighbors = pandas.DataFrame(edges).groupby(0)[1].apply(list).to_dict() - W.__init__(self, voronoi_neighbors, **kwargs) + W.__init__(self, voronoi_neighbors, id_order=list(ids), **kwargs) def _voronoi_edges(self, coordinates): dt = _Delaunay(coordinates) @@ -72,53 +82,85 @@ def _voronoi_edges(self, coordinates): return edges, dt @classmethod - def from_dataframe(cls, df, **kwargs): + def from_dataframe(cls, df, geom_col=None, ids=None, use_index=None, **kwargs): """ - Construct a Delaunay triangulation from a geopandas GeoDataFrame. - Not that the input geometries in the dataframe must be Points. - Polygons or lines must be converted to points (e.g. using + Construct a Delaunay triangulation from a geopandas GeoDataFrame. + Not that the input geometries in the dataframe must be Points. + Polygons or lines must be converted to points (e.g. using df.geometry.centroid). Parameters ---------- df : geopandas.GeoDataFrame - GeoDataFrame containing points to construct the Delaunay - Triangulation. + GeoDataFrame containing points to construct the Delaunay + Triangulation. + geom_col : string + the name of the column in `df` that contains the + geometries. Defaults to active geometry column. + ids : list-like, string + a list-like of ids to use to index the spatial weights object or + the name of the column to use as IDs. If nothing is + provided, the dataframe index is used if `use_index=True` or + a positional index is used if `use_index=False`. + Order of the resulting W is not respected from this list. + use_index : bool + use index of `df` as `ids` to index the spatial weights object. **kwargs : keyword arguments Keyword arguments that are passed downwards to the weights.W constructor. """ - geomtypes = df.geometry.type.unique() + if isinstance(df, pandas.Series): + df = df.to_frame("geometry") + if geom_col is None: + geom_col = df.geometry.name + geomtypes = df[geom_col].geom_type.unique() + + if ids is None: + if use_index is None: + warnings.warn( + "`use_index` defaults to False but will default to True in future. " + "Set True/False directly to control this behavior and silence this " + "warning", + FutureWarning, + stacklevel=2, + ) + use_index = False + if use_index: + ids = df.index.tolist() + + elif isinstance(ids, str): + ids = df[ids].tolist() + try: assert len(geomtypes) == 1 - assert geomtypes[0] == 'Point' + assert geomtypes[0] == "Point" point_array = numpy.column_stack( - (df.geometry.x.values, df.geometry.y.values) - ) - return cls(point_array, **kwargs) + (df[geom_col].x.values, df[geom_col].y.values) + ) + return cls(point_array, ids=ids, **kwargs) except AssertionError: raise TypeError( - f'The input dataframe has geometry types {geomtypes}' - f' but this delaunay triangulation is only well-defined for points.' - f' Choose a method to convert your dataframe into points (like using' - f' the df.centroid) and use that to estimate this graph.' - ) + f"The input dataframe has geometry types {geomtypes}" + f" but this delaunay triangulation is only well-defined for points." + f" Choose a method to convert your dataframe into points (like using" + f" the df.centroid) and use that to estimate this graph." + ) class Gabriel(Delaunay): """ - Constructs the Gabriel graph of a set of points. This graph is a subset of - the Delaunay triangulation where only "short" links are retained. This + Constructs the Gabriel graph of a set of points. This graph is a subset of + the Delaunay triangulation where only "short" links are retained. This function is also accelerated using numba, and implemented on top of the - scipy.spatial.Delaunay class. + scipy.spatial.Delaunay class. For a link (i,j) connecting node i to j in the Delaunay triangulation to be retained in the Gabriel graph, it must pass a point set exclusion test: 1. Construct the circle C_ij containing link (i,j) as its diameter - 2. If any other node k is contained within C_ij, then remove link (i,j) - from the graph. - 3. Once all links are evaluated, the remaining graph is the Gabriel graph. + 2. If any other node k is contained within C_ij, then remove link (i,j) + from the graph. + 3. Once all links are evaluated, the remaining graph is the Gabriel graph. Parameters ---------- @@ -128,29 +170,38 @@ class Gabriel(Delaunay): **kwargs : keyword argument list keyword arguments passed directly to weights.W """ + def __init__(self, coordinates, **kwargs): try: from numba import njit except ModuleNotFoundError: - warnings.warn("The numba package is used extensively in this module" - " to accelerate the computation of graphs. Without numba," - " these computations may become unduly slow on large data." - ) - edges, _ = self._voronoi_edges(coordinates) + warnings.warn( + "The numba package is used extensively in this module" + " to accelerate the computation of graphs. Without numba," + " these computations may become unduly slow on large data." + ) edges, dt = self._voronoi_edges(coordinates) droplist = _filter_gabriel( edges, dt.points, ) - output = set(map(tuple, edges)).difference(set(droplist)) + output = numpy.row_stack(list(set(map(tuple, edges)).difference(set(droplist)))) + ids = kwargs.get("ids") + if ids is not None: + ids = numpy.asarray(ids) + output = numpy.column_stack((ids[output[:, 0]], ids[output[:, 1]])) + del kwargs["ids"] + else: + ids = numpy.arange(coordinates.shape[0]) + gabriel_neighbors = pandas.DataFrame(output).groupby(0)[1].apply(list).to_dict() - W.__init__(self, gabriel_neighbors, **kwargs) + W.__init__(self, gabriel_neighbors, id_order=list(ids), **kwargs) class Relative_Neighborhood(Delaunay): """ - Constructs the Relative Neighborhood graph from a set of points. - This graph is a subset of the Delaunay triangulation, where only + Constructs the Relative Neighborhood graph from a set of points. + This graph is a subset of the Delaunay triangulation, where only "relative neighbors" are retained. Further, it is a superset of the Minimum Spanning Tree, with additional "relative neighbors" introduced. @@ -168,37 +219,42 @@ class Relative_Neighborhood(Delaunay): **kwargs : keyword argument list keyword arguments passed directly to weights.W """ + def __init__(self, coordinates, binary=True, **kwargs): try: from numba import njit except ModuleNotFoundError: - warnings.warn("The numba package is used extensively in this module" - " to accelerate the computation of graphs. Without numba," - " these computations may become unduly slow on large data." - ) - edges, _ = self._voronoi_edges(coordinates) + warnings.warn( + "The numba package is used extensively in this module" + " to accelerate the computation of graphs. Without numba," + " these computations may become unduly slow on large data." + ) edges, dt = self._voronoi_edges(coordinates) - output, dkmax = _filter_relativehood( - edges, dt.points, return_dkmax=False - ) + output, dkmax = _filter_relativehood(edges, dt.points, return_dkmax=False) row, col, data = zip(*output) if binary: data = numpy.ones_like(col, dtype=float) - sp = sparse.csc_matrix((data, (row, col))) #TODO: faster way than this? - tmp = WSP(sp).to_W() - W.__init__(self, tmp.neighbors, tmp.weights, **kwargs) - + sp = sparse.csc_matrix((data, (row, col))) # TODO: faster way than this? + ids = kwargs.get("ids") + if ids is None: + ids = numpy.arange(sp.shape[0]) + else: + del kwargs["ids"] + ids = list(ids) + tmp = WSP(sp, id_order=ids).to_W() + W.__init__(self, tmp.neighbors, tmp.weights, id_order=ids, **kwargs) #### utilities + @njit def _edges_from_simplices(simplices): """ - Construct the sets of links that correspond to the edges of each + Construct the sets of links that correspond to the edges of each simplex. Each simplex has three "sides," and thus six undirected - edges. Thus, the input should be a list of three-length tuples, - that are then converted into the six non-directed edges for + edges. Thus, the input should be a list of three-length tuples, + that are then converted into the six non-directed edges for each simplex. """ edges = [] @@ -216,20 +272,20 @@ def _edges_from_simplices(simplices): def _filter_gabriel(edges, coordinates): """ For an input set of edges and coordinates, filter the input edges - depending on the Gabriel rule: + depending on the Gabriel rule: For each simplex, let i,j be the diameter of the circle defined by edge (i,j), and let k be the third point defining the simplex. The limiting case for the Gabriel rule is when k is also on the circle with diameter (i,j). In this limiting case, then simplex ijk must - be a right triangle, and dij**2 = djk**2 + dki**2 (by thales theorem). + be a right triangle, and dij**2 = djk**2 + dki**2 (by thales theorem). - This means that when dij**2 > djk**2 + dki**2, then k is inside the circle. - In contrast, when dij**2 < djk**2 + dji*2, k is outside of the circle. + This means that when dij**2 > djk**2 + dki**2, then k is inside the circle. + In contrast, when dij**2 < djk**2 + dji*2, k is outside of the circle. Therefore, it's sufficient to take each observation i, iterate over its - Delaunay neighbors j,k, and remove links whre dij**2 > djk**2 + dki**2 - in order to construct the Gabriel graph. + Delaunay neighbors j,k, and remove links whre dij**2 > djk**2 + dki**2 + in order to construct the Gabriel graph. """ edge_pointer = 0 n = edges.max() @@ -264,7 +320,7 @@ def _filter_gabriel(edges, coordinates): @njit def _filter_relativehood(edges, coordinates, return_dkmax=False): """ - This is a direct implementation of the algorithm from Toussaint (1980), RNG-2 + This is a direct implementation of the algorithm from Toussaint (1980), RNG-2 1. Compute the delaunay 2. for each edge of the delaunay (i,j), compute @@ -282,12 +338,12 @@ def _filter_relativehood(edges, coordinates, return_dkmax=False): pi = coordinates[i] pj = coordinates[j] dkmax = 0 - dij = ((pi - pj)**2).sum()**.5 + dij = ((pi - pj) ** 2).sum() ** 0.5 prune = False for k in range(n): pk = coordinates[k] - dik = ((pi - pk)**2).sum()**.5 - djk = ((pj - pk)**2).sum()**.5 + dik = ((pi - pk) ** 2).sum() ** 0.5 + djk = ((pj - pk) ** 2).sum() ** 0.5 distances = numpy.array([dik, djk, dkmax]) dkmax = distances.max() prune = dkmax < dij @@ -300,4 +356,3 @@ def _filter_relativehood(edges, coordinates, return_dkmax=False): r.append(dkmax) return out, r - diff --git a/libpysal/weights/tests/test_contiguity.py b/libpysal/weights/tests/test_contiguity.py index 06a30414d..ea6b3877f 100644 --- a/libpysal/weights/tests/test_contiguity.py +++ b/libpysal/weights/tests/test_contiguity.py @@ -21,6 +21,7 @@ try: import shapely + HAS_SHAPELY = True except ImportError: HAS_SHAPELY = False @@ -97,7 +98,6 @@ def test_from_array(self): # test named, sparse from point array pass - @ut.skipIf(PANDAS_EXTINCT, "Missing pandas") def test_from_dataframe(self): # basic @@ -110,7 +110,6 @@ def test_from_dataframe(self): w = self.cls.from_dataframe(df, geom_col="the_geom") self.assertEqual(w[self.known_wi], self.known_w) - @ut.skipIf(GEOPANDAS_EXTINCT, "Missing geopandas") def test_from_geodataframe(self): df = pdio.read_files(self.polygon_path) @@ -121,15 +120,25 @@ def test_from_geodataframe(self): self.assertEqual(w[self.known_wi], self.known_w) # named geometry + named obs - w = self.cls.from_dataframe(df, geom_col="the_geom", idVariable=self.idVariable) + w = self.cls.from_dataframe(df, geom_col="the_geom", ids=self.idVariable) self.assertEqual(w[self.known_name], self.known_namedw) + @ut.skipIf(GEOPANDAS_EXTINCT, "Missing geopandas") + def test_from_geodataframe_order(self): + import geopandas + + south = geopandas.read_file(pysal_examples.get_path("south.shp")) + expected = south.FIPS.iloc[:5].tolist() + for ids_ in ("FIPS", south.FIPS): + w = self.cls.from_dataframe(south, ids=ids_) + self.assertEqual(w.id_order[:5], expected) + def test_from_xarray(self): w = self.cls.from_xarray(self.da, sparse=False, n_jobs=-1) self.assertEqual(w[self.known_wi_da], self.known_w_da) ws = self.cls.from_xarray(self.da) srowvec = ws.sparse[self.known_wspi_da].todense().tolist()[0] - this_w = {i:k for i,k in enumerate(srowvec) if k>0} + this_w = {i: k for i, k in enumerate(srowvec) if k > 0} self.assertEqual(this_w, self.known_wsp_da) @@ -151,7 +160,7 @@ def setUp(self): self.cls = c.Queen self.idVariable = "POLYID" self.known_name = 5 - self.known_namedw = {k+1:v for k,v in list(self.known_w.items())} + self.known_namedw = {k + 1: v for k, v in list(self.known_w.items())} self.known_wspi_da = 1 self.known_wsp_da = {0: 1, 2: 1, 4: 1, 5: 1, 6: 1} self.known_wi_da = (1, -30.0, -60.0) @@ -163,8 +172,8 @@ def setUp(self): (1, -30.0, 60.0): 1, (1, 30.0, -180.0): 1, (1, 30.0, -60.0): 1, - (1, 30.0, 60.0): 1 - } + (1, 30.0, 60.0): 1, + } @ut.skipIf(GEOPANDAS_EXTINCT, "Missing Geopandas") def test_linestrings(self): @@ -197,11 +206,15 @@ def setUp(self): self.cls = c.Rook self.idVariable = "POLYID" self.known_name = 5 - self.known_namedw = {k+1:v for k,v in list(self.known_w.items())} + self.known_namedw = {k + 1: v for k, v in list(self.known_w.items())} self.known_wspi_da = 1 self.known_wsp_da = {0: 1, 2: 1, 5: 1} self.known_wi_da = (1, -30.0, -180.0) - self.known_w_da = {(1, 30.0, -180.0): 1, (1, -30.0, -60.0): 1, (1, -90.0, -180.0): 1} + self.known_w_da = { + (1, 30.0, -180.0): 1, + (1, -30.0, -60.0): 1, + (1, -90.0, -180.0): 1, + } class Test_Voronoi(ut.TestCase):