Geohash layer
This example aggregates public NYC 311 requests into geohash cells and renders their density with GeohashLayer. Brighter cells received fewer reports; darker red cells received more.
The data comes from NYC Open Data's 311 Service Requests dataset.
Dependencies¶
Install uv and then launch this notebook with:
uvx juv run examples/geohash-layer.ipynb
In [ ]:
Copied!
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "geohash2>=1.1",
# "lonboard>=0.16.0",
# "matplotlib>=3.11.1",
# "palettable>=3.3.3",
# "pandas>=3.0.5",
# "pyarrow>=25.0.1",
# "pygeohash>=3.3.1",
# "requests>=2.34.2",
# ]
# ///
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "geohash2>=1.1",
# "lonboard>=0.16.0",
# "matplotlib>=3.11.1",
# "palettable>=3.3.3",
# "pandas>=3.0.5",
# "pyarrow>=25.0.1",
# "pygeohash>=3.3.1",
# "requests>=2.34.2",
# ]
# ///
In [ ]:
Copied!
import pandas as pd
import pygeohash
import requests
from matplotlib.colors import LogNorm
from palettable.colorbrewer.sequential import YlOrRd_9
from lonboard import GeohashLayer, Map
from lonboard.basemap import CartoStyle, MaplibreBasemap
from lonboard.colormap import apply_continuous_cmap
from lonboard.view_state import MapViewState
import pandas as pd
import pygeohash
import requests
from matplotlib.colors import LogNorm
from palettable.colorbrewer.sequential import YlOrRd_9
from lonboard import GeohashLayer, Map
from lonboard.basemap import CartoStyle, MaplibreBasemap
from lonboard.colormap import apply_continuous_cmap
from lonboard.view_state import MapViewState
Download and aggregate the data¶
In [ ]:
Copied!
API_URL = "https://data.cityofnewyork.us/resource/erm2-nwe9.json"
PARAMS = {
"$select": "latitude, longitude, complaint_type, borough",
"$where": (
"created_date >= '2025-01-01T00:00:00' AND "
"created_date < '2025-01-08T00:00:00' AND "
"latitude IS NOT NULL AND longitude IS NOT NULL"
),
"$order": "created_date ASC",
"$limit": 10_000,
}
try:
response = requests.get(API_URL, params=PARAMS, timeout=30)
response.raise_for_status()
except requests.RequestException as exc:
raise RuntimeError(
"Unable to download NYC 311 data. Please try again later.",
) from exc
requests_df = pd.DataFrame(response.json())
if requests_df.empty:
raise RuntimeError("The NYC 311 query returned no requests.")
requests_df = requests_df.astype({"latitude": "float64", "longitude": "float64"})
requests_df["geohash"] = [
pygeohash.encode(latitude, longitude, precision=6)
for latitude, longitude in zip(
requests_df["latitude"],
requests_df["longitude"],
strict=True,
)
]
def aggregate_cell_data(df: pd.DataFrame) -> pd.Series:
top_complaint = (
df["complaint_type"].mode()[0] if not df["complaint_type"].empty else "N/A"
)
return pd.Series(
{
"request_count": len(df),
"primary_borough": df["borough"].mode()[0]
if "borough" in df.columns
else "Unspecified",
"top_complaint": top_complaint,
"top_complaint_count": (df["complaint_type"] == top_complaint).sum(),
},
)
cells = requests_df.groupby("geohash", as_index=False).apply(aggregate_cell_data)
cells.head()
API_URL = "https://data.cityofnewyork.us/resource/erm2-nwe9.json"
PARAMS = {
"$select": "latitude, longitude, complaint_type, borough",
"$where": (
"created_date >= '2025-01-01T00:00:00' AND "
"created_date < '2025-01-08T00:00:00' AND "
"latitude IS NOT NULL AND longitude IS NOT NULL"
),
"$order": "created_date ASC",
"$limit": 10_000,
}
try:
response = requests.get(API_URL, params=PARAMS, timeout=30)
response.raise_for_status()
except requests.RequestException as exc:
raise RuntimeError(
"Unable to download NYC 311 data. Please try again later.",
) from exc
requests_df = pd.DataFrame(response.json())
if requests_df.empty:
raise RuntimeError("The NYC 311 query returned no requests.")
requests_df = requests_df.astype({"latitude": "float64", "longitude": "float64"})
requests_df["geohash"] = [
pygeohash.encode(latitude, longitude, precision=6)
for latitude, longitude in zip(
requests_df["latitude"],
requests_df["longitude"],
strict=True,
)
]
def aggregate_cell_data(df: pd.DataFrame) -> pd.Series:
top_complaint = (
df["complaint_type"].mode()[0] if not df["complaint_type"].empty else "N/A"
)
return pd.Series(
{
"request_count": len(df),
"primary_borough": df["borough"].mode()[0]
if "borough" in df.columns
else "Unspecified",
"top_complaint": top_complaint,
"top_complaint_count": (df["complaint_type"] == top_complaint).sum(),
},
)
cells = requests_df.groupby("geohash", as_index=False).apply(aggregate_cell_data)
cells.head()
Out[ ]:
| geohash | request_count | primary_borough | top_complaint | top_complaint_count | |
|---|---|---|---|---|---|
| 0 | dr5nqr | 1 | STATEN ISLAND | Noise - Commercial | 1 |
| 1 | dr5nqw | 1 | STATEN ISLAND | Noise - Residential | 1 |
| 2 | dr5nqx | 1 | STATEN ISLAND | Illegal Parking | 1 |
| 3 | dr5nqz | 1 | STATEN ISLAND | Damaged Tree | 1 |
| 4 | dr5nw9 | 1 | STATEN ISLAND | Building/Use | 1 |
Explore neighborhood-scale demand¶
Hover over a cell to inspect its geohash and request count.
In [ ]:
Copied!
color_scale = LogNorm(
vmin=cells["request_count"].min(),
vmax=cells["request_count"].max(),
)
layer = GeohashLayer.from_pandas(
cells,
get_geohash=cells["geohash"],
get_fill_color=apply_continuous_cmap(
color_scale(cells["request_count"]),
YlOrRd_9,
alpha=0.88,
),
get_line_color=[35, 12, 8, 160],
line_width_min_pixels=0.75,
pickable=True,
)
map_ = Map(
layer,
basemap=MaplibreBasemap(style=CartoStyle.DarkMatter),
view_state=MapViewState(longitude=-73.96, latitude=40.73, zoom=10),
height=700,
show_tooltip=True,
show_side_panel=False,
picking_radius=5,
)
map_
color_scale = LogNorm(
vmin=cells["request_count"].min(),
vmax=cells["request_count"].max(),
)
layer = GeohashLayer.from_pandas(
cells,
get_geohash=cells["geohash"],
get_fill_color=apply_continuous_cmap(
color_scale(cells["request_count"]),
YlOrRd_9,
alpha=0.88,
),
get_line_color=[35, 12, 8, 160],
line_width_min_pixels=0.75,
pickable=True,
)
map_ = Map(
layer,
basemap=MaplibreBasemap(style=CartoStyle.DarkMatter),
view_state=MapViewState(longitude=-73.96, latitude=40.73, zoom=10),
height=700,
show_tooltip=True,
show_side_panel=False,
picking_radius=5,
)
map_
Out[ ]:
VBox(children=(<lonboard._map.Map object at 0x11ce1a360>, VBox(children=(ErrorOutput(), ErrorOutput(), ErrorOu…