From f7e68ccf29142e6aebeddfab578f8f6e015e5f11 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Tue, 26 Mar 2024 10:49:27 -0700 Subject: [PATCH 1/8] allow user-defined edge attr aggregation when simplifying graph --- osmnx/simplification.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 4e71bbf06..de194ed05 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -274,13 +274,14 @@ def _remove_rings( return G -def simplify_graph( # noqa: PLR0912 +def simplify_graph( # noqa: C901, PLR0912 G: nx.MultiDiGraph, *, node_attrs_include: Iterable[str] | None = None, edge_attrs_differ: Iterable[str] | None = None, remove_rings: bool = True, track_merged: bool = False, + edge_attrs_agg: dict[str, Any] | None = None, ) -> nx.MultiDiGraph: """ Simplify a graph's topology by removing interstitial nodes. @@ -339,8 +340,9 @@ def simplify_graph( # noqa: PLR0912 msg = "Begin topologically simplifying the graph..." utils.log(msg, level=lg.INFO) - # define edge segment attributes to sum upon edge simplification - attrs_to_sum = {"length", "travel_time"} + # define edge segment attributes to aggregate upon edge simplification + if edge_attrs_agg is None: + edge_attrs_agg = {"length": sum, "travel_time": sum} # make a copy to not mutate original graph object caller passed in G = G.copy() @@ -386,9 +388,10 @@ def simplify_graph( # noqa: PLR0912 # consolidate the path's edge segments' attribute values for attr in path_attributes: - if attr in attrs_to_sum: - # if this attribute must be summed, sum it now - path_attributes[attr] = sum(path_attributes[attr]) + if attr in edge_attrs_agg: + # if this attribute's values must be aggregated, do so now + agg_func = edge_attrs_agg[attr] + path_attributes[attr] = agg_func(path_attributes[attr]) elif len(set(path_attributes[attr])) == 1: # if there's only 1 unique value in this attribute list, # consolidate it to the single value (the zero-th): From 7bb7c41f6a1e7b0921822013bec966c774d0479b Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Fri, 29 Mar 2024 23:58:45 -0400 Subject: [PATCH 2/8] update docstrings --- osmnx/simplification.py | 48 +++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index de194ed05..76a4288bf 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -60,13 +60,13 @@ def _is_endpoint( The ID of the node. node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if it has one - or more of the attributes in `node_attrs_include`. + determination. A node is always an endpoint if it possesses one or + more of the attributes in `node_attrs_include`. edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if its - incident edges have different values than each other for any of the - edge attributes in `edge_attrs_differ`. + determination. A node is always an endpoint if its incident edges have + different values than each other for any of the edge attributes in + `edge_attrs_differ`. Returns ------- @@ -209,13 +209,13 @@ def _get_paths_to_simplify( Input graph. node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if it has one - or more of the attributes in `node_attrs_include`. + determination. A node is always an endpoint if it possesses one or + more of the attributes in `node_attrs_include`. edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if its - incident edges have different values than each other for any of the - edge attributes in `edge_attrs_differ`. + determination. A node is always an endpoint if its incident edges have + different values than each other for any of the edge attributes in + `edge_attrs_differ`. Yields ------ @@ -253,13 +253,13 @@ def _remove_rings( Input graph. node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if it has one - or more of the attributes in `node_attrs_include`. + determination. A node is always an endpoint if it possesses one or + more of the attributes in `node_attrs_include`. edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if its - incident edges have different values than each other for any of the - edge attributes in `edge_attrs_differ`. + determination. A node is always an endpoint if its incident edges have + different values than each other for any of the edge attributes in + `edge_attrs_differ`. Returns ------- @@ -286,7 +286,7 @@ def simplify_graph( # noqa: C901, PLR0912 """ Simplify a graph's topology by removing interstitial nodes. - This simplifies graph topology by removing all nodes that are not + This simplifies the graph's topology by removing all nodes that are not intersections or dead-ends, by creating an edge directly between the end points that encapsulate them while retaining the full geometry of the original edges, saved as a new `geometry` attribute on the new edge. @@ -313,19 +313,25 @@ def simplify_graph( # noqa: C901, PLR0912 Input graph. node_attrs_include Node attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if it has one - or more of the attributes in `node_attrs_include`. + determination. A node is always an endpoint if it possesses one or + more of the attributes in `node_attrs_include`. edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint - determination. If not None, a node is always an endpoint if its - incident edges have different values than each other for any of the - edge attributes in `edge_attrs_differ`. + determination. A node is always an endpoint if its incident edges have + different values than each other for any of the edge attributes in + `edge_attrs_differ`. remove_rings If True, remove any graph components that consist only of a single chordless cycle (i.e., an isolated self-contained ring). track_merged If True, add `merged_edges` attribute on simplified edges, containing a list of all the `(u, v)` node pairs that were merged together. + edge_attrs_agg + Allows user to aggregate edge segment attributes when simplifying an + edge. Keys are edge attribute names and values are aggregation + functions to apply to these attributes when they exist for a set of + edges being simplified. If None, this will default to a value of: + `{"length": sum, "travel_time": sum}`. Returns ------- From 70b63c848ae5ce8950f1e1f48f66bc08bda965c0 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Sat, 30 Mar 2024 13:31:10 -0700 Subject: [PATCH 3/8] improve docstrings --- osmnx/_osm_xml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osmnx/_osm_xml.py b/osmnx/_osm_xml.py index e97e0a871..b8649d67f 100644 --- a/osmnx/_osm_xml.py +++ b/osmnx/_osm_xml.py @@ -168,7 +168,7 @@ def _save_graph_xml( `settings.data_folder/graph.osm`. way_tag_aggs Keys are OSM way tag keys and values are aggregation functions - (anything accepted as an argument by pandas.agg). Allows user to + (anything accepted as an argument by `pandas.agg`). Allows user to aggregate graph edge attribute values into single OSM way values. If None, or if some tag's key does not exist in the dict, the way attribute will be assigned the value of the first edge of the way. @@ -304,7 +304,7 @@ def _add_ways_xml( edges into ways. way_tag_aggs Keys are OSM way tag keys and values are aggregation functions - (anything accepted as an argument by pandas.agg). Allows user to + (anything accepted as an argument by `pandas.agg`). Allows user to aggregate graph edge attribute values into single OSM way values. If None, or if some tag's key does not exist in the dict, the way attribute will be assigned the value of the first edge of the way. From 21916bc0d45ed0ac5846e7b9649c6affde6a1d50 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Sat, 30 Mar 2024 13:31:28 -0700 Subject: [PATCH 4/8] add node_attrs_agg param when consolidating intersections --- osmnx/simplification.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 76a4288bf..2d17b12c5 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -399,11 +399,10 @@ def simplify_graph( # noqa: C901, PLR0912 agg_func = edge_attrs_agg[attr] path_attributes[attr] = agg_func(path_attributes[attr]) elif len(set(path_attributes[attr])) == 1: - # if there's only 1 unique value in this attribute list, - # consolidate it to the single value (the zero-th): + # if there's only 1 unique value, keep that single value path_attributes[attr] = path_attributes[attr][0] else: - # otherwise, if there are multiple values, keep one of each + # otherwise, if there are multiple uniques, keep one of each path_attributes[attr] = list(set(path_attributes[attr])) # construct the new consolidated edge's geometry for this path @@ -450,6 +449,7 @@ def consolidate_intersections( rebuild_graph: bool = True, dead_ends: bool = False, reconnect_edges: bool = True, + node_attrs_agg: dict[str, Any] | None = None, ) -> nx.MultiDiGraph | gpd.GeoSeries: """ Consolidate intersections comprising clusters of nearby nodes. @@ -500,6 +500,11 @@ def consolidate_intersections( False, the returned graph has no edges (which is faster if you just need topologically consolidated intersection counts). Ignored if `rebuild_graph` is not True. + node_attrs_agg + Allows user to aggregate node attributes values when consolidating a + nodes. Keys are node attribute names and values are aggregation + functions (anything accepted as an argument by `pandas.agg`). If None, + merge node attributes contain unique values across the merged nodes. Returns ------- @@ -524,7 +529,12 @@ def consolidate_intersections( return G # otherwise - return _consolidate_intersections_rebuild_graph(G, tolerance, reconnect_edges) + return _consolidate_intersections_rebuild_graph( + G, + tolerance, + reconnect_edges, + node_attrs_agg, + ) # otherwise, if we're not rebuilding the graph if len(G) == 0: @@ -564,6 +574,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 G: nx.MultiDiGraph, tolerance: float, reconnect_edges: bool, # noqa: FBT001 + node_attrs_agg: dict[str, Any] | None, ) -> nx.MultiDiGraph: """ Consolidate intersections comprising clusters of nearby nodes. @@ -593,6 +604,11 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 False, the returned graph has no edges (which is faster if you just need topologically consolidated intersection counts). Ignored if `rebuild_graph` is not True. + node_attrs_agg + Allows user to aggregate node attributes values when consolidating a + nodes. Keys are node attribute names and values are aggregation + functions (anything accepted as an argument by `pandas.agg`). If None, + merge node attributes contain unique values across the merged nodes. Returns ------- @@ -669,11 +685,14 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 for col in set(nodes_subset.columns).intersection(settings.useful_tags_node): # get the unique non-null values (we won't add null attrs) unique_vals = list(set(nodes_subset[col].dropna())) - if len(unique_vals) == 1: + if len(unique_vals) > 0 and node_attrs_agg is not None and col in node_attrs_agg: + # if this attribute's values must be aggregated, do so now + node_attrs[col] = nodes_subset[col].agg(node_attrs_agg[col]) + elif len(unique_vals) == 1: # if there's 1 unique value for this attribute, keep it node_attrs[col] = unique_vals[0] elif len(unique_vals) > 1: - # if there are multiple unique values, keep all uniques + # if there are multiple unique values, keep one of each node_attrs[col] = unique_vals Gc.add_node(cluster_label, **node_attrs) From 9c3a27befd7c854efda26268392433f7c881d7b3 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Sun, 31 Mar 2024 20:56:22 -0700 Subject: [PATCH 5/8] handle elevation during node consolidation --- osmnx/elevation.py | 5 +++-- osmnx/simplification.py | 22 +++++++++++++--------- tests/test_osmnx.py | 3 +++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/osmnx/elevation.py b/osmnx/elevation.py index 8f4322c07..b0dc6cb96 100644 --- a/osmnx/elevation.py +++ b/osmnx/elevation.py @@ -37,9 +37,10 @@ def add_edge_grades(G: nx.MultiDiGraph, *, add_absolute: bool = True) -> nx.Mult """ Calculate and add `grade` attributes to all graph edges. - Vectorized function to calculate the directed grade (ie, rise over run) + Vectorized function to calculate the directed grade (i.e., rise over run) for each edge in the graph and add it to the edge as an attribute. Nodes - must already have `elevation` attributes before using this function. + must already have `elevation` and `length` attributes before using this + function. See also the `add_node_elevations_raster` and `add_node_elevations_google` functions. diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 2d17b12c5..28d9b6105 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -14,7 +14,6 @@ from shapely.geometry import Polygon from . import convert -from . import settings from . import stats from . import utils from ._errors import GraphSimplificationError @@ -346,7 +345,7 @@ def simplify_graph( # noqa: C901, PLR0912 msg = "Begin topologically simplifying the graph..." utils.log(msg, level=lg.INFO) - # define edge segment attributes to aggregate upon edge simplification + # default edge segment attributes to aggregate upon simplification if edge_attrs_agg is None: edge_attrs_agg = {"length": sum, "travel_time": sum} @@ -616,6 +615,10 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 A rebuilt graph with consolidated intersections and reconnected edge geometries. """ + # default node attributes to aggregate upon consolidation + if node_attrs_agg is None: + node_attrs_agg = {"elevation": "mean"} + # STEP 1 # buffer nodes to passed-in distance and merge overlaps. turn merged nodes # into gdf and get centroids of each cluster as x, y @@ -628,9 +631,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 # attach each node to its cluster of merged nodes. first get the original # graph's node points then spatial join to give each node the label of # cluster it's within. make cluster labels type string. - node_points = convert.graph_to_gdfs(G, edges=False) - cols = set(node_points.columns).intersection(["geometry", *settings.useful_tags_node]) - node_points = node_points[list(cols)] + node_points = convert.graph_to_gdfs(G, edges=False).drop(columns=["x", "y"]) gdf = gpd.sjoin(node_points, node_clusters, how="left", predicate="within") gdf = gdf.drop(columns="geometry").rename(columns={"index_right": "cluster"}) gdf["cluster"] = gdf["cluster"].astype(str) @@ -640,8 +641,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 # move each component to its own cluster (otherwise you will connect # nodes together that are not truly connected, e.g., nearby deadends or # surface streets with bridge). - groups = gdf.groupby("cluster") - for cluster_label, nodes_subset in groups: + for cluster_label, nodes_subset in gdf.groupby("cluster"): if len(nodes_subset) > 1: # identify all the (weakly connected) component in cluster wccs = list(nx.weakly_connected_components(G.subgraph(nodes_subset.index))) @@ -682,12 +682,16 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 "x": nodes_subset["x"].iloc[0], "y": nodes_subset["y"].iloc[0], } - for col in set(nodes_subset.columns).intersection(settings.useful_tags_node): + for col in set(nodes_subset.columns): # get the unique non-null values (we won't add null attrs) unique_vals = list(set(nodes_subset[col].dropna())) - if len(unique_vals) > 0 and node_attrs_agg is not None and col in node_attrs_agg: + if len(unique_vals) > 0 and col in node_attrs_agg: # if this attribute's values must be aggregated, do so now node_attrs[col] = nodes_subset[col].agg(node_attrs_agg[col]) + elif col == "street_count": + # if user doesn't specifically handle street_count with an + # agg function, just skip it here then calculate it later + continue elif len(unique_vals) == 1: # if there's 1 unique value for this attribute, keep it node_attrs[col] = unique_vals[0] diff --git a/tests/test_osmnx.py b/tests/test_osmnx.py index 976405d3e..fe5328133 100644 --- a/tests/test_osmnx.py +++ b/tests/test_osmnx.py @@ -269,6 +269,9 @@ def test_elevation() -> None: G = ox.elevation.add_node_elevations_raster(G, rasters) assert pd.notna(pd.Series(dict(G.nodes(data="elevation")))).all() + # consolidate nodes with elevation (by default will aggregate via mean) + G = ox.simplification.consolidate_intersections(G) + # add edge grades and their absolute values G = ox.add_edge_grades(G, add_absolute=True) From 3fcafe2b3db2adbb7a042137ad4a622bbc7e623c Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Mon, 1 Apr 2024 15:28:55 -0700 Subject: [PATCH 6/8] clean up --- osmnx/simplification.py | 62 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 28d9b6105..6bb54322f 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -8,6 +8,7 @@ import geopandas as gpd import networkx as nx +import numpy as np from shapely.geometry import LineString from shapely.geometry import MultiPolygon from shapely.geometry import Point @@ -64,7 +65,7 @@ def _is_endpoint( edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint determination. A node is always an endpoint if its incident edges have - different values than each other for any of the edge attributes in + different values than each other for any attribute in `edge_attrs_differ`. Returns @@ -213,7 +214,7 @@ def _get_paths_to_simplify( edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint determination. A node is always an endpoint if its incident edges have - different values than each other for any of the edge attributes in + different values than each other for any attribute in `edge_attrs_differ`. Yields @@ -257,7 +258,7 @@ def _remove_rings( edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint determination. A node is always an endpoint if its incident edges have - different values than each other for any of the edge attributes in + different values than each other for any attribute in `edge_attrs_differ`. Returns @@ -280,7 +281,7 @@ def simplify_graph( # noqa: C901, PLR0912 edge_attrs_differ: Iterable[str] | None = None, remove_rings: bool = True, track_merged: bool = False, - edge_attrs_agg: dict[str, Any] | None = None, + edge_attr_aggs: dict[str, Any] | None = None, ) -> nx.MultiDiGraph: """ Simplify a graph's topology by removing interstitial nodes. @@ -317,7 +318,7 @@ def simplify_graph( # noqa: C901, PLR0912 edge_attrs_differ Edge attribute names for relaxing the strictness of endpoint determination. A node is always an endpoint if its incident edges have - different values than each other for any of the edge attributes in + different values than each other for any attribute in `edge_attrs_differ`. remove_rings If True, remove any graph components that consist only of a single @@ -325,12 +326,13 @@ def simplify_graph( # noqa: C901, PLR0912 track_merged If True, add `merged_edges` attribute on simplified edges, containing a list of all the `(u, v)` node pairs that were merged together. - edge_attrs_agg + edge_attr_aggs Allows user to aggregate edge segment attributes when simplifying an edge. Keys are edge attribute names and values are aggregation functions to apply to these attributes when they exist for a set of - edges being simplified. If None, this will default to a value of: - `{"length": sum, "travel_time": sum}`. + edges being merged. Edge attributes not in `edge_attr_aggs` will + contain the unique values across the merged edge segments. If None, + defaults to `{"length": sum, "travel_time": sum}`. Returns ------- @@ -346,8 +348,8 @@ def simplify_graph( # noqa: C901, PLR0912 utils.log(msg, level=lg.INFO) # default edge segment attributes to aggregate upon simplification - if edge_attrs_agg is None: - edge_attrs_agg = {"length": sum, "travel_time": sum} + if edge_attr_aggs is None: + edge_attr_aggs = {"length": sum, "travel_time": sum} # make a copy to not mutate original graph object caller passed in G = G.copy() @@ -393,9 +395,9 @@ def simplify_graph( # noqa: C901, PLR0912 # consolidate the path's edge segments' attribute values for attr in path_attributes: - if attr in edge_attrs_agg: + if attr in edge_attr_aggs: # if this attribute's values must be aggregated, do so now - agg_func = edge_attrs_agg[attr] + agg_func = edge_attr_aggs[attr] path_attributes[attr] = agg_func(path_attributes[attr]) elif len(set(path_attributes[attr])) == 1: # if there's only 1 unique value, keep that single value @@ -448,7 +450,7 @@ def consolidate_intersections( rebuild_graph: bool = True, dead_ends: bool = False, reconnect_edges: bool = True, - node_attrs_agg: dict[str, Any] | None = None, + node_attr_aggs: dict[str, Any] | None = None, ) -> nx.MultiDiGraph | gpd.GeoSeries: """ Consolidate intersections comprising clusters of nearby nodes. @@ -499,11 +501,12 @@ def consolidate_intersections( False, the returned graph has no edges (which is faster if you just need topologically consolidated intersection counts). Ignored if `rebuild_graph` is not True. - node_attrs_agg - Allows user to aggregate node attributes values when consolidating a - nodes. Keys are node attribute names and values are aggregation - functions (anything accepted as an argument by `pandas.agg`). If None, - merge node attributes contain unique values across the merged nodes. + node_attr_aggs + Allows user to aggregate node attributes values when merging nodes. + Keys are node attribute names and values are aggregation functions + (anything accepted as an argument by `pandas.agg`). Node attributes + not in `node_attr_aggs` will contain the unique values across the + merged nodes. If None, defaults to `{"elevation": numpy.mean}`. Returns ------- @@ -532,7 +535,7 @@ def consolidate_intersections( G, tolerance, reconnect_edges, - node_attrs_agg, + node_attr_aggs, ) # otherwise, if we're not rebuilding the graph @@ -573,7 +576,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 G: nx.MultiDiGraph, tolerance: float, reconnect_edges: bool, # noqa: FBT001 - node_attrs_agg: dict[str, Any] | None, + node_attr_aggs: dict[str, Any] | None, ) -> nx.MultiDiGraph: """ Consolidate intersections comprising clusters of nearby nodes. @@ -603,11 +606,12 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 False, the returned graph has no edges (which is faster if you just need topologically consolidated intersection counts). Ignored if `rebuild_graph` is not True. - node_attrs_agg - Allows user to aggregate node attributes values when consolidating a - nodes. Keys are node attribute names and values are aggregation - functions (anything accepted as an argument by `pandas.agg`). If None, - merge node attributes contain unique values across the merged nodes. + node_attr_aggs + Allows user to aggregate node attributes values when merging nodes. + Keys are node attribute names and values are aggregation functions + (anything accepted as an argument by `pandas.agg`). Node attributes + not in `node_attr_aggs` will contain the unique values across the + merged nodes. If None, defaults to `{"elevation": numpy.mean}`. Returns ------- @@ -616,8 +620,8 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 geometries. """ # default node attributes to aggregate upon consolidation - if node_attrs_agg is None: - node_attrs_agg = {"elevation": "mean"} + if node_attr_aggs is None: + node_attr_aggs = {"elevation": np.mean} # STEP 1 # buffer nodes to passed-in distance and merge overlaps. turn merged nodes @@ -685,9 +689,9 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 for col in set(nodes_subset.columns): # get the unique non-null values (we won't add null attrs) unique_vals = list(set(nodes_subset[col].dropna())) - if len(unique_vals) > 0 and col in node_attrs_agg: + if len(unique_vals) > 0 and col in node_attr_aggs: # if this attribute's values must be aggregated, do so now - node_attrs[col] = nodes_subset[col].agg(node_attrs_agg[col]) + node_attrs[col] = nodes_subset[col].agg(node_attr_aggs[col]) elif col == "street_count": # if user doesn't specifically handle street_count with an # agg function, just skip it here then calculate it later From d702c3ff5fa3edb15e1c2857abfdba1dd5916157 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Mon, 1 Apr 2024 16:35:41 -0700 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c012a2b..a40245d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Read the v2 [migration guide](https://github.com/gboeing/osmnx/issues/1123) - make consolidate_intersections function retain unique attribute values when consolidating nodes (#1144) - add OSM junction and railway tags to the default settings.useful_tags_node (#1144) - fix graph projection creating useless lat and lon node attributes (#1144) +- add edge_attr_aggs argument to simplify_graph function to specify aggregation behavior (#1155) +- add node_attr_aggs argument to the consolidate_intersections function to specify aggregation behavior (#1155) - make optional function parameters keyword-only throughout package (#1134) - make dist function parameters required rather than optional throughout package (#1134) - make which_result function parameter consistently able to accept a list throughout package (#1113) From ba66e0e9837212186adce11410fe23e710ae1ed8 Mon Sep 17 00:00:00 2001 From: Geoff Boeing Date: Mon, 1 Apr 2024 16:35:49 -0700 Subject: [PATCH 8/8] clean up --- osmnx/simplification.py | 13 ++++++------- osmnx/stats.py | 10 ++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osmnx/simplification.py b/osmnx/simplification.py index 6bb54322f..cd765790e 100644 --- a/osmnx/simplification.py +++ b/osmnx/simplification.py @@ -604,8 +604,7 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 If True, reconnect edges (and their geometries) to the consolidated nodes in rebuilt graph, and update the edge length attributes. If False, the returned graph has no edges (which is faster if you just - need topologically consolidated intersection counts). Ignored if - `rebuild_graph` is not True. + need topologically consolidated intersection counts). node_attr_aggs Allows user to aggregate node attributes values when merging nodes. Keys are node attribute names and values are aggregation functions @@ -704,11 +703,6 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 node_attrs[col] = unique_vals Gc.add_node(cluster_label, **node_attrs) - # calculate street_count attribute for all nodes lacking it - null_nodes = [n for n, sc in Gc.nodes(data="street_count") if sc is None] - street_count = stats.count_streets_per_node(Gc, nodes=null_nodes) - nx.set_node_attributes(Gc, street_count, name="street_count") - if len(G.edges) == 0 or not reconnect_edges: # if reconnect_edges is False or there are no edges in original graph # (after dead-end removed), then skip edges and return new graph as-is @@ -756,4 +750,9 @@ def _consolidate_intersections_rebuild_graph( # noqa: C901,PLR0912,PLR0915 # update the edge length attribute, given the new geometry Gc.edges[u, v, k]["length"] = new_geom.length + # calculate street_count attribute for all nodes lacking it + null_nodes = [n for n, sc in Gc.nodes(data="street_count") if sc is None] + street_counts = stats.count_streets_per_node(Gc, nodes=null_nodes) + nx.set_node_attributes(Gc, street_counts, name="street_count") + return Gc diff --git a/osmnx/stats.py b/osmnx/stats.py index 14115a46c..62cd42cab 100644 --- a/osmnx/stats.py +++ b/osmnx/stats.py @@ -13,9 +13,9 @@ from __future__ import annotations -import itertools import logging as lg from collections import Counter +from itertools import chain from typing import TYPE_CHECKING from typing import Any @@ -34,7 +34,9 @@ def streets_per_node(G: nx.MultiDiGraph) -> dict[int, int]: """ - Count streets (undirected edges) incident on each node. + Retrieve nodes' `street_count` attribute values. + + See also the `count_streets_per_node` function for the calculation. Parameters ---------- @@ -303,7 +305,7 @@ def count_streets_per_node( # appear twice in the undirected graph (u,v,0 and u,v,1 where u=v), but # one-way self-loops will appear only once Gu = G.to_undirected(reciprocal=False, as_view=True) - self_loop_edges = set(nx.selfloop_edges(Gu)) + self_loop_edges = set(nx.selfloop_edges(Gu, keys=False)) # get all non-self-loop undirected edges, including parallel edges non_self_loop_edges = [e for e in Gu.edges(keys=False) if e not in self_loop_edges] @@ -313,7 +315,7 @@ def count_streets_per_node( all_unique_edges = non_self_loop_edges + list(self_loop_edges) # flatten list of (u, v) edge tuples to count how often each node appears - edges_flat = itertools.chain.from_iterable(all_unique_edges) + edges_flat = chain.from_iterable(all_unique_edges) counts = Counter(edges_flat) streets_per_node = {node: counts[node] for node in nodes}