Measuring spatial distribution

Spatial distribution can be captured many ways. This notebook show couple of them, based on orientation and street corridor.

import matplotlib.pyplot as plt
import momepy
import osmnx as ox
gdf = ox.features_from_place("Kahla, Germany", tags={"building": True})
buildings = ox.projection.project_gdf(gdf).reset_index()

limit = momepy.buffered_limit(buildings)
tessellation = momepy.morphological_tessellation(buildings, clip=limit)
streets_graph = ox.graph_from_place("Kahla, Germany", network_type="drive")
streets_graph = ox.projection.project_graph(streets_graph)
edges = ox.graph_to_gdfs(
    streets_graph,
    nodes=False,
    edges=True,
    node_geometry=False,
    fill_edge_geometry=True,
).reset_index(drop=True)

Alignment

We can measure alignment of different elements to their neighbours (for which Graph is needed) or to different elements. We will explore cell alignment (difference of orientation of buildings and cells) and street alignment (difference of orientation of buildings and street segments).

Cell alignment

For cell_alignment we need to know orientations, so let’s calculate them first. Orientation is defined as an orientation of the longext axis of bounding rectangle in range [0,45). It captures the deviation of orientation from cardinal directions:

buildings["orientation"] = momepy.orientation(buildings)
tessellation["orientation"] = momepy.orientation(tessellation)
buildings.plot(
    column="orientation", legend=True, cmap="Spectral", figsize=(10, 10)
).set_axis_off()
../../_images/d10e9bd9ff8fd54c32b81826b328334a0300ecd9078de5e76ca65f64885709d3.png

cell_alignment requires both orientation arrays:

buildings["cell_align"] = momepy.cell_alignment(
    buildings.orientation, tessellation.orientation
)
buildings.plot(
    column="cell_align", legend=True, cmap="Reds", figsize=(10, 10)
).set_axis_off()
../../_images/d35c8a457151cc31790eb16677c9c9d8b473cf76cd16aaf1d5cdca9b1250cc09.png

No really clear pattern is visible in this case, but it might be in other, especially comparing building orientation with plots.

Street alignment

Street alignment works on the same principle as cell alignment. What we do not have at this moment is street index linked to buildings:

buildings["street_index"] = momepy.get_nearest_street(buildings, edges)

street_alignment then requires orientations of both builings and streets and the index of the nearest street.

buildings["str_align"] = momepy.street_alignment(
    buildings.orientation, momepy.orientation(edges), buildings["street_index"]
)
ax = edges.plot(color="grey", linewidth=0.5, figsize=(10, 10))
buildings.plot(ax=ax, column="str_align", legend=True)
ax.set_axis_off()
../../_images/fb68735b4b0e4fb624e7250f07b82090d224e0db4ee396f8751a517212442372.png

Street profile

street_profile captures several characters at the same time. It generates a series of perpendicular ticks of set length and set spacing and returns mean widths of street profile, their standard deviation, mean height and its standard deviation, profile as a ratio of widht and height and degree of openness. If heights are not passed, it will not return them and profile. We will use Manhattan example to illustrate how it works. Building height column is converted to float and buildings are exploded to avoid multipolygons.

point = (40.731603, -73.977857)
dist = 1000
gdf = ox.features_from_point(point, dist=dist, tags={"building": True})
buildings = ox.projection.project_gdf(gdf)
buildings = buildings[buildings.geom_type.isin(["Polygon", "MultiPolygon"])]
def clean_heights(x):
    try:
        return float(x)
    except ValueError:
        return 0


buildings["height"] = buildings["height"].fillna(0).apply(clean_heights)
buildings = buildings.explode(ignore_index=True)
streets_graph = ox.graph_from_point(point, dist, network_type="drive")
streets_graph = ox.projection.project_graph(streets_graph)
edges = ox.graph_to_gdfs(
    streets_graph,
    nodes=False,
    edges=True,
    node_geometry=False,
    fill_edge_geometry=True,
).reset_index()
ax = buildings.plot(figsize=(10, 10), color="lightgrey")
edges.plot(ax=ax)
ax.set_axis_off()
../../_images/bb0161aa5684c50247129292efb9adbdb9b68bdd0701cc98e85988fbdcaf6b1f.png
profile = momepy.street_profile(edges, buildings, height=buildings["height"])

We can assign measrued characters as columns of edges gdf:

edges[profile.columns] = profile
edges[profile.columns].head()
width openness width_deviation height height_deviation hw_ratio
0 22.072200 0.416667 1.219565 16.178571 12.013273 0.732984
1 25.313778 0.312500 3.306950 21.672727 20.769510 0.856163
2 30.702528 0.888889 0.808025 19.000000 0.000000 0.618842
3 22.070729 0.416667 1.219296 16.178571 12.013273 0.733033
4 31.086188 0.763158 4.312195 23.122222 12.376871 0.743810
f, axes = plt.subplots(figsize=(15, 25), ncols=2, nrows=3)
edges.plot(ax=axes[0][0], column="width", legend=True, cmap="Blues_r")
buildings.plot(ax=axes[0][0], color="lightgrey")
edges.plot(ax=axes[0][1], column="width_deviation", legend=True)
buildings.plot(ax=axes[0][1], color="lightgrey")
axes[0][0].set_axis_off()
axes[0][0].set_title("width")
axes[0][1].set_axis_off()
axes[0][1].set_title("width_deviation")
edges.plot(ax=axes[1][0], column="hw_ratio", legend=True, cmap="Spectral")
buildings.plot(ax=axes[1][0], color="lightgrey")
edges.plot(ax=axes[1][1], column="openness", legend=True, cmap="Greens")
buildings.plot(ax=axes[1][1], color="lightgrey")
axes[1][0].set_axis_off()
axes[1][0].set_title("profile")
axes[1][1].set_axis_off()
axes[1][1].set_title("openness")
edges.plot(ax=axes[2][0], column="height", legend=True, cmap="Reds")
buildings.plot(ax=axes[2][0], color="lightgrey")
edges.plot(ax=axes[2][1], column="height_deviation", legend=True)
buildings.plot(ax=axes[2][1], color="lightgrey")
axes[2][0].set_axis_off()
axes[2][0].set_title("height")
axes[2][1].set_axis_off()
axes[2][1].set_title("height_deviations")
Text(0.5, 1.0, 'height_deviations')
../../_images/dae00951295d28fe5dc6852f47bb1fda9e836be1ee023826c3670c4bd5680a2a.png