Examples of usage

Spatial graphs are used across momepy. This notebook will illustrate its use on three examples.

import momepy
import osmnx as ox
from libpysal import graph

We will again use osmnx to get the data for our example and after preprocessing of building layer will generate tessellation layer.

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)

First order contiguity

Distance to neighbours

To calculate the mean distance to neighbouring buildings, we need queen contiguity weights of the first order capturing the relationship between immediate neighbours. Relationship between buildings is here represented by relationships between their tessellation cells.

contiguity = graph.Graph.build_contiguity(tessellation)
contiguity
<Graph of 3052 nodes and 17774 nonzero edges (6 components, 3 isolates) indexed by
 [0, 1, 2, 3, 4, ...]>
buildings["neighbour_dist"] = momepy.neighbor_distance(buildings, contiguity)
ax = buildings.plot(
    column="neighbour_dist",
    scheme="naturalbreaks",
    k=8,
    legend=True,
    cmap="Spectral",
    figsize=(8, 8),
)
ax.set_axis_off()
../../_images/0678752e9557dcfaeac255a5f50772b3a58e1088bd8d46cec3b253f4a109e7b9.png

Higher order / distance

However, typical usage of spatial graphs is to capture the vicinity of each feature. As illustrated in the previous notebook, there are multiple options on how to capture it. In this example, we will use queen contiguity of the higher order (3) based on morphological tessellation. Because we want to treat the building itself as part of its neighborhood, we assing self weights on top.

contiguity_k3 = contiguity.higher_order(3).assign_self_weight()
contiguity_k3
<Graph of 3052 nodes and 75684 nonzero edges (8 components, 0 isolates) indexed by
 [0, 1, 2, 3, 4, ...]>

Neighborhood description

Mean value of selected character within a vicinity of each cell (or building, plot) is a simple example. Most of the common descriptive statistics are available using the method Graph.describe().

describe_k3 = contiguity_k3.describe(tessellation.area)
describe_k3.head()
count mean median std min max sum nunique mode
focal
0 32 5940.075351 4835.592889 4223.147329 56.796506 15265.547587 190082.411239 32 56.796506
1 34 5906.437767 3135.727002 8874.521084 178.566245 47783.603656 200818.884087 34 178.566245
2 43 4779.708520 2636.014900 7788.380281 259.477203 47783.603656 205527.466379 43 259.477203
3 22 6615.741568 4781.701014 6563.479841 494.013255 27961.403116 145546.314498 22 494.013255
4 47 5208.238322 2661.934262 8088.375787 259.477203 53766.510632 244787.201135 47 259.477203
ax = tessellation.plot(
    column=describe_k3["mean"],
    legend=True,
    scheme="quantiles",
    k=10,
    cmap="Blues_r",
    figsize=(8, 8),
)
buildings.plot(ax=ax, color="white", alpha=0.4)
ax.set_axis_off()
../../_images/2c6dea1d73e68f9155c219057bce88fdf842134809b0fafe28c9e800293c3d0d.png
ax = tessellation.plot(
    column=describe_k3["median"],
    legend=True,
    scheme="quantiles",
    k=10,
    cmap="Blues_r",
    figsize=(8, 8),
)
buildings.plot(ax=ax, color="white", alpha=0.4)
ax.set_axis_off()
../../_images/d165414fcf6dcf78ab4449be40b4aba4c9276ab55bd0be17aaecc4ebfbf90970.png

In some cases, we might want to eliminate the effect of outliers. To do so, we can specify the quantile range on which should describe work. Below we will measure only interquartile mean.

describe_k3_iqr = contiguity_k3.describe(tessellation.area, q=(25, 75))
ax = tessellation.plot(
    column=describe_k3_iqr["mean"],
    legend=True,
    scheme="quantiles",
    k=10,
    cmap="Greens_r",
    figsize=(8, 8),
)
buildings.plot(ax=ax, color="white", alpha=0.4)
ax.set_axis_off()
../../_images/dabdb05aff921efca25831ffec49b2584cf0db67a6b3722cac54e97023fd7951.png

Weighted character

The weighted average is another example using the same spatial graphs. For illustration, we can try area-weighted circular compactness:

circular_compactness = momepy.circular_compactness(buildings)
buildings["weighted_circom"] = momepy.weighted_character(
    circular_compactness, buildings.area, contiguity_k3
)
ax = buildings.plot(
    column="weighted_circom",
    legend=True,
    scheme="quantiles",
    k=15,
    cmap="viridis",
    figsize=(8, 8),
)
ax.set_axis_off()
../../_images/58ccdbdd20753fb37e4826e04de147716cbed5814d5cc71491320ad4c6cad2b5.png

Density

We will again use our Manhattan case study to illustrate using graphs in capturing gross density.

point = (40.731603, -73.977857)
dist = 1000
gdf = ox.features_from_point(point, dist=dist, tags={"building": True})
gdf_projected = ox.projection.project_gdf(gdf)
buildings = gdf_projected[
    gdf_projected.geom_type.isin(["Polygon", "MultiPolygon"])
].reset_index()

limit = momepy.buffered_limit(buildings)
tessellation = momepy.morphological_tessellation(buildings, clip=limit)
ax = tessellation.plot(figsize=(8, 8))
buildings.plot(ax=ax, color="white", alpha=0.5)
ax.set_axis_off()
../../_images/9e89053a029aedb64399e6f9ea6068f1b7cb762648cd90c9c8559d22f38e5e9f.png

To get gross density, we need to know floor areas:

def clean_heights(x):
    try:
        return float(x)
    except ValueError:
        return 0


buildings["height"] = buildings["height"].fillna(0).apply(clean_heights)
buildings["floors"] = buildings["height"] // 3

buildings["floor_area"] = buildings.area * buildings["floors"]

Gross density can be expressed as sum of floor area in a neighborhood divided by sum areas within the same neighborhoods. Graph.describe() is perfect for that.

First, build the Graph.

contiguity_ny_k3 = (
    graph.Graph.build_contiguity(tessellation)
    .higher_order(3)
    .assign_self_weight()
)
contiguity_ny_k3
<Graph of 3157 nodes and 99033 nonzero edges (4 components, 0 isolates) indexed by
 [0, 1, 2, 3, 4, ...]>

Then you can measure sums and derive gross density.

areas = contiguity_ny_k3.describe(tessellation.area, statistics=["sum"])["sum"]
floor_areas = contiguity_ny_k3.describe(
    buildings["floor_area"], statistics=["sum"]
)["sum"]

tessellation["gross_density"] = floor_areas / areas
ax = tessellation.plot(
    column="gross_density",
    legend=True,
    scheme="naturalbreaks",
    k=10,
    figsize=(8, 8),
)
buildings.plot(ax=ax, color="white", alpha=0.5)
ax.set_axis_off()
../../_images/26ed94eb156231618c9e834dbf2ded71f125db5d1256731255b01c2f5a9ac381.png

In a similar way can be done gross coverage.

areas = contiguity_ny_k3.describe(tessellation.area, statistics=["sum"])["sum"]
blg_areas = contiguity_ny_k3.describe(buildings.area, statistics=["sum"])["sum"]

tessellation["gross_coverage"] = blg_areas / areas
ax = tessellation.plot(figsize=(8, 8), column="gross_coverage", legend=True)
buildings.plot(ax=ax, color="white", alpha=0.5)
ax.set_axis_off()
../../_images/41cfecd42fec8075835621b3676d4da6064904dc4f974df3fd0f828811e895a0.png