diff --git a/docs/source/usage_01_gap_identification.ipynb b/docs/source/usage_01_gap_identification.ipynb index 7108010..bb8abbe 100644 --- a/docs/source/usage_01_gap_identification.ipynb +++ b/docs/source/usage_01_gap_identification.ipynb @@ -28,7 +28,7 @@ "tags": [] }, "source": [ - "**Parameters covered**: `numgaps`, `settings.radius`, `mingap`, `maxgap`" + "**Parameters covered**: `num_gaps`, `settings.radius`, `min_gap_length`, `max_gap_length`" ] }, { @@ -90,7 +90,7 @@ "tags": [] }, "source": [ - "FixBikeNet is based on the idea that any shortest path between two nodes on an existing bicycle network, where the entire path goes through the non-bicycle network, is a potential gap. Out of these potentially millions of gaps in a city, FixBikeNet identifies those that are most important to close in terms of providing the most cycled meters (per gap meter). It ranks the identified gaps and selects the top `numgaps` - the first parameter for running FixBikeNet. The ranking happens through betweenness centrality, a proxy for cyclist flow. This measure assumes that cyclists move from each intersection to each other intersection in the city.\n", + "FixBikeNet is based on the idea that any shortest path between two nodes on an existing bicycle network, where the entire path goes through the non-bicycle network, is a potential gap. Out of these potentially millions of gaps in a city, FixBikeNet identifies those that are most important to close in terms of providing the most cycled meters (per gap meter). It ranks the identified gaps and selects the top `num_gaps` - the first parameter for running FixBikeNet. The ranking happens through betweenness centrality, a proxy for cyclist flow. This measure assumes that cyclists move from each intersection to each other intersection in the city.\n", "\n", "The assumption of such a global all-to-all flow is relaxed in FixBikeNet to only consider local betweenness flows within a `settings.radius` (that is loaded from `fbn.settings`). This relaxation is done for several reasons: \n", "1) Typical cyclists are usually not traversing the whole city\n", @@ -168,7 +168,7 @@ "source": [ "gaps_ordered = fbn.fixbikenet(\n", " \"Frederiksberg municipality\",\n", - " numgaps=10,\n", + " num_gaps=10,\n", ")" ] }, @@ -1203,7 +1203,7 @@ "tags": [] }, "source": [ - "The default minimum gap length is not zero, because often very short gaps are either data artefacts of undershoots (pieces did not get connected exactly), or due to intersections or parallel lanes. However, it straightforward to change the minimum gap length to, say 0, via `mingap=0`." + "The default minimum gap length is not zero, because often very short gaps are either data artefacts of undershoots (pieces did not get connected exactly), or due to intersections or parallel lanes. However, it straightforward to change the minimum gap length to, say 0, via `min_gap_length=0`." ] }, { @@ -1265,10 +1265,10 @@ } ], "source": [ - "gaps_ordered_mingap0 = fbn.fixbikenet(\n", + "gaps_ordered_min_gap_length0 = fbn.fixbikenet(\n", " \"Frederiksberg municipality\",\n", - " numgaps=10,\n", - " mingap=0,\n", + " num_gaps=10,\n", + " min_gap_length=0,\n", ")" ] }, @@ -1284,7 +1284,7 @@ "tags": [] }, "source": [ - "Let us add the outcome with `mingap=0` in orange to see the difference:" + "Let us add the outcome with `min_gap_length=0` in orange to see the difference:" ] }, { @@ -1787,7 +1787,7 @@ " },\n", " overlays : {\n", " "Existing bike network" : geo_json_d7a520d45602743edf243e6abafe845e,\n", - " "Identified gaps, mingap=0" : geo_json_647b515abac5bc1b389f265c33e24cba,\n", + " "Identified gaps, min_gap_length=0" : geo_json_647b515abac5bc1b389f265c33e24cba,\n", " "Identified gaps" : geo_json_932fb79874d1b4d847dcb9efc3fb09ab,\n", " },\n", " };\n", @@ -1823,10 +1823,10 @@ " style_kwds={\"weight\": 2, \"color\": \"#9999cc\"},\n", " name=\"Existing bike network\",\n", ")\n", - "viz = gaps_ordered_mingap0.explore(\n", + "viz = gaps_ordered_min_gap_length0.explore(\n", " m=viz, \n", " style_kwds={\"weight\": 6, \"color\": \"#f19730\"},\n", - " name=\"Identified gaps, mingap=0\",\n", + " name=\"Identified gaps, min_gap_length=0\",\n", ")\n", "viz = gaps_ordered.explore(\n", " m=viz, \n", @@ -1850,7 +1850,7 @@ "tags": [] }, "source": [ - "It is also possible to consider longer gaps, for example, up to 1500m via `maxgap=1500`. This is computationally expensive and takes much longer to calculate. In the case of Frederiksberg, around 3 times as many potential gaps have to be considered, increasing the runtime by a factor of around 9. The result is however identical, which is why we are not running it here. The default maximum gap is already quite high, catching most long gaps. It can be useful to lower the `maxgap` value though, to making search faster and limiting it to only shorter gaps." + "It is also possible to consider longer gaps, for example, up to 1500m via `max_gap_length=1500`. This is computationally expensive and takes much longer to calculate. In the case of Frederiksberg, around 3 times as many potential gaps have to be considered, increasing the runtime by a factor of around 9. The result is however identical, which is why we are not running it here. The default maximum gap is already quite high, catching most long gaps. It can be useful to lower the `max_gap_length` value though, to making search faster and limiting it to only shorter gaps." ] } ], diff --git a/docs/source/usage_04_data_imports_for_prioritizing_gaps.ipynb b/docs/source/usage_04_data_imports_for_prioritizing_gaps.ipynb index 453d35f..4a7be5e 100644 --- a/docs/source/usage_04_data_imports_for_prioritizing_gaps.ipynb +++ b/docs/source/usage_04_data_imports_for_prioritizing_gaps.ipynb @@ -561,7 +561,7 @@ "source": [ "gaps_ordered = fbn.fixbikenet(\n", " \"Copenhagen municipality\",\n", - " numgaps = 20,\n", + " num_gaps = 20,\n", " import_files = {\n", " 'city_boundary': 'copenhagen_boundary.geojson',\n", " },\n", @@ -623,7 +623,7 @@ "source": [ "gaps_ordered_with_survey = fbn.fixbikenet(\n", " \"Copenhagen municipality\",\n", - " numgaps = 20,\n", + " num_gaps = 20,\n", " import_files = {\n", " 'city_boundary': 'copenhagen_boundary.geojson',\n", " 'point_data': 'copenhagen_citizen_input.geojson',\n", diff --git a/examples/development.py b/examples/development.py index 6187d23..c56ea36 100644 --- a/examples/development.py +++ b/examples/development.py @@ -25,9 +25,9 @@ fbn.fixbikenet( city_query="Frederiksberg Municipality", - mingap = 0, - maxgap = 100, - numgaps = 20, + min_gap_length = 0, + max_gap_length = 100, + num_gaps = 20, import_files={ 'city_boundary': "./tests/test_data/frederiksberg_boundary.geojson", # 'street_network': "./tests/test_data/frederiksberg_streetbike_network.gpkg", diff --git a/fixbikenet/constants.py b/fixbikenet/constants.py index 019190d..59318d5 100644 --- a/fixbikenet/constants.py +++ b/fixbikenet/constants.py @@ -7,7 +7,8 @@ Number of random nodes to select for local betweenness calculations, for computational reasons. _CLUSTER_GAPS_PER_FINAL_GAP : int, default 15 - Factor to multiply `numgaps` with, giving the number of gaps to consider for declustering. + Factor to multiply `num_gaps` with, giving the number of gaps to consider + for declustering. _CRS_CALCULATIONS : str, default 'auto' EPSG code of the coordinate reference system that is used to project OSM data for calculations. This has to be a distance-preserving projected CRS, @@ -28,6 +29,6 @@ _BETWEENNESS_RANDOM_NODES = 300 _CLUSTER_GAPS_PER_FINAL_GAP = 15 _CRS_CALCULATIONS = 'auto' -_PROGRESS_BAR_DESC_LENGTH = 24 +_PROGRESS_BAR_DESC_LENGTH = 23 _PROGRESS_BAR_LENGTH = 23 _ROUTING_PENALTY = {0: 1.5, 1: 1} \ No newline at end of file diff --git a/fixbikenet/fixbikenet.py b/fixbikenet/fixbikenet.py index 489fd3d..86f8c9e 100644 --- a/fixbikenet/fixbikenet.py +++ b/fixbikenet/fixbikenet.py @@ -47,28 +47,28 @@ def fixbikenet( city_query, - mingap = 20, - maxgap = 800, - numgaps = 50, + min_gap_length = 20, + max_gap_length = 800, + num_gaps = 50, export_data = True, city_id = None, export_plot=False, import_files={}, ): - """Find gaps in bicycle networks and return the `numgaps` that are the most + """Find gaps in bicycle networks and return the `num_gaps` that are the most important to fill. Parameters ---------- city_query : str Name of the city that the analysis should be performed on. - mingap : int, default 20 + min_gap_length : int, default 20 Minimum distance between node pairs to be considered as a potential gap, in meters. - maxgap : int, default 800 + max_gap_length : int, default 800 Maximum distance between node pairs to be considered as a potential gap, in meters. - numgaps : int, default 50 + num_gaps : int, default 50 Number of gaps to find. export_data : bool, default True If set to True, data will be saved to a file. The filename is @@ -111,7 +111,7 @@ def fixbikenet( Returns ------- gaps_ordered : geopandas.geodataframe.GeoDataFrame - Ordered geodataframe with the `numgaps` most important gaps to fill. + Ordered geodataframe with the `num_gaps` most important gaps to fill. References ---------- @@ -122,7 +122,7 @@ def fixbikenet( starttime = time.time() np.random.seed(settings.random_seed) # Set random number generator seed for reproducibility setting_was_auto = _validate_settings() - import_files = _validate_parameters(city_query, mingap, maxgap, export_data, import_files) + import_files = _validate_parameters(city_query, min_gap_length, max_gap_length, export_data, import_files) _print_header(city_query) ### Import data files @@ -188,10 +188,10 @@ def fixbikenet( contact_nodes = find_contact_nodes(G) # finding potential gaps in network - potential_gaps = find_potential_gaps(contact_nodes, nodes_gdf, maxgap) + potential_gaps = find_potential_gaps(contact_nodes, nodes_gdf, max_gap_length) # add routing for gaps in network - found_gaps, found_gaps_nsp = find_actual_gaps(G, potential_gaps, mingap) + found_gaps, found_gaps_nsp = find_actual_gaps(G, potential_gaps, min_gap_length) # calculating local betweenness score dependent on radius ebc = compute_local_betweenness_centrality(G, nodes_gdf) @@ -226,17 +226,17 @@ def fixbikenet( ) df = df.sort_values(by="benefit", ascending=False).reset_index(drop=True) - # Only keep the numgaps*constants._CLUSTER_GAPS_PER_FINAL_GAP most + # Only keep the num_gaps*constants._CLUSTER_GAPS_PER_FINAL_GAP most # important gaps before declustering. If there are fewer than - # numgaps*constants._CLUSTER_GAPS_PER_FINAL_GAP gaps keep only those - if df.shape[0] > numgaps*constants._CLUSTER_GAPS_PER_FINAL_GAP: - df = df.iloc[:numgaps*constants._CLUSTER_GAPS_PER_FINAL_GAP] + # num_gaps*constants._CLUSTER_GAPS_PER_FINAL_GAP gaps keep only those + if df.shape[0] > num_gaps*constants._CLUSTER_GAPS_PER_FINAL_GAP: + df = df.iloc[:num_gaps*constants._CLUSTER_GAPS_PER_FINAL_GAP] #decluster edges - gap_df = gap_declustering(df, G, ebc, contact_nodes, mingap) + gap_df = gap_declustering(df, G, ebc, contact_nodes, min_gap_length) progress_bar = initialize_progress_bar("Postprocess data", 5) - gap_df = gap_df.nlargest(numgaps, "benefit").reset_index(drop=True) + gap_df = gap_df.nlargest(num_gaps, "benefit").reset_index(drop=True) progress_bar.update(1) # compute list of all edges that are part of each gap, where each edge is u,v diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index 15061a7..039d2d3 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -25,8 +25,8 @@ def _validate_parameters( city_query, - mingap, - maxgap, + min_gap_length, + max_gap_length, export_data, import_files, ): @@ -48,10 +48,10 @@ def _validate_parameters( """ if type(city_query) != str: raise TypeError("city_query must be a string") - if type(mingap) != int: - raise TypeError("mingap must be an integer") - if type(maxgap) != int: - raise TypeError("maxgap must be an integer") + if type(min_gap_length) != int: + raise TypeError("min_gap_length must be an integer") + if type(max_gap_length) != int: + raise TypeError("max_gap_length must be an integer") if type(export_data) is not bool: raise TypeError("export_data must be a boolean") @@ -400,9 +400,9 @@ def find_contact_nodes(G): contact_nodes.append(node) return contact_nodes -def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): +def find_potential_gaps(contact_nodes, nodes_gdf, max_gap_length): """Find potential gaps in protected bicycle network, corresponding to two - contact nodes that are within maxgap euclidean distance of each other. + contact nodes that are within max_gap_length euclidean distance of each other. Parameters ---------- @@ -410,7 +410,7 @@ def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): list of all nodes that fulfill criteria to be a contact node. nodes_gdf : geopandas.GeoDataFrame All nodes in street network. - maxgap : int + max_gap_length : int User defined maximal euclidean distance between two contact nodes. Returns @@ -423,7 +423,7 @@ def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): contact_nodes_gdf = nodes_gdf[nodes_gdf['osmid'].isin(contact_nodes)] for node in contact_nodes: - node_buffer = contact_nodes_gdf.loc[node, "geometry"].buffer(maxgap) + node_buffer = contact_nodes_gdf.loc[node, "geometry"].buffer(max_gap_length) q = contact_nodes_gdf.sindex.query(node_buffer, predicate="intersects") neighbours = list(contact_nodes_gdf.iloc[q].index) neighbours.remove(node) @@ -435,7 +435,7 @@ def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): potential_gaps = list(set(potential_gaps)) return potential_gaps -def find_actual_gaps(G, potential_gaps, mingap): +def find_actual_gaps(G, potential_gaps, min_gap_length): """Determine which potential gaps are actual gaps by finding paths between all contact nodes and only keeping the gaps that have no protected bike infrastructure. @@ -447,7 +447,7 @@ def find_actual_gaps(G, potential_gaps, mingap): edges. potential_gaps : list All unique potential gaps in protected bicycle network. - mingap : int + min_gap_length : int Minimum distance between node pairs to be considered as a potential gap, in meters. @@ -492,7 +492,7 @@ def find_actual_gaps(G, potential_gaps, mingap): break gaplength = nx.shortest_path_length(G, u, v, weight="length") - if valid and gaplength >= mingap: + if valid and gaplength >= min_gap_length: found_gaps.append((u, v)) found_gaps_nsp.append(nodelist) @@ -868,7 +868,7 @@ def compute_benefit_metric(comp, node_path, ebc): B = sum(lengths * ebcs) / sum(lengths) return B -def gap_declustering(gaps_df, G, ebc, contact_nodes, mingap): +def gap_declustering(gaps_df, G, ebc, contact_nodes, min_gap_length): """ Parameters @@ -882,7 +882,7 @@ def gap_declustering(gaps_df, G, ebc, contact_nodes, mingap): local betweenness centrality values for all edges in network. contact_nodes : list List of contact nodes. - mingap : int + min_gap_length : int Minimum distance between node pairs to be considered as a potential gap, in meters. @@ -949,7 +949,7 @@ def gap_declustering(gaps_df, G, ebc, contact_nodes, mingap): target=target, weight="length" ) - if node_path and gaplength >= mingap: + if node_path and gaplength >= min_gap_length: candidate_paths.append(node_path) except nx.NetworkXNoPath: continue diff --git a/tests/test_fixbikenet.py b/tests/test_fixbikenet.py index dc5e9e3..286587d 100644 --- a/tests/test_fixbikenet.py +++ b/tests/test_fixbikenet.py @@ -20,9 +20,9 @@ def test_fixbikenet_case_success_offline1(validation_gdf_frederiksberg): fbn.settings.radius = 1000 gaps_ordered = fbn.fixbikenet( city_query="Frederiksberg", - mingap = 0, - maxgap = 100, - numgaps = 20, + min_gap_length = 0, + max_gap_length = 100, + num_gaps = 20, import_files={ 'city_boundary': "./tests/test_data/frederiksberg_boundary.geojson", 'street_network': "./tests/test_data/frederiksberg_streetbike_network.gpkg", diff --git a/tests/test_functions.py b/tests/test_functions.py index 8782ad1..8100a49 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -89,9 +89,9 @@ def create_test_contact_nodes(): return contact_nodes @pytest.fixture -def create_maxgap(): - maxgap = 50 - return maxgap +def create_max_gap_length(): + max_gap_length = 50 + return max_gap_length @pytest.fixture def create_test_nodes_gdf(): @@ -105,8 +105,8 @@ def create_validation_gaps(): gaps = [(1,3)] return gaps -def test_find_potential_gaps(create_test_contact_nodes, create_maxgap, create_test_nodes_gdf, create_validation_gaps): - assert find_potential_gaps(create_test_contact_nodes,create_test_nodes_gdf, create_maxgap) == create_validation_gaps +def test_find_potential_gaps(create_test_contact_nodes, create_max_gap_length, create_test_nodes_gdf, create_validation_gaps): + assert find_potential_gaps(create_test_contact_nodes,create_test_nodes_gdf, create_max_gap_length) == create_validation_gaps @pytest.fixture def create_graph_for_routing():