如何使用 Python OSMnx 库绘制多个等时线多边形?

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

我正在尝试为一些学校建立等时线,以了解可访问性。我的旅行方式是“步行”,我想创建一个至少包含 2 个旅行时间、30 分钟和 1 小时的列表。

下面是我的代码:

# 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

当我调用 get_isochrone 函数时:

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

返回的是一个多边形列表,每个学校点的多边形等于旅行时间列表中的项目数。 然而,我注意到为每个点返回的多边形完全相同。

只是为了验证:这个脚本表明它们是相同的:

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

结果如下:

如您所见,每个学校点 returns 两个具有完全相同坐标的多边形,用于不同的旅行时间。 python函数我复习了好几遍,甚至把旅行时间改成100之类的旅行时间列表中的一项,我还是得到了完全一样的坐标。我可能错过了一些东西,我会很感激任何帮助。谢谢!

另外,我还发现函数中的add_patch()方法只是给plot_graph()方法加色。如何将颜色添加到 Folium 地图中的多边形?

  • 看来你用过这个例子:https://github.com/gboeing/osmnx-examples/blob/main/notebooks/13-isolines-isochrones.ipynb
  • 如果你想要的只是多边形,那么就没有必要PolygonPatch
  • 您重构不正确,多次步行时间。你只需要生成edges一次
  • 综合起来:
    1. 寻找一些学校
    2. get_isochrone() 重构为您的用例。已更改 do it returns 一个 dict 具有正在调查的点/学校的索引和名称。
    3. 生成一个 geopandas 等时线数据框
    4. 形象化

数据来源

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}

整合

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"})

合并重叠的多边形

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

我正在尝试复制 ,但是当我 运行 该代码时出现错误。我认为这些错误是由于我使用的是不同版本的 osmnx。你能告诉我你使用的每个包是哪个版本吗?

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"})

我收到以下错误消息:

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'