The CIESIN GRID3 data provides high-resolution geospatial data that can be used to analyze various aspects of infrastructure and services in Nigeria. One practical application of this data is to find optimal routes between schools and health facilities. Below is an example of how to achieve this using Python and relevant libraries.
# importimport geopandas as gpdimport networkx as nximport momepyimport osmnx as oximport matplotlib.pyplot as pltfrom itertools import productimport numpy as npimport mapclassifyimport foliumimport jsonimport pandas as pdimport momepy import requests import randomimport matplotlib.colors as mcolors
# install arcgis API for accessing data# check that arcgis is installed # !pip install arcgis
Requirement already satisfied: arcgis in c:\users\jfm2205\appdata\local\esri\conda\envs\jfm-py2\lib\site-packages (2.4.1.1)
# import arcgisfrom arcgis.gis import GIS
# Anonymously authenticate arcgis APIgis = GIS()
def fetch_nga_roads(bbox, crs, record_limit, url):""" Fetch NGA roads from an ArcGIS FeatureServer within a bounding box. Parameters ---------- bbox : dict Bounding box with keys xmin, ymin, xmax, ymax (in EPSG:3857). crs : str, optional CRS for the output GeoDataFrame (default: EPSG:3857). record_limit : int, optional Maximum number of records per request (default: 200000). url : str, optional ArcGIS FeatureServer query URL. Returns ------- geopandas.GeoDataFrame GeoDataFrame containing all fetched road features. """ params = {"f": "geojson","where": "1=1","geometry": json.dumps({**bbox,"spatialReference": {"wkid": int(crs.split(":")[1])}, }),"geometryType": "esriGeometryEnvelope","spatialRel": "esriSpatialRelContains","outFields": "*","returnGeometry": "true","resultRecordCount": record_limit, } all_features = [] offset =0whileTrue: paged_params = params | {"resultOffset": offset} r = requests.get(url, params=paged_params) r.raise_for_status() data = r.json() features = data.get("features", [])ifnot features:break all_features.extend(features) offset +=len(features) valid_features = [ f for f in all_featuresif f.get("geometry") isnotNone]return gpd.GeoDataFrame.from_features(valid_features, crs=crs)
# Read in service for NGA roadsurl ="https://services3.arcgis.com/BU6Aadhn6tbBEdyk/arcgis/rest/services/GRID3_NGA_roads/FeatureServer/0/query"boundbox = {"xmin": 360102.4944,"ymin": 725614.4996,"xmax": 381199.1042,"ymax": 746837.3928 }gdf = fetch_nga_roads(boundbox, crs="EPSG:3857", record_limit=1000, url=url)gdf.head()
# Calculate time to traverse a road segment in minutes using the speed estimate and lengthedges["travel_time_mins"] = ((edges["length_meters"]/1000) / (edges["speed_estimate"])) *60# drop if travel_time_mins is infinite or NaN or less than equal to zeroedges = edges.replace([np.inf, -np.inf], np.nan)edges = edges.dropna(subset=['travel_time_mins'])edges = edges[edges['travel_time_mins'] >0] edges.head()
# Shortest path based on distancefig, ax = ox.plot_graph_route(Gs, path, figsize=(5,5), dpi=120 )# Add the travel time as titleax.set_xlabel("Shortest path is {t: .1f} minutes.".format(t=length))#show plotplt.show()
# Explore graph edges and route together in a single maproute_edges = ox.routing.route_to_gdf(Gs, path, weight='length')m = edges.explore(color="black", tiles="cartodbdarkmatter")m = route_edges.explore(m=m, color="maroon", style_kwds={"weight": 5})# Add the Esri World Imagery tile layerfolium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Esri Satellite', overlay=False, control=True).add_to(m)# Add a layer control to toggle between tile layersfolium.LayerControl().add_to(m)m
#set same crs as graphschools = schools.to_crs(Gs.graph["crs"])health_fac = health_fac.to_crs(Gs.graph["crs"])schools["longitude"] = schools.geometry.xschools["latitude"] = schools.geometry.yhealth_fac["longitude"] = health_fac.geometry.xhealth_fac["latitude"] = health_fac.geometry.y# Caclulate shortest route between every school and health facility using the travel_time_mins variable as the weight# this can take a few minutesweight_attribute ='travel_time_mins'# Find nearest nodes to each school and health facilityorig_node_id, dist_to_orig = ox.distance.nearest_nodes( Gs, X=schools.geometry.x, Y=schools.geometry.y, return_dist=True)dest_node_id, dist_to_dest = ox.distance.nearest_nodes(Gs, X=health_fac.geometry.x, Y=health_fac.geometry.y, return_dist=True)shortest_paths = {}for source in orig_node_id:for target in dest_node_id:try:# Get the actual path path = nx.shortest_path(Gs, source=source, target=target, weight=weight_attribute)# Get the path length length = nx.shortest_path_length(Gs, source=source, target=target, weight=weight_attribute) shortest_paths[(source, target)] = {'path': path, 'length': length}except nx.NetworkXNoPath:# build dictionary with shortest routes shortest_paths[(source, target)] = {'path': None, 'length': float('inf')}print("Unique school nodes:", len(set(orig_node_id)))print("Unique facility nodes:", len(set(dest_node_id)))print("Snap distances (schools):", dist_to_orig.min(), dist_to_orig.max())print("Snap distances (facilities):", dist_to_dest.min(), dist_to_dest.max())
# create dataframe of shortest paths between every school and health facility and drop school/health facility combinations that aren't the closest# build dataframedf = pd.DataFrame(shortest_paths).Tdf = df.reset_index()df = df.rename(columns={'level_0': 'school_node', 'level_1': 'health_facility_node'})df = df.replace([np.inf, -np.inf], np.nan)# sort by length and keep only the shortest routes for each schooldf = df.sort_values(by=['school_node', 'length'], ascending=[True, True])df = df.drop_duplicates(subset=['school_node'], keep='first')df.head(5)
school_node
health_facility_node
path
length
33384
83
30027
[83, 84, 124, 160, 13322, 47675, 42536, 13314,...
18.0
624
116
116
[116]
0.0
1057
221
15747
[221, 222, 3836, 3837, 15788, 15747]
5.0
17962
643
183
[643, 17917, 5788, 661, 5793, 13140, 183]
6.0
28450
712
712
[712]
0.0
# merge nearest node ID back to schools and health facitility geodataframes# set node idsschools['node_id'] = orig_node_idhealth_fac['node_id'] = dest_node_id# get only columns needed for mergeschool_merge = schools[['node_id', 'name','latitude','longitude']]health_merge = health_fac[['node_id', 'facility_name']]# merge nearest routes with school and health facility informationnearest_df = df.merge(school_merge, left_on='school_node', right_on='node_id')nearest_df = nearest_df.merge(health_merge, left_on='health_facility_node', right_on='node_id')# drop duplicatesnearest_df1 = nearest_df.drop_duplicates(subset=['school_node', 'length'])nearest_df1.head()
# Explore a route interactively# select school to viewnearest_df1_s = nearest_df1[nearest_df1['name'] =='Saint Joseph Secondary School']route_edges = ox.routing.route_to_gdf(Gs, nearest_df1_s.iloc[0].path, weight='length')# map facilities, roads, and routesm = edges.explore(color="black", tiles="cartodbdarkmatter")m = route_edges.explore(m=m, color="yellow", style_kwds={"weight": 5})m = schools.explore(m=m, color ="blue", style_kwds={"weight": 5})m = health_fac.explore(m=m, color ="red", style_kwds={"weight": 5})# add the Esri World Imagery tile layer to interactive mapfolium.TileLayer( tiles='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Esri Satellite', overlay=False, control=True).add_to(m)# add a layer control to toggle between tile layersfolium.LayerControl().add_to(m)m
# Map the travel time from each school to the nearset health facility - darker dots are closer to a health facility, red dots are health facilities# construct geodataframegeometry = gpd.points_from_xy(nearest_df1['longitude'], nearest_df1['latitude'])nearest_gdf = gpd.GeoDataFrame(nearest_df1, geometry=geometry, crs='EPSG:4326')edges_plot = edges.to_crs(epsg=4326)health_plot = health_fac.to_crs(epsg=4326)# Plot with length column for travel timefig, ax = plt.subplots(1, 1, figsize=(5, 5), dpi=120)edges.plot(ax=ax, color='grey', alpha=.1, zorder=1)health_fac.plot(marker='+', color='red', legend=True, ax=ax, markersize=5, zorder=3)nearest_gdf.plot(column='length', cmap='inferno', legend=True, ax=ax, markersize=25, zorder=2)plt.title("Time from school to nearest health facility")ax.set_xlim(bbox_3857[0], bbox_3857[2])ax.set_ylim(bbox_3857[1], bbox_3857[3] )plt.show()