Steel8
Steel8

Reputation: 155

How do I Plot multiple Isochrones Polygons using Python OSMnx library?

I'm trying to build isochrones for some schools, to understand the accessibility. My mode of travelling is "walk" and I wanted to create a list of at least 2 travel times, 30 minutes and 1 hour.

Here below is my code:

# import required libraries
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import networkx as nx
import osmnx as ox
from descartes import PolygonPatch
from shapely.geometry import Point, LineString, Polygon
from IPython.display import IFrame
import folium
import itertools # will be used to perform feature joining

# %matplotlib inline
ox.__version__

# function to get isochrones
def get_isochrone(lon, lat, walk_time=[30,60], speed=4.5):
    loc = (lat, lon)
    G = ox.graph_from_point(loc, simplify=True, network_type='walk')
    # Create nodes geodataframe from Graph network (G)
    gdf_nodes = ox.graph_to_gdfs(G, edges=False)
    x, y = gdf_nodes['geometry'].unary_union.centroid.xy
    center_node = ox.get_nearest_node(G, (y[0], x[0]))
    meters_per_minute = speed * 1000 / 60 #km per hour to m per minute
    for u, v, k, data in G.edges(data=True, keys=True):
        data['time'] = data['length'] / meters_per_minute
    # get one color for each isochrone
    iso_colors = ox.plot.get_colors(n=len(walk_time), cmap="plasma", start=0, return_hex=True)
    # color the nodes according to isochrone then plot the street network
    node_colors = {}
    for walks_time, color in zip(sorted(walk_time, reverse=True), iso_colors):
        subgraph = nx.ego_graph(G, center_node, radius=walks_time, distance="time")
        for node in subgraph.nodes():
            node_colors[node] = color
        
    nc = [node_colors[node] if node in node_colors else "none" for node in G.nodes()]
    ns = [15 if node in node_colors else 0 for node in G.nodes()] 
    # make isochrone polygons
    isochrone_polys = []
    for trip_times in sorted(walk_time, reverse=True):
        subgraph = nx.ego_graph(G, center_node, radius=trip_times, distance="time")
        node_points = [Point(data['x'], data['y']) for node, data in subgraph.nodes(data=True)]
        polys = gpd.GeoSeries(node_points).unary_union.convex_hull
        isochrone_polys.append(polys)
    # adding color
    for polygon, fc in zip(isochrone_polys, iso_colors):
        patch = PolygonPatch(polygon, fc=fc, ec="none", alpha=0.6, zorder=-1)
        # isochrone_polys.add_patch(patch) 
    return isochrone_polys

When I call the get_isochrone function:

# calling the function with the coordinates
map = coordinates.apply(lambda x: get_isochrone(x.longitude, x.latitude), axis=1)

What is returned is a list of polygons with each school point having polygons equal to the number of items in the travel times list. I however noticed that the polygons returned for each point are exactly the same.

Just to verify: This script shows that they are the same:

for i in map:
    print(i[0])
    print(i[1])
    print('\n')

Here are the results: Image showing what the above script returns

As you can see, each school point returns two polygons with exactly the same coordinates, for different travel times. I have reviewed the python function several times, even changed the travel time to one item in the travel time list like 100, and I still get exactly the same coordinates. I may have missed something and I'll appreciate any help. Thanks!

In addition, I also realized that the add_patch() method in the function adds color to the plot_graph() only. How can I add the colors to the polygons in the Folium map?

Upvotes: 1

Views: 2580

Answers (2)

im trying to replicate Steel8's answer, but when I run that code I get errors. I think these errors are due to the fact that I am using a different version of osmnx. Could you tell me which version of each package are you using?

import osmnx as ox
import pandas as pd
import warnings
import networkx as nx
import geopandas as gpd
from shapely.geometry import Point

warnings.simplefilter(action="ignore", category=FutureWarning)
warnings.simplefilter(action="ignore", category=PendingDeprecationWarning)
ox.config(use_cache=True, log_console=False)
# get some cities
cities = ["Hereford"]  # ["Hereford", "Worcester", "Gloucester"]
cities = ox.geocode_to_gdf([{"city": c, "country": "UK"} for c in cities])

# get some schools
tags = {"amenity": "school"}
schools = pd.concat(
    [
        ox.geometries.geometries_from_polygon(r["geometry"], tags)
        for i, r in cities.iterrows()
    ]
)
schools = (
    schools.loc["way"].dropna(axis=1, thresh=len(schools) / 4).drop(columns=["nodes"])
)
# change polygon to point
schools["geometry"] = schools.to_crs(schools.estimate_utm_crs())[
    "geometry"
].centroid.to_crs(schools.crs)

# function to get isochrones
def get_isochrone(
    lon, lat, walk_times=[15, 30], speed=4.5, name=None, point_index=None
):
    loc = (lat, lon)
    G = ox.graph_from_point(loc, simplify=True, network_type="walk")
    gdf_nodes = ox.graph_to_gdfs(G, edges=False)
    center_node = ox.distance.nearest_nodes(G, lon, lat)

    meters_per_minute = speed * 1000 / 60  # km per hour to m per minute
    for u, v, k, data in G.edges(data=True, keys=True):
        data["time"] = data["length"] / meters_per_minute
    polys = []
    for walk_time in walk_times:
        subgraph = nx.ego_graph(G, center_node, radius=walk_time, distance="time")
        node_points = [
            Point(data["x"], data["y"]) for node, data in subgraph.nodes(data=True)
        ]
        polys.append(gpd.GeoSeries(node_points).unary_union.convex_hull)
    info = {}
    if name:
        info["name"] = [name for t in walk_times]
    if point_index:
        info["point_index"] = [point_index for t in walk_times]
    return {**{"geometry": polys, "time": walk_times}, **info}

WT = [5, 10, 15]
SCHOOLS = 5

# build geopandas data frame of isochrone polygons for each school
isochrones = pd.concat(
    [
        gpd.GeoDataFrame(
            get_isochrone(
                r["geometry"].x,
                r["geometry"].y,
                name=r["name"],
                point_index=i,
                walk_times=WT,
            ),
            crs=schools.crs,
        )
        for i, r in schools.head(SCHOOLS).iterrows()
    ]
)

warnings.filterwarnings("ignore")

gdf = isochrones.set_index(["time", "point_index"]).copy()
# remove shorter walk time from longer walk time polygon to make folium work better
for idx in range(len(WT)-1,0,-1):
    gdf.loc[WT[idx], "geometry"] = (
        gdf.loc[WT[idx]]
        .apply(
            lambda r: r["geometry"].symmetric_difference(
                gdf.loc[(WT[idx-1], r.name), "geometry"]
            ),
            axis=1,
        )
        .values
    )

m = gdf.reset_index().explore(column="time", height=300, width=500, scheme="boxplot")
schools.head(SCHOOLS).explore(m=m, marker_kwds={"radius": 3, "color": "red"})

I'm gettin the following error message:

PS C:\Users\IGPOZO\Desktop\INAKI\Isochrones\Python> python test4.py
Traceback (most recent call last):
  File "C:\Users\IGPOZO\Anaconda3\envs\isochrones_env\lib\site-packages\pandas\core\indexes\base.py", line 3621, in get_loc
    return self._engine.get_loc(casted_key)
  File "pandas\_libs\index.pyx", line 136, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\index.pyx", line 144, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\index_class_helper.pxi", line 41, in pandas._libs.index.Int64Engine._check_type
KeyError: 'way'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\IGPOZO\Desktop\INAKI\Isochrones\Python\test4.py", line 24, in <module>
    schools.loc["way"].dropna(axis=1, thresh=len(schools) / 4).drop(columns=["nodes"])
  File "C:\Users\IGPOZO\Anaconda3\envs\isochrones_env\lib\site-packages\pandas\core\indexing.py", line 967, in __getitem__
    return self._getitem_axis(maybe_callable, axis=axis)
  File "C:\Users\IGPOZO\Anaconda3\envs\isochrones_env\lib\site-packages\pandas\core\indexing.py", line 1202, in _getitem_axis
    return self._get_label(key, axis=axis)
  File "C:\Users\IGPOZO\Anaconda3\envs\isochrones_env\lib\site-packages\pandas\core\indexing.py", line 1153, in _get_label
    return self.obj.xs(label, axis=axis)
  File "C:\Users\IGPOZO\Anaconda3\envs\isochrones_env\lib\site-packages\pandas\core\generic.py", line 3876, in xs
    loc = index.get_loc(key)
  File "C:\Users\IGPOZO\Anaconda3\envs\isochrones_env\lib\site-packages\pandas\core\indexes\base.py", line 3623, in get_loc
    raise KeyError(key) from err
KeyError: 'way'

Upvotes: 0

Rob Raymond
Rob Raymond

Reputation: 31166

  • it appears that you have used this example: https://github.com/gboeing/osmnx-examples/blob/main/notebooks/13-isolines-isochrones.ipynb
  • if all you want is polygons, then there is no need to do PolygonPatch
  • you have refactored incorrectly, for multiple walk times. You only need to generate edges once
  • pulling it all together:
    1. source some schools
    2. get_isochrone() refactored to your use case. Have changed do it returns a dict that has index and name of point / school being investigated.
    3. generate a geopandas data frame of isochrones
    4. visualise it

data sourcing

import osmnx as ox
import pandas as pd
import warnings
import networkx as nx
import geopandas as gpd
from shapely.geometry import Point

warnings.simplefilter(action="ignore", category=FutureWarning)
warnings.simplefilter(action="ignore", category=PendingDeprecationWarning)
ox.config(use_cache=True, log_console=False)
# get some cities
cities = ["Hereford"]  # ["Hereford", "Worcester", "Gloucester"]
cities = ox.geocode_to_gdf([{"city": c, "country": "UK"} for c in cities])

# get some schools
tags = {"amenity": "school"}
schools = pd.concat(
    [
        ox.geometries.geometries_from_polygon(r["geometry"], tags)
        for i, r in cities.iterrows()
    ]
)
schools = (
    schools.loc["way"].dropna(axis=1, thresh=len(schools) / 4).drop(columns=["nodes"])
)
# change polygon to point
schools["geometry"] = schools.to_crs(schools.estimate_utm_crs())[
    "geometry"
].centroid.to_crs(schools.crs)

get_isochrone()


# function to get isochrones
def get_isochrone(
    lon, lat, walk_times=[15, 30], speed=4.5, name=None, point_index=None
):
    loc = (lat, lon)
    G = ox.graph_from_point(loc, simplify=True, network_type="walk")
    gdf_nodes = ox.graph_to_gdfs(G, edges=False)
    center_node = ox.distance.nearest_nodes(G, lon, lat)

    meters_per_minute = speed * 1000 / 60  # km per hour to m per minute
    for u, v, k, data in G.edges(data=True, keys=True):
        data["time"] = data["length"] / meters_per_minute
    polys = []
    for walk_time in walk_times:
        subgraph = nx.ego_graph(G, center_node, radius=walk_time, distance="time")
        node_points = [
            Point(data["x"], data["y"]) for node, data in subgraph.nodes(data=True)
        ]
        polys.append(gpd.GeoSeries(node_points).unary_union.convex_hull)
    info = {}
    if name:
        info["name"] = [name for t in walk_times]
    if point_index:
        info["point_index"] = [point_index for t in walk_times]
    return {**{"geometry": polys, "time": walk_times}, **info}

integration

WT = [5, 10, 15]
SCHOOLS = 5

# build geopandas data frame of isochrone polygons for each school
isochrones = pd.concat(
    [
        gpd.GeoDataFrame(
            get_isochrone(
                r["geometry"].x,
                r["geometry"].y,
                name=r["name"],
                point_index=i,
                walk_times=WT,
            ),
            crs=schools.crs,
        )
        for i, r in schools.head(SCHOOLS).iterrows()
    ]
)

visualise

warnings.filterwarnings("ignore")

gdf = isochrones.set_index(["time", "point_index"]).copy()
# remove shorter walk time from longer walk time polygon to make folium work better
for idx in range(len(WT)-1,0,-1):
    gdf.loc[WT[idx], "geometry"] = (
        gdf.loc[WT[idx]]
        .apply(
            lambda r: r["geometry"].symmetric_difference(
                gdf.loc[(WT[idx-1], r.name), "geometry"]
            ),
            axis=1,
        )
        .values
    )

m = gdf.reset_index().explore(column="time", height=300, width=500, scheme="boxplot")
schools.head(SCHOOLS).explore(m=m, marker_kwds={"radius": 3, "color": "red"})

enter image description here

merge overlapping polygons

import matplotlib.cm as cm
import matplotlib.colors as colors
import folium

# merge overlapping polygons
# https://gis.stackexchange.com/questions/334459/how-to-dissolve-overlapping-polygons-using-geopandas
mergedpolys = gpd.GeoDataFrame(
    geometry=isochrones.groupby("time")["geometry"]
    .agg(lambda g: g.unary_union)
    .apply(lambda g: [g] if isinstance(g, shapely.geometry.Polygon) else g.geoms)
    .explode(),
    crs=isochrones.crs,
)

# visualize merged polygons
m = None
for i, wt in enumerate(WT[::-1]):
    m = mergedpolys.loc[[wt]].explore(
        m=m,
        color=colors.to_hex(cm.get_cmap("tab20b", len(WT))(i)),
        name=wt,
        height=300,
        width=500,
    )

m = schools.head(SCHOOLS).explore(
    m=m, marker_kwds={"radius": 3, "color": "red"}, name="schools"
)
folium.LayerControl().add_to(m)

m

enter image description here

Upvotes: 4

Related Questions