Skip to content

[WIP] shared perimeter-weighted contiguity #507

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
97 changes: 94 additions & 3 deletions libpysal/weights/contiguity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
import warnings

import numpy
import pandas as pd

from ..cg import voronoi_frames
from ..io.fileio import FileIO
from ._contW_lists import ContiguityWeightsLists
from .raster import da2W, da2WSP
from .util import get_ids, get_points_array
from .weights import WSP, W
from .raster import da2W, da2WSP

try:
from shapely.geometry import Point as shapely_point

from ..cg.shapes import Point as pysal_point

point_type = (shapely_point, pysal_point)
Expand Down Expand Up @@ -141,6 +143,8 @@ def from_dataframe(
ids=None,
id_order=None,
use_index=None,
perimeter=False,
perim_std=False,
**kwargs,
):
"""
Expand Down Expand Up @@ -175,6 +179,12 @@ def from_dataframe(
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.
perimeter : bool
if True, use the length of the shared boundary between adjacent units as
the weight value
perim_std : bool
if True, (and `perimeter==True`), then perimeter weights are set to the
ratio of the shared boudary length to the focal unit's perimeter

See Also
--------
Expand Down Expand Up @@ -239,9 +249,13 @@ def from_dataframe(
if id_order is None:
id_order = ids

return cls.from_iterable(
w = cls.from_iterable(
df[geom_col].tolist(), ids=ids, id_order=id_order, **kwargs
)
if perimeter:
w = _return_length_weighted_w(w, df, perim_std)
return w


@classmethod
def from_xarray(
Expand Down Expand Up @@ -427,6 +441,8 @@ def from_dataframe(
ids=None,
id_order=None,
use_index=None,
perimeter=False,
perim_std=False,
**kwargs,
):
"""
Expand Down Expand Up @@ -461,6 +477,12 @@ def from_dataframe(
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.
perimeter : bool
if True, use the length of the shared boundary between adjacent units as
the weight value
perim_std : bool
if True, (and `perimeter==True`), then perimeter weights are set to the
ratio of the shared boudary length to the focal unit's perimeter

See Also
--------
Expand Down Expand Up @@ -525,9 +547,13 @@ def from_dataframe(
if id_order is None:
id_order = ids

return cls.from_iterable(
w = cls.from_iterable(
df[geom_col].tolist(), ids=ids, id_order=id_order, **kwargs
)
if perimeter:
w = _return_length_weighted_w(w, df, perim_std)
return w


@classmethod
def from_xarray(
Expand Down Expand Up @@ -748,3 +774,68 @@ def buildContiguity(polygons, criterion="rook", ids=None):
return Queen(polygons, ids=ids)
else:
raise Exception('Weights criterion "{}" was not found.'.format(criterion))


def _return_length_weighted_w(w, data, perimeter_standardize):
"""Return a W object whose value is the length of the common boundary of two areal units that share border.

Parameters
----------
w : libpsal.weights.Rook
data : pandas.DataFrame
perimeter_standardize: bool
if True, scale the weight value equal to the shared
boundary divided by the total boundary of the focal unit.

Returns
--------
w : libpysal.weights.W
weights object with values equal to the shared border of ij

"""
try:
import geopandas as gpd
except ImportError as e:
raise e('You must have geopandas installed to create perimeter-weighted weights')
adjlist = w.to_adjlist()
islands = pd.DataFrame.from_records(
[{"focal": island, "neighbor": island, "weight": 0} for island in w.islands]
)
merged = adjlist.merge(
data.geometry.to_frame("geometry"),
left_on="focal",
right_index=True,
how="left",
).merge(
data.geometry.to_frame("geometry"),
left_on="neighbor",
right_index=True,
how="left",
suffixes=("_focal", "_neighbor"),
)
# Transforming from pandas to geopandas
merged = gpd.GeoDataFrame(merged, geometry="geometry_focal")
total_boundary_length = merged.boundary.length
merged["geometry_neighbor"] = gpd.GeoSeries(merged.geometry_neighbor)

# Getting the shared boundaries
merged["shared_boundary"] = merged.geometry_focal.intersection(
merged["geometry_neighbor"]
)

# Putting it back to a matrix
if perimeter_standardize:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ljwolf do we want to keep the option to standardize when the boundary isnt exhausted? (so the denom is boundary_i instead of \sum{sharedboundary_ij})?

merged['weight'] = merged["shared_boundary"].length / total_boundary_length
else:
merged["weight"] = merged["shared_boundary"].length
merged_with_islands = pd.concat((merged, islands))
length_weighted_w = W.from_adjlist(
merged_with_islands[["focal", "neighbor", "weight"]]
)
for island in w.islands:
length_weighted_w.neighbors[island] = []
del length_weighted_w.weights[island]

length_weighted_w._reset()

return length_weighted_w
56 changes: 56 additions & 0 deletions libpysal/weights/tests/test_perimeter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import tempfile

import unittest
import pytest
import numpy as np
import geopandas as gpd
from ..weights import W, WSP
from ..user import build_lattice_shapefile
from .. import util
from ..contiguity import Rook, _return_length_weighted_w
from ... import examples

NPTA3E = np.testing.assert_array_almost_equal


class TestPerimeter(unittest.TestCase):
def setUp(self):
shp = build_lattice_shapefile(3, 3, "tmp.shp")
gdf = gpd.read_file("tmp.shp")
dv = [0] * 3
dv.extend(list(range(1, 7)))
gdf["dv"] = dv
gdf = gdf.dissolve(by="dv")
self.w0 = Rook.from_dataframe(gdf, perimeter=True, perim_std=False)
self.gdf = gdf

# us case
usgdf = gpd.read_file(examples.get_path("us48.shp"))
usgdf.set_crs("epsg:4326", inplace=True)
usgdf.to_crs(usgdf.estimate_utm_crs(), inplace=True)
self.usgdf = usgdf
self.wus = Rook.from_dataframe(usgdf, perimeter=True)

def test_perimeter(self):
NPTA3E(self.w0.pct_nonzero, 40.81632653)

def test_return_length_weighted(self):
w1 = _return_length_weighted_w(self.w0, self.gdf, False)
NPTA3E(w1.pct_nonzero, 40.81632653)
self.assertEqual(w1.weights[0], [1, 1, 1])
self.assertEqual(w1.weights[2], [1, 1, 1, 1])

def test_return_length_weighted_us(self):
w1 = _return_length_weighted_w(self.wus, self.usgdf, False)
self.assertAlmostEqual(w1[0][7], 354625.0684908081)
self.assertAlmostEqual(w1[0][10], 605834.5010118643)
NPTA3E(w1[0][7], w1[7][0])
w1.transform = "r"
self.assertAlmostEqual(w1[0][7], 0.3692243585791264)
self.assertAlmostEqual(w1[7][0], 0.12891667056448083)
self.assertNotAlmostEquals(w1[0][7], w1[7][0])


if __name__ == "__main__":
unittest.main()
Loading