Filtering by attribute¶
OSM tag queries can't express an AND on an attribute. osmnx ORs the keys, so
{"amenity": "restaurant", "cuisine": "italian"} means restaurant or anything
italian-cuisined — not restaurants whose cuisine is italian. The fix is the per-feature
filter option, applied to the rows that come back from a
broad tag fetch:
- OR — a list of values inside one column:
{"cuisine": ["italian", "pizza"]}keeps a row whosecuisineis any of them (values are split on;, so"italian;pizza"matches too). - AND — several columns, all of which must match:
{"highway": ["footway"], "surface": ["asphalt", "paved"]}keeps only footways that are also smoothly surfaced.
This notebook fetches real OpenStreetMap data for a slice of central London and works through one example of each, with maps and the resulting raster.
import warnings
warnings.filterwarnings("ignore")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import ListedColormap
from matplotlib.patches import Patch
from osm_rasterizer import rasterize, fetch_features
plt.rcParams.update({"figure.dpi": 110, "font.size": 10, "axes.titlesize": 12})
TEAL, ORANGE, PURPLE, GREY = "#0f8a8a", "#e2711d", "#7048b6", "#c9d1d3"
# A compact bbox over Soho / the West End, in WGS84 (minx, miny, maxx, maxy).
BBOX = (-0.14, 51.505, -0.11, 51.52)
def basemap(ax, title):
ax.set_title(title)
ax.set_axis_off()
ax.set_aspect("equal")
OR within a column — restaurants by cuisine¶
We fetch every restaurant once, then split them into cuisine families. Each family is an OR over
a set of cuisine values. A restaurant is fetched with the broad {"amenity": "restaurant"} query;
osmnx would never let us ask for "italian restaurants" directly.
restaurants = fetch_features(BBOX, {"amenity": "restaurant"})
restaurants = restaurants.to_crs(3857) # web-mercator metres, for nice map aspect
print(f"{len(restaurants)} restaurants fetched")
# Each family is an OR-set of cuisine values — exactly what a filter list means.
families = {
"Italian": ["italian", "pizza"],
"East Asian": ["chinese", "japanese", "korean", "thai", "vietnamese", "asian"],
"Indian": ["indian"],
}
def family_of(cuisine):
tokens = {t.strip() for t in str(cuisine).split(";")} if cuisine == cuisine else set()
for name, values in families.items():
if tokens & set(values):
return name
return "Other / untagged"
restaurants["family"] = restaurants["cuisine"].map(family_of)
restaurants["family"].value_counts()
879 restaurants fetched
family Other / untagged 525 East Asian 201 Italian 117 Indian 36 Name: count, dtype: int64
colors = {"Italian": TEAL, "East Asian": ORANGE, "Indian": PURPLE, "Other / untagged": GREY}
fig, ax = plt.subplots(figsize=(8, 6))
# draw the "other" points first so the coloured families sit on top
for name in ["Other / untagged", "Indian", "East Asian", "Italian"]:
sub = restaurants[restaurants["family"] == name]
sub.plot(ax=ax, color=colors[name], markersize=18 if name != "Other / untagged" else 8,
alpha=0.9 if name != "Other / untagged" else 0.5, label=f"{name} ({len(sub)})")
basemap(ax, "Restaurants by cuisine family — OR within the `cuisine` column")
ax.legend(loc="upper left", frameon=True, framealpha=0.9)
plt.tight_layout()
plt.show()
# The same split, as a bar chart of counts.
counts = restaurants["family"].value_counts().reindex(
["Italian", "East Asian", "Indian", "Other / untagged"])
fig, ax = plt.subplots(figsize=(7, 3.2))
ax.bar(counts.index, counts.values, color=[colors[i] for i in counts.index])
ax.set_ylabel("restaurants")
ax.set_title("Restaurants per cuisine family")
for i, v in enumerate(counts.values):
ax.text(i, v + 2, str(v), ha="center", fontsize=9)
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
plt.show()
Passed to rasterize, that OR-set is just a filter list — here as three bands of a categorical
raster:
rasterize(BBOX, features=[
("italian", {"amenity": "restaurant"}, {"filter": {"cuisine": ["italian", "pizza"]}}),
("east_asian", {"amenity": "restaurant"}, {"filter": {"cuisine": ["chinese", "japanese", "korean", "thai", "vietnamese", "asian"]}}),
("indian", {"amenity": "restaurant"}, {"filter": {"cuisine": ["indian"]}}),
], single_layer=True)
AND across columns — footways by surface¶
Now an AND: we want footways (a highway value) that are also smoothly surfaced (a surface
value). The tag query {"highway": "footway", "surface": "asphalt"} can't express this — it returns
footways or anything asphalt. With a filter dict, listing two columns means both must match,
while each column's list is still an OR over its values.
# One broad fetch of walkable ways; the filter narrows it two different ways.
ways = fetch_features(BBOX, {"highway": ["footway", "pedestrian", "path"]})
ways = ways[ways.geometry.type.isin(["LineString", "MultiLineString"])].to_crs(3857)
print(f"{len(ways)} walkable ways fetched")
SMOOTH = ["asphalt", "paved", "concrete"]
STONE = ["paving_stones", "sett", "cobblestone", "unhewn_cobblestone"]
def surface_class(row):
if row["highway"] != "footway":
return None
s = str(row.get("surface"))
if s in SMOOTH:
return "Smooth footway"
if s in STONE:
return "Stone-paved footway"
return None
ways["klass"] = ways.apply(surface_class, axis=1)
ways["klass"].value_counts(dropna=False)
5759 walkable ways fetched
klass NaN 2545 Stone-paved footway 1961 Smooth footway 1253 Name: count, dtype: int64
fig, ax = plt.subplots(figsize=(8, 6))
ways.plot(ax=ax, color=GREY, linewidth=0.6) # all ways, context
ways[ways["klass"] == "Smooth footway"].plot(ax=ax, color=TEAL, linewidth=1.8)
ways[ways["klass"] == "Stone-paved footway"].plot(ax=ax, color=ORANGE, linewidth=1.8)
basemap(ax, "Footways by surface — highway=footway AND surface ∈ {…}")
ax.legend(handles=[
Patch(color=GREY, label="other walkable ways"),
Patch(color=TEAL, label="Smooth footway (asphalt/paved/concrete)"),
Patch(color=ORANGE, label="Stone-paved footway (paving_stones/sett/…)"),
], loc="upper left", frameon=True, framealpha=0.9)
plt.tight_layout()
plt.show()
And the payoff — rasterize the two AND-filtered classes into a single categorical raster with
osm_rasterizer. The filter dicts carry the AND (highway and surface) and the OR (the
surface lists):
result = rasterize(
BBOX,
features=[
("smooth_footway", {"highway": ["footway", "pedestrian", "path"]},
{"filter": {"highway": ["footway"], "surface": SMOOTH}, "line_width": 6}),
("stone_footway", {"highway": ["footway", "pedestrian", "path"]},
{"filter": {"highway": ["footway"], "surface": STONE}, "line_width": 6}),
],
resolution=4.0,
single_layer=True, # 1 = smooth, 2 = stone (later feature wins overlaps)
)
print("raster shape:", result.array.shape, "| categories:", result.categories)
raster = result.array[0]
cmap = ListedColormap(["white", TEAL, ORANGE]) # 0 = nodata, 1 = smooth, 2 = stone
fig, ax = plt.subplots(figsize=(8, 6))
ax.imshow(raster, cmap=cmap, vmin=0, vmax=2, interpolation="nearest")
basemap(ax, "Rasterized footways by surface (4 m pixels)")
ax.legend(handles=[
Patch(color=TEAL, label="Smooth footway"),
Patch(color=ORANGE, label="Stone-paved footway"),
], loc="upper left", frameon=True, framealpha=0.9)
plt.tight_layout()
plt.show()
raster shape: (1, 438, 537) | categories: ['smooth_footway', 'stone_footway']
Recap¶
- OR = a list of values in one column —
{"cuisine": ["italian", "pizza"]}. - AND = multiple columns that must all match —
{"highway": ["footway"], "surface": [...]}. - They compose, and OSM
;-multi-values are matched token-by-token. - For arbitrary logic (numeric ranges, regex), pass a callable
gdf -> gdfas thefilterinstead of a dict, or handrasterizea pre-filtered GeoDataFrame directly.