From 2ba809b24fdd1ea1c444e9bda452cb543db74d4d Mon Sep 17 00:00:00 2001 From: Ted Wong Date: Wed, 28 Aug 2024 10:53:51 -0400 Subject: [PATCH 1/3] Create percentpop_euclideanprox_openspace.py --- .../metrics/percentpop_euclideanprox_openspace.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 city_metrix/metrics/percentpop_euclideanprox_openspace.py diff --git a/city_metrix/metrics/percentpop_euclideanprox_openspace.py b/city_metrix/metrics/percentpop_euclideanprox_openspace.py new file mode 100644 index 0000000..f1983e1 --- /dev/null +++ b/city_metrix/metrics/percentpop_euclideanprox_openspace.py @@ -0,0 +1,13 @@ +from geopandas import GeoDataFrame, GeoSeries + +from city_metrix.layers import EsaWorldCover, EsaWorldCoverClass, OpenStreetMap, OpenStreetMapClass + + +def urban_open_space(zones: GeoDataFrame) -> GeoSeries: + built_up_land = EsaWorldCover(land_cover_class=EsaWorldCoverClass.BUILT_UP) + open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE) + + open_space_in_built_land = open_space.mask(built_up_land).groupby(zones).count() + built_land_counts = built_up_land.groupby(zones).count() + + return open_space_in_built_land.fillna(0) / built_land_counts From 7129acf0cdc6c0985828d8a9937994e4f823d334 Mon Sep 17 00:00:00 2001 From: Ted Wong Date: Mon, 23 Sep 2024 17:07:02 -0400 Subject: [PATCH 2/3] new indicator --- city_metrix/layers/open_street_map.py | 10 ++++++++- .../percentpop_euclideanprox_openspace.py | 21 +++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/city_metrix/layers/open_street_map.py b/city_metrix/layers/open_street_map.py index 45f0900..d1f1eab 100644 --- a/city_metrix/layers/open_street_map.py +++ b/city_metrix/layers/open_street_map.py @@ -4,6 +4,7 @@ import pandas as pd from .layer import Layer +from .layer import get_utm_zone_epsg class OpenStreetMapClass(Enum): @@ -35,9 +36,10 @@ class OpenStreetMapClass(Enum): class OpenStreetMap(Layer): - def __init__(self, osm_class=None, **kwargs): + def __init__(self, osm_class=None, buffer_distance=None, **kwargs): super().__init__(**kwargs) self.osm_class = osm_class + self.buffer_distance = buffer_distance # meters def get_data(self, bbox): north, south, east, west = bbox[3], bbox[1], bbox[0], bbox[2] @@ -71,4 +73,10 @@ def get_data(self, bbox): keep_col.append('lanes') osm_feature = osm_feature.reset_index()[keep_col] + if self.buffer_distance is not None: + target_crs = get_utm_zone_epsg(bbox) + osm_feature_utm = osm_feature.to_crs(target_crs) + osm_feature_utm_buffered = osm_feature_utm.buffer(self.buffer_distance) + osm_feature = osm_feature_utm_buffered.to_crs("EPSG:4326") + return osm_feature diff --git a/city_metrix/metrics/percentpop_euclideanprox_openspace.py b/city_metrix/metrics/percentpop_euclideanprox_openspace.py index f1983e1..115d901 100644 --- a/city_metrix/metrics/percentpop_euclideanprox_openspace.py +++ b/city_metrix/metrics/percentpop_euclideanprox_openspace.py @@ -1,13 +1,16 @@ from geopandas import GeoDataFrame, GeoSeries +from city_metrix.layers import OpenStreetMap, OpenStreetMapClass, WorldPop +from city_metrix.layers.layer import get_utm_zone_epsg -from city_metrix.layers import EsaWorldCover, EsaWorldCoverClass, OpenStreetMap, OpenStreetMapClass -def urban_open_space(zones: GeoDataFrame) -> GeoSeries: - built_up_land = EsaWorldCover(land_cover_class=EsaWorldCoverClass.BUILT_UP) - open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE) - - open_space_in_built_land = open_space.mask(built_up_land).groupby(zones).count() - built_land_counts = built_up_land.groupby(zones).count() - - return open_space_in_built_land.fillna(0) / built_land_counts +def percentpop_euclideanprox_openspace(zones: GeoDataFrame, distance=400) -> GeoSeries: +# (Later add agesex_classes) + open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE, buffer_distance=distance).groupby(zones) + population = WorldPop.get_data(bbox).fillna(0) + open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE, buffer_distance=distance) + population = WorldPop() + population_masked_byzone = population.mask(open_space).groupby(zones) + population_byzone = population.groupby(zones) + result = (population_masked_byzone.mean() * population_masked_byzone.count()) / (population_byzone.mean() * population_byzone.count()) + return result From 2f8507e0c89410f3649e3ed050cf682af18f4211 Mon Sep 17 00:00:00 2001 From: weiqi-tori Date: Wed, 25 Sep 2024 16:45:31 +0800 Subject: [PATCH 3/3] add sum() function and fix metrix error --- city_metrix/layers/layer.py | 5 +++++ city_metrix/layers/open_street_map.py | 12 +++++++++++- city_metrix/metrics/__init__.py | 3 ++- .../percentpop_euclideanprox_openspace.py | 16 ---------------- city_metrix/metrics/pop_open_space.py | 13 +++++++++++++ tests/test_metrics.py | 7 +++++++ 6 files changed, 38 insertions(+), 18 deletions(-) delete mode 100644 city_metrix/metrics/percentpop_euclideanprox_openspace.py create mode 100644 city_metrix/metrics/pop_open_space.py diff --git a/city_metrix/layers/layer.py b/city_metrix/layers/layer.py index c86e9fe..c2d5c82 100644 --- a/city_metrix/layers/layer.py +++ b/city_metrix/layers/layer.py @@ -96,6 +96,9 @@ def mean(self): def count(self): return self._zonal_stats("count") + + def sum(self): + return self._zonal_stats("sum") def _zonal_stats(self, stats_func): if box(*self.zones.total_bounds).area <= MAX_TILE_SIZE**2: @@ -251,6 +254,8 @@ def _aggregate_stats(df, stats_func): elif stats_func == "mean": # mean must weight by number of pixels used for each tile return (df["mean"] * df["count"]).sum() / df["count"].sum() + elif stats_func == "sum": + return df["sum"].sum() def get_stats_funcs(stats_func): diff --git a/city_metrix/layers/open_street_map.py b/city_metrix/layers/open_street_map.py index a4e5489..1b50c55 100644 --- a/city_metrix/layers/open_street_map.py +++ b/city_metrix/layers/open_street_map.py @@ -35,9 +35,16 @@ class OpenStreetMapClass(Enum): class OpenStreetMap(Layer): - def __init__(self, osm_class=None, **kwargs): + """ + Attributes: + osm_class: Enum value from OpenStreetMapClass + buffer_distance: meters distance for buffer around osm features + """ + + def __init__(self, osm_class=None, buffer_distance=None, **kwargs): super().__init__(**kwargs) self.osm_class = osm_class + self.buffer_distance = buffer_distance # meters def get_data(self, bbox): north, south, east, west = bbox[3], bbox[1], bbox[0], bbox[2] @@ -74,4 +81,7 @@ def get_data(self, bbox): crs = get_utm_zone_epsg(bbox) osm_feature = osm_feature.to_crs(crs) + if self.buffer_distance: + osm_feature['geometry'] = osm_feature.geometry.buffer(self.buffer_distance) + return osm_feature diff --git a/city_metrix/metrics/__init__.py b/city_metrix/metrics/__init__.py index 10b559c..1e6cfef 100644 --- a/city_metrix/metrics/__init__.py +++ b/city_metrix/metrics/__init__.py @@ -2,5 +2,6 @@ from .built_land_with_low_surface_reflectivity import built_land_with_low_surface_reflectivity from .built_land_with_high_land_surface_temperature import built_land_with_high_land_surface_temperature from .mean_tree_cover import mean_tree_cover +from .natural_areas import natural_areas +from .pop_open_space import pop_open_space from .urban_open_space import urban_open_space -from .natural_areas import natural_areas \ No newline at end of file diff --git a/city_metrix/metrics/percentpop_euclideanprox_openspace.py b/city_metrix/metrics/percentpop_euclideanprox_openspace.py deleted file mode 100644 index 115d901..0000000 --- a/city_metrix/metrics/percentpop_euclideanprox_openspace.py +++ /dev/null @@ -1,16 +0,0 @@ -from geopandas import GeoDataFrame, GeoSeries -from city_metrix.layers import OpenStreetMap, OpenStreetMapClass, WorldPop -from city_metrix.layers.layer import get_utm_zone_epsg - - - -def percentpop_euclideanprox_openspace(zones: GeoDataFrame, distance=400) -> GeoSeries: -# (Later add agesex_classes) - open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE, buffer_distance=distance).groupby(zones) - population = WorldPop.get_data(bbox).fillna(0) - open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE, buffer_distance=distance) - population = WorldPop() - population_masked_byzone = population.mask(open_space).groupby(zones) - population_byzone = population.groupby(zones) - result = (population_masked_byzone.mean() * population_masked_byzone.count()) / (population_byzone.mean() * population_byzone.count()) - return result diff --git a/city_metrix/metrics/pop_open_space.py b/city_metrix/metrics/pop_open_space.py new file mode 100644 index 0000000..1676c02 --- /dev/null +++ b/city_metrix/metrics/pop_open_space.py @@ -0,0 +1,13 @@ +from geopandas import GeoDataFrame, GeoSeries +from city_metrix.layers import OpenStreetMap, OpenStreetMapClass, WorldPop + + +def pop_open_space(zones: GeoDataFrame, buffer_distance=400) -> GeoSeries: +# (Later add agesex_classes) + pop = WorldPop() + open_space = OpenStreetMap(osm_class=OpenStreetMapClass.OPEN_SPACE, buffer_distance=buffer_distance) + + pop_open_space_sum = pop.mask(open_space).groupby(zones).sum() + pop_sum = pop.groupby(zones).sum() + + return pop_open_space_sum / pop_sum diff --git a/tests/test_metrics.py b/tests/test_metrics.py index dcd731b..b037094 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -37,6 +37,13 @@ def test_natural_areas(): assert expected_zone_size == actual_indicator_size +def test_pop_open_space(): + indicator = pop_open_space(ZONES) + expected_zone_size = ZONES.geometry.size + actual_indicator_size = indicator.size + assert expected_zone_size == actual_indicator_size + + def test_urban_open_space(): indicator = urban_open_space(ZONES) expected_zone_size = ZONES.geometry.size