diff --git a/assets/geohash-layer.jpeg b/assets/geohash-layer.jpeg new file mode 100644 index 00000000..78dff7e6 Binary files /dev/null and b/assets/geohash-layer.jpeg differ diff --git a/examples/geohash-layer.ipynb b/examples/geohash-layer.ipynb new file mode 100644 index 00000000..50dda09b --- /dev/null +++ b/examples/geohash-layer.ipynb @@ -0,0 +1,307 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9740523b", + "metadata": {}, + "source": [ + "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.\n", + "\n", + "The data comes from [NYC Open Data's 311 Service Requests dataset](https://data.cityofnewyork.us/Social-Services/311-Service-Requests-from-2020-to-Present/erm2-nwe9/about_data).\n" + ] + }, + { + "cell_type": "markdown", + "id": "7cb2e3c6", + "metadata": {}, + "source": [ + "## Dependencies\n", + "\n", + "Install `uv` and then launch this notebook with:\n", + "\n", + "```bash\n", + "uvx juv run examples/geohash-layer.ipynb" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2375ded", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "# /// script\n", + "# requires-python = \">=3.12\"\n", + "# dependencies = [\n", + "# \"geohash2>=1.1\",\n", + "# \"lonboard>=0.16.0\",\n", + "# \"matplotlib>=3.11.1\",\n", + "# \"palettable>=3.3.3\",\n", + "# \"pandas>=3.0.5\",\n", + "# \"pyarrow>=25.0.1\",\n", + "# \"pygeohash>=3.3.1\",\n", + "# \"requests>=2.34.2\",\n", + "# ]\n", + "# ///" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b81dd458", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import pygeohash\n", + "import requests\n", + "from matplotlib.colors import LogNorm\n", + "from palettable.colorbrewer.sequential import YlOrRd_9\n", + "\n", + "from lonboard import GeohashLayer, Map\n", + "from lonboard.basemap import CartoStyle, MaplibreBasemap\n", + "from lonboard.colormap import apply_continuous_cmap\n", + "from lonboard.view_state import MapViewState" + ] + }, + { + "cell_type": "markdown", + "id": "5d4db92c", + "metadata": {}, + "source": [ + "## Download and aggregate the data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdc67d5d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
geohashrequest_countprimary_boroughtop_complainttop_complaint_count
0dr5nqr1STATEN ISLANDNoise - Commercial1
1dr5nqw1STATEN ISLANDNoise - Residential1
2dr5nqx1STATEN ISLANDIllegal Parking1
3dr5nqz1STATEN ISLANDDamaged Tree1
4dr5nw91STATEN ISLANDBuilding/Use1
\n", + "
" + ], + "text/plain": [ + " geohash request_count primary_borough top_complaint \\\n", + "0 dr5nqr 1 STATEN ISLAND Noise - Commercial \n", + "1 dr5nqw 1 STATEN ISLAND Noise - Residential \n", + "2 dr5nqx 1 STATEN ISLAND Illegal Parking \n", + "3 dr5nqz 1 STATEN ISLAND Damaged Tree \n", + "4 dr5nw9 1 STATEN ISLAND Building/Use \n", + "\n", + " top_complaint_count \n", + "0 1 \n", + "1 1 \n", + "2 1 \n", + "3 1 \n", + "4 1 " + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "API_URL = \"https://data.cityofnewyork.us/resource/erm2-nwe9.json\"\n", + "PARAMS = {\n", + " \"$select\": \"latitude, longitude, complaint_type, borough\",\n", + " \"$where\": (\n", + " \"created_date >= '2025-01-01T00:00:00' AND \"\n", + " \"created_date < '2025-01-08T00:00:00' AND \"\n", + " \"latitude IS NOT NULL AND longitude IS NOT NULL\"\n", + " ),\n", + " \"$order\": \"created_date ASC\",\n", + " \"$limit\": 10_000,\n", + "}\n", + "\n", + "try:\n", + " response = requests.get(API_URL, params=PARAMS, timeout=30)\n", + " response.raise_for_status()\n", + "except requests.RequestException as exc:\n", + " raise RuntimeError(\n", + " \"Unable to download NYC 311 data. Please try again later.\",\n", + " ) from exc\n", + "\n", + "requests_df = pd.DataFrame(response.json())\n", + "if requests_df.empty:\n", + " raise RuntimeError(\"The NYC 311 query returned no requests.\")\n", + "\n", + "requests_df = requests_df.astype({\"latitude\": \"float64\", \"longitude\": \"float64\"})\n", + "requests_df[\"geohash\"] = [\n", + " pygeohash.encode(latitude, longitude, precision=6)\n", + " for latitude, longitude in zip(\n", + " requests_df[\"latitude\"],\n", + " requests_df[\"longitude\"],\n", + " strict=True,\n", + " )\n", + "]\n", + "\n", + "\n", + "def aggregate_cell_data(df: pd.DataFrame) -> pd.Series:\n", + " top_complaint = (\n", + " df[\"complaint_type\"].mode()[0] if not df[\"complaint_type\"].empty else \"N/A\"\n", + " )\n", + " return pd.Series(\n", + " {\n", + " \"request_count\": len(df),\n", + " \"primary_borough\": df[\"borough\"].mode()[0]\n", + " if \"borough\" in df.columns\n", + " else \"Unspecified\",\n", + " \"top_complaint\": top_complaint,\n", + " \"top_complaint_count\": (df[\"complaint_type\"] == top_complaint).sum(),\n", + " },\n", + " )\n", + "\n", + "\n", + "cells = requests_df.groupby(\"geohash\", as_index=False).apply(aggregate_cell_data)\n", + "cells.head()" + ] + }, + { + "cell_type": "markdown", + "id": "d00c9dc4", + "metadata": {}, + "source": [ + "## Explore neighborhood-scale demand\n", + "Hover over a cell to inspect its geohash and request count." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7be0a8a8", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6c77d7957c374aa6a283c2abd76b9eaf", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(, VBox(children=(ErrorOutput(), ErrorOutput(), ErrorOu…" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "color_scale = LogNorm(\n", + " vmin=cells[\"request_count\"].min(),\n", + " vmax=cells[\"request_count\"].max(),\n", + ")\n", + "\n", + "layer = GeohashLayer.from_pandas(\n", + " cells,\n", + " get_geohash=cells[\"geohash\"],\n", + " get_fill_color=apply_continuous_cmap(\n", + " color_scale(cells[\"request_count\"]),\n", + " YlOrRd_9,\n", + " alpha=0.88,\n", + " ),\n", + " get_line_color=[35, 12, 8, 160],\n", + " line_width_min_pixels=0.75,\n", + " pickable=True,\n", + ")\n", + "\n", + "map_ = Map(\n", + " layer,\n", + " basemap=MaplibreBasemap(style=CartoStyle.DarkMatter),\n", + " view_state=MapViewState(longitude=-73.96, latitude=40.73, zoom=10),\n", + " height=700,\n", + " show_tooltip=True,\n", + " show_side_panel=False,\n", + " picking_radius=5,\n", + ")\n", + "map_" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/index.md b/examples/index.md index fa0cec8d..85f3889c 100644 --- a/examples/index.md +++ b/examples/index.md @@ -12,6 +12,7 @@ - [Global boundaries ![](../assets/boundaries.png)](../examples/global-boundaries) using [`PolygonLayer`][lonboard.PolygonLayer] - [Raster PMTiles ![](../assets/raster-pmtiles.jpg)](../examples/raster-pmtiles) using [`RasterLayer`][lonboard.RasterLayer] - [H3 Population Data ![](../assets/kontur-h3.jpg)](../examples/kontur_pop) using [`H3HexagonLayer`][lonboard.H3HexagonLayer] +- [NYC 311 Data ![](../assets/geohash-layer.jpeg)](../examples/geohash-layer) using [`GeohashLayer`][lonboard.GeohashLayer] - [U.S. County-to-County Migration ![](../assets/arc-layer-migration-example.gif)](../examples/migration) using [`ArcLayer`][lonboard.ArcLayer] and [`BrushingExtension`][lonboard.layer_extension.BrushingExtension] - [Scatterplot with GPU data filtering ![](../assets/data-filter-extension.gif)](../examples/data-filter-extension) using [`ScatterplotLayer`][lonboard.ScatterplotLayer] and [`DataFilterExtension`][lonboard.layer_extension.DataFilterExtension] - [Categorical Filtering ![](../assets/data-filter-extension-categorical.gif)](../examples/data-filter-extension-categorical) using [`ScatterplotLayer`][lonboard.ScatterplotLayer] and [`DataFilterExtension`][lonboard.layer_extension.DataFilterExtension] diff --git a/mkdocs.yml b/mkdocs.yml index e730ed66..4516bcae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,7 @@ nav: - examples/interleaved-labels.ipynb - examples/linked-maps.ipynb - examples/clicked-point.ipynb + - examples/geohash-layer.ipynb - NYC Taxi Trips: examples/marimo/nyc_taxi_trips.md - Integrations: - examples/duckdb.ipynb