Note

This page was generated from user_guide/getting_started.ipynb.
Interactive online version: Binder badge

Getting started

An introduction to momepy

Momepy is a library for quantitative analysis of urban form - urban morphometrics. It is built on top of GeoPandas, PySAL and networkX.

Some of the functionality that momepy offers:

  • Measuring dimensions of morphological elements, their parts, and aggregated structures.

  • Quantifying shapes of geometries representing a wide range of morphological features.

  • Capturing spatial distribution of elements of one kind as well as relationships between different kinds.

  • Computing density and other types of intensity characters.

  • Calculating diversity of various aspects of urban form.

  • Capturing connectivity of urban street networks

  • Generating relational elements of urban form (e.g. morphological tessellation)

Momepy aims to provide a wide range of tools for a systematic and exhaustive analysis of urban form. It can work with a wide range of elements, while focused on building footprints and street networks.

Installation

Momepy can be easily installed using conda from conda-forge. For detailed installation instructions, please refer to the installation documentation.

conda install momepy -c conda-forge

Dependencies

To run all examples in this notebook, you will need osmnx to download data from OpenStreetMap. You can install all using the following commands:

conda install osmnx -c conda-forge

Simple examples

Here are simple examples using embedded bubenec dataset (part of the Bubeneč neighborhood in Prague).

[1]:
import geopandas as gpd
import momepy

Morphometric analysis using momepy usually starts with GeoPandas GeoDataFrame containing morphological elements. In this case, we will begin with buildings represented by their footprints.

We have imported momepy, geopandas to handle the spatial data, and matplotlib to get a bit more control over plotting. To load bubenec data, we need to get retrieve correct path using momepy.datasets.get_path("bubenec"). The dataset itself is a GeoPackage with more layers, and now we want buildings.

[2]:
buildings = gpd.read_file(
    momepy.datasets.get_path("bubenec"), layer="buildings"
)
[3]:
ax = buildings.plot(figsize=(8, 8))
ax.set_axis_off()
../_images/user_guide_getting_started_6_0.png

Momepy defines function for each measurable character. To illustrate how momepy works, we can try to measure a few simple characters.

Equivalent rectangular index

momepy.equivalent_rectangular_index is an example of a morphometric character capturing the shape of each object. It can be calculated using a simple function consuimg GeoPandas object:

[4]:
blg_eri = momepy.equivalent_rectangular_index(buildings)
/Users/martin/miniforge3/envs/momepy/lib/python3.11/site-packages/pandas/core/arraylike.py:492: RuntimeWarning: invalid value encountered in oriented_envelope
  return getattr(ufunc, method)(*new_inputs, **kwargs)

The result is a pandas.Series.

[5]:
blg_eri.head()
[5]:
0    0.787923
1    0.443137
2    0.954253
3    0.851658
4    0.957543
dtype: float64

In a typical worflow, you add the result as a new column of the GeoDataFrame

[6]:
buildings["eri"] = momepy.equivalent_rectangular_index(buildings)

Check how it looks.

[7]:
ax = buildings.plot("eri", legend=True, figsize=(8, 8))
ax.set_axis_off()
../_images/user_guide_getting_started_15_0.png

Perimeter wall length

Geometric elements are rarely considered individually. With urban fabric forming blocks like the one used here, you may be interseted in a length of the perceived perimeter wall of such a block. Note that all buildings forming the same wall share the resulting value.

[8]:
buildings["wall_length"] = momepy.perimeter_wall(buildings)
buildings.head()
[8]:
uID geometry eri wall_length
0 1 POLYGON ((1603599.221 6464369.816, 1603602.984... 0.787923 137.186310
1 2 POLYGON ((1603042.88 6464261.498, 1603038.961 ... 0.443137 663.342296
2 3 POLYGON ((1603044.65 6464178.035, 1603049.192 ... 0.954253 663.342296
3 4 POLYGON ((1603036.557 6464141.467, 1603036.969... 0.851658 663.342296
4 5 POLYGON ((1603082.387 6464142.022, 1603081.574... 0.957543 663.342296
[9]:
ax = buildings.plot("wall_length", legend=True, figsize=(8, 8))
ax.set_axis_off()
../_images/user_guide_getting_started_18_0.png

Capturing relation between different elements

Urban form is a complex entity that needs to be represented by multiple morphological elements, and we need to be able to describe the relationship between them. With the absence of plots for our bubenec case, we can use morphological tessellation as the smallest spatial division.

[10]:
tessellation = momepy.morphological_tessellation(
    buildings, clip=momepy.buffered_limit(buildings, buffer=50)
)
tessellation.head()
[10]:
geometry
0 POLYGON ((1603577.153 6464348.291, 1603576.946...
1 POLYGON ((1603166.356 6464326.62, 1603166.425 ...
2 POLYGON ((1603006.941 6464167.63, 1603009.97 6...
3 POLYGON ((1602988.227 6464129.905, 1602995.269...
4 POLYGON ((1603084.231 6464104.386, 1603083.773...
[11]:
ax = tessellation.plot(edgecolor="white", figsize=(8, 8))
buildings.plot(ax=ax, color="white", alpha=0.5)
ax.set_axis_off()
../_images/user_guide_getting_started_21_0.png

Coverage area ratio

Now we can calculate how big part of each tessellation cell is covered by a related building. This is straightforward without a need for an additional function, because the two have matching index. Buildings have the same index as related tessellation cells, which is used to link both together (see Data Structure).

[12]:
tessellation["CAR"] = buildings.area / tessellation.area
[13]:
ax = tessellation.plot(
    "CAR",
    edgecolor="white",
    legend=True,
    scheme="NaturalBreaks",
    k=10,
    legend_kwds={"loc": "lower left"},
    figsize=(8, 8),
)
buildings.plot(ax=ax, color="white", alpha=0.5)
ax.set_axis_off()
../_images/user_guide_getting_started_24_0.png

Finally, to cover the last of the essential elements, we import the street network.

[14]:
streets = gpd.read_file(momepy.datasets.get_path("bubenec"), layer="streets")
[15]:
ax = tessellation.plot(edgecolor="white", linewidth=0.2, figsize=(8, 8))
buildings.plot(ax=ax, color="white", alpha=0.5)
streets.plot(ax=ax, color="black")
ax.set_axis_off()
../_images/user_guide_getting_started_27_0.png

Street profile

momepy.street_profile captures the relations between the segments of the street network and buildings.

[16]:
profile = momepy.street_profile(streets, buildings)
profile.head()
[16]:
width openness width_deviation
0 47.905964 0.946429 0.020420
1 42.418068 0.615385 2.644521
2 32.131831 0.608696 2.864438
3 50.000000 1.000000 NaN
4 50.000000 1.000000 NaN

We have now captured multiple characters of the street profile. As we did not specify building height (we do not know it for our data), we have widths (mean) - profile["width"], standard deviation of widths (along the segment) - profile["width_deviation"], and the degree of openness - profile["openness"].

[17]:
streets["width"] = profile["width"]
streets["openness"] = profile["openness"]
[18]:
ax = buildings.plot()
streets.plot("width", ax=ax, legend=True, figsize=(8, 8))
ax.set_axis_off()
_ = ax.set_title("width")
../_images/user_guide_getting_started_32_0.png
[19]:
ax = buildings.plot()
streets.plot("openness", ax=ax, legend=True, figsize=(8, 8))
ax.set_axis_off()
_ = ax.set_title("openness")
../_images/user_guide_getting_started_33_0.png

Using OpenStreetMap data

In some cases (based on the completeness of OSM), we can use OpenStreetMap data without the need to save them to file and read them via GeoPandas. We can use OSMnx to retrieve them directly. In this example, we will download the building footprint of Kahla in Germany and project it to projected CRS. Momepy expects that all GeoDataFrames have the same (projected) CRS.

[20]:
import osmnx as ox

gdf = ox.features_from_place("Kahla, Germany", tags={"building": True})
gdf_projected = ox.projection.project_gdf(gdf)
/Users/martin/miniforge3/envs/momepy/lib/python3.11/site-packages/osmnx/features.py:294: FutureWarning: The 'unary_union' attribute is deprecated, use the 'union_all()' method instead.
  polygon = gdf_place["geometry"].unary_union
[21]:
ax = gdf_projected.plot(figsize=(8, 8))
ax.set_axis_off()
../_images/user_guide_getting_started_36_0.png

Now we are in the same situation as we were above, and we can start our morphometric analysis as illustrated on the equivalent rectangular index below.

[22]:
gdf_projected["eri"] = momepy.equivalent_rectangular_index(gdf_projected)
[23]:
ax = gdf_projected.plot(
    "eri", legend=True, scheme="NaturalBreaks", k=10, figsize=(8, 8)
)
ax.set_axis_off()
../_images/user_guide_getting_started_39_0.png

Next steps

Now we have basic knowledge of what momepy is and how it works. It is time to install momepy (if you haven’t done so yet) and browse the rest of this user guide to see more examples. Once done, head to the API reference to see the full extent of characters momepy can capture and find those you need for your research.