From 56333c4f80e9fa76c0f3e6c9098174fd9bf5108f Mon Sep 17 00:00:00 2001 From: Krasen Samardzhiev Date: Fri, 3 May 2024 14:39:37 +0200 Subject: [PATCH 1/3] functional density --- momepy/functional/_intensity.py | 56 ++++++++++++++++++++++- momepy/functional/tests/test_intensity.py | 40 ++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/momepy/functional/_intensity.py b/momepy/functional/_intensity.py index 0b4fe6bb..8531d58a 100644 --- a/momepy/functional/_intensity.py +++ b/momepy/functional/_intensity.py @@ -1,9 +1,11 @@ +import numpy as np +import pandas as pd import shapely from geopandas import GeoDataFrame, GeoSeries from libpysal.graph import Graph from pandas import Series -__all__ = ["courtyards"] +__all__ = ["courtyards", "density"] def courtyards(geometry: GeoDataFrame | GeoSeries, graph: Graph) -> Series: @@ -48,3 +50,55 @@ def _calculate_courtyards(group): ) return result + + +def density(values, areas, graph) -> pd.Series: + """ + Calculate the gross density. + + .. math:: + \\frac{\\sum \\text {values}}{\\sum \\text {areas}} + + Adapted from :cite:`dibble2017`. + + Parameters + ---------- + values : pd.Series, pd.DataFrame + The character values for density calculations. + The index is used to arrange the final results. + areas : np.array, pd.Series + The area values for the density calculations, + an ``np.array``, or ``pd.Series``. + graph : libpysal.graph.Graph + A spatial weights matrix for the geodataframe, + it is used to denote adjacent elements. + + Returns + ------- + DataFrame + + + Examples + -------- + >>> tessellation_df['floor_area_dens'] = mm.density(tessellation_df['floor_area'], + ... tessellation_df['area'], + ... graph) + """ + + if isinstance(values, np.ndarray): + values = pd.DataFrame(values) + elif isinstance(values, pd.Series): + values = values.to_frame() + + if isinstance(areas, np.ndarray): + areas = pd.Series(values) + + stats = graph.apply( + pd.concat((values, areas.rename("area")), axis=1), + lambda x: (x.loc[:, x.columns != "area"].sum() / x["area"].sum()), + ) + result = pd.DataFrame( + np.full(values.shape, np.nan), index=values.index, columns=values.columns + ) + result[values.columns] = stats[values.columns] + return result diff --git a/momepy/functional/tests/test_intensity.py b/momepy/functional/tests/test_intensity.py index 18e25122..4ddb5546 100644 --- a/momepy/functional/tests/test_intensity.py +++ b/momepy/functional/tests/test_intensity.py @@ -38,6 +38,23 @@ def test_courtyards(self): expected = {"mean": 0.6805555555555556, "sum": 98, "min": 0, "max": 1} assert_result(courtyards, expected, self.df_buildings) + def test_density(self): + graph = ( + Graph.build_contiguity(self.df_tessellation, rook=False) + .higher_order(k=3, lower_order=True) + .assign_self_weight() + ) + dens_new = mm.density( + self.df_buildings["fl_area"], self.df_tessellation.geometry.area, graph + ) + dens_expected = { + "count": 144, + "mean": 1.6615871155383324, + "max": 2.450536855278486, + "min": 0.9746481727569978, + } + assert_result(dens_new["fl_area"], dens_expected, self.df_tessellation) + class TestIntensityEquality: def setup_method(self): @@ -70,3 +87,26 @@ def test_courtyards(self): assert_series_equal( new_courtyards, old_courtyards, check_names=False, check_dtype=False ) + + def test_density(self): + sw = mm.sw_high(k=3, gdf=self.df_tessellation, ids="uID") + graph = ( + Graph.build_contiguity(self.df_tessellation, rook=False) + .higher_order(k=3, lower_order=True) + .assign_self_weight() + ) + dens_new = mm.density( + self.df_buildings["fl_area"], self.df_tessellation.geometry.area, graph + ) + + dens_old = mm.Density( + self.df_tessellation, + self.df_buildings["fl_area"], + sw, + "uID", + self.df_tessellation.area, + ).series + + assert_series_equal( + dens_new["fl_area"], dens_old, check_names=False, check_dtype=False + ) From 6caece2013b20fe02fa1a09e05947d1685511466 Mon Sep 17 00:00:00 2001 From: Krasen Samardzhiev Date: Fri, 31 May 2024 15:49:56 +0200 Subject: [PATCH 2/3] density using describe --- momepy/functional/_intensity.py | 23 ++++++++++------------- momepy/functional/tests/test_intensity.py | 8 ++++++-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/momepy/functional/_intensity.py b/momepy/functional/_intensity.py index 0e203dcd..2e540d2f 100644 --- a/momepy/functional/_intensity.py +++ b/momepy/functional/_intensity.py @@ -3,8 +3,11 @@ import shapely from geopandas import GeoDataFrame, GeoSeries from libpysal.graph import Graph +from numpy.typing import NDArray from pandas import Series +from momepy import describe + __all__ = ["courtyards", "node_density", "density"] @@ -114,7 +117,9 @@ def _calc_nodedensity(group, edges): def density( - values: Series | np.ndarray, areas: Series | np.ndarray, graph: Graph + values: Series | NDArray[np.float_], + areas: Series | NDArray[np.float_], + graph: Graph, ) -> Series: """Calculate the gross density. @@ -148,19 +153,11 @@ def density( """ if isinstance(values, np.ndarray): - values = pd.DataFrame(values) - elif isinstance(values, pd.Series): - values = values.to_frame() + values = pd.Series(values) if isinstance(areas, np.ndarray): areas = pd.Series(values) - stats = graph.apply( - pd.concat((values, areas.rename("area")), axis=1), - lambda x: (x.loc[:, x.columns != "area"].sum() / x["area"].sum()), - ) - result = pd.DataFrame( - np.full(values.shape, np.nan), index=values.index, columns=values.columns - ) - result[values.columns] = stats[values.columns] - return result + stats = describe(values, graph)["sum"] + areas = describe(areas, graph)["sum"] + return stats / areas diff --git a/momepy/functional/tests/test_intensity.py b/momepy/functional/tests/test_intensity.py index d6667511..f2c3e983 100644 --- a/momepy/functional/tests/test_intensity.py +++ b/momepy/functional/tests/test_intensity.py @@ -54,7 +54,7 @@ def test_density(self): "max": 2.450536855278486, "min": 0.9746481727569978, } - assert_result(dens_new["fl_area"], dens_expected, self.df_tessellation) + assert_result(dens_new, dens_expected, self.df_tessellation, exact=False) def test_node_density(self): nx = mm.gdf_to_nx(self.df_streets, integer_labels=True) @@ -160,7 +160,11 @@ def test_density(self): ).series assert_series_equal( - dens_new["fl_area"], dens_old, check_names=False, check_dtype=False + dens_new, + dens_old, + check_names=False, + check_dtype=False, + check_index_type=False, ) def test_node_density(self): From 392e44f2259506bdfd8438e67261ed4cd76d2d12 Mon Sep 17 00:00:00 2001 From: Krasen Samardzhiev Date: Wed, 5 Jun 2024 18:14:18 +0200 Subject: [PATCH 3/3] using graph.describe --- momepy/functional/_intensity.py | 52 +---------------------- momepy/functional/tests/test_diversity.py | 23 ++++++++++ momepy/functional/tests/test_intensity.py | 24 ++--------- 3 files changed, 28 insertions(+), 71 deletions(-) diff --git a/momepy/functional/_intensity.py b/momepy/functional/_intensity.py index 2e540d2f..09a6cf4c 100644 --- a/momepy/functional/_intensity.py +++ b/momepy/functional/_intensity.py @@ -3,12 +3,9 @@ import shapely from geopandas import GeoDataFrame, GeoSeries from libpysal.graph import Graph -from numpy.typing import NDArray from pandas import Series -from momepy import describe - -__all__ = ["courtyards", "node_density", "density"] +__all__ = ["courtyards", "node_density"] def courtyards(geometry: GeoDataFrame | GeoSeries, graph: Graph) -> Series: @@ -114,50 +111,3 @@ def _calc_nodedensity(group, edges): summation_values = pd.Series(np.ones(nodes.shape[0]), index=nodes.index) return graph.apply(summation_values, _calc_nodedensity, edges=edges) - - -def density( - values: Series | NDArray[np.float_], - areas: Series | NDArray[np.float_], - graph: Graph, -) -> Series: - """Calculate the gross density. - - .. math:: - \\frac{\\sum \\text {values}}{\\sum \\text {areas}} - - Adapted from :cite:`dibble2017`. - - Parameters - ---------- - values : pd.Series | np.ndarray - The character values for density calculations. - The index is used to arrange the final results. - areas : np.array | pd.Series - The area values for the density calculations, - an ``np.ndarray``, or ``pd.Series``. - graph : libpysal.graph.Graph - A spatial weights matrix for the geodataframe, - it is used to denote adjacent elements. - - Returns - ------- - DataFrame - - - Examples - -------- - >>> tessellation_df['floor_area_dens'] = mm.density(tessellation_df['floor_area'], - ... tessellation_df['area'], - ... graph) - """ - - if isinstance(values, np.ndarray): - values = pd.Series(values) - - if isinstance(areas, np.ndarray): - areas = pd.Series(values) - - stats = describe(values, graph)["sum"] - areas = describe(areas, graph)["sum"] - return stats / areas diff --git a/momepy/functional/tests/test_diversity.py b/momepy/functional/tests/test_diversity.py index 78e472c0..8cc66b8b 100644 --- a/momepy/functional/tests/test_diversity.py +++ b/momepy/functional/tests/test_diversity.py @@ -279,6 +279,29 @@ def test_na_results(self): assert_frame_equal(pandas_agg_vals, numba_agg_vals) + def test_density(self): + graph = ( + Graph.build_contiguity(self.df_tessellation, rook=False) + .higher_order(k=3, lower_order=True) + .assign_self_weight() + ) + fl_area = graph.describe(self.df_buildings["fl_area"])["sum"] + tess_area = graph.describe(self.df_tessellation["area"])["sum"] + dens_new = fl_area / tess_area + dens_expected = { + "count": 144, + "mean": 1.6615871155383324, + "max": 2.450536855278486, + "min": 0.9746481727569978, + } + assert_result( + dens_new, + dens_expected, + self.df_tessellation, + exact=False, + check_names=False, + ) + class TestDescribeEquality: def setup_method(self): diff --git a/momepy/functional/tests/test_intensity.py b/momepy/functional/tests/test_intensity.py index f2c3e983..dc7ae702 100644 --- a/momepy/functional/tests/test_intensity.py +++ b/momepy/functional/tests/test_intensity.py @@ -39,23 +39,6 @@ def test_courtyards(self): expected = {"mean": 0.6805555555555556, "sum": 98, "min": 0, "max": 1} assert_result(courtyards, expected, self.df_buildings) - def test_density(self): - graph = ( - Graph.build_contiguity(self.df_tessellation, rook=False) - .higher_order(k=3, lower_order=True) - .assign_self_weight() - ) - dens_new = mm.density( - self.df_buildings["fl_area"], self.df_tessellation.geometry.area, graph - ) - dens_expected = { - "count": 144, - "mean": 1.6615871155383324, - "max": 2.450536855278486, - "min": 0.9746481727569978, - } - assert_result(dens_new, dens_expected, self.df_tessellation, exact=False) - def test_node_density(self): nx = mm.gdf_to_nx(self.df_streets, integer_labels=True) nx = mm.node_degree(nx) @@ -147,9 +130,10 @@ def test_density(self): .higher_order(k=3, lower_order=True) .assign_self_weight() ) - dens_new = mm.density( - self.df_buildings["fl_area"], self.df_tessellation.geometry.area, graph - ) + + fl_area = graph.describe(self.df_buildings["fl_area"])["sum"] + tess_area = graph.describe(self.df_tessellation["area"])["sum"] + dens_new = fl_area / tess_area dens_old = mm.Density( self.df_tessellation,