{ "cells": [ { "cell_type": "markdown", "id": "51d818d9", "metadata": {}, "source": [ "# Visualize Germany's Population using lonboard\n", "In this notebook we’ll use the [`lonboard`](https://developmentseed.org/lonboard/) library to visualize Germany’s population data, which consists of approximately 3 million polygons. By the end, you'll know how to create interactive maps of large spatial datasets directly in Jupyter notebooks." ] }, { "cell_type": "markdown", "id": "908f9777", "metadata": {}, "source": [ "## Imports\n", "`lonboard` is designed to work within Jupyter Notebooks. Please make sure you're working in a Jupyter environment to follow along.\n", "\n", "Before starting, make sure you have the following packages installed:\n", "- `lonboard` and its dependencies: `anywidget`, `geopandas`, `matplotlib`, `palettable`, `pandas` (which requires `numpy`), `pyarrow` and `shapely`.\n", "- `requests`: For fetching data.\n", "- `seaborn`: For statistical data visualization.\n", "\n", "The following modules come standard with Python, so no need for separate installation:\n", "- `zipfile`\n", "- `io`" ] }, { "cell_type": "code", "execution_count": 1, "id": "0ffbe742-9fdd-4efe-9200-8efd8f78f168", "metadata": { "scrolled": true }, "outputs": [], "source": [ "import requests\n", "import zipfile\n", "import io\n", "\n", "from lonboard import Map, SolidPolygonLayer\n", "from lonboard.layer_extension import DataFilterExtension\n", "import ipywidgets as widgets\n", "\n", "import pandas as pd\n", "import numpy as np\n", "import geopandas as gpd\n", "\n", "import matplotlib as mpl\n", "import matplotlib.pyplot as plt\n", "from matplotlib.colors import LogNorm\n", "import matplotlib.ticker as mticker\n", "\n", "from shapely.geometry import Polygon\n", "\n", "import seaborn as sns" ] }, { "cell_type": "markdown", "id": "398705b0", "metadata": {}, "source": [ "## Fetch Population Data\n", "The population data we’ll be using is stored in a `.csv` file inside a `.zip` folder. It consists of 100m x 100m grid cells covering the area of Germany, with population counts for each cell. We'll download this file, extract the data, and load it into a `pandas` DataFrame." ] }, { "cell_type": "code", "execution_count": 2, "id": "c2d4910d", "metadata": {}, "outputs": [], "source": [ "url = \"https://www.zensus2022.de/static/Zensus_Veroeffentlichung/Zensus2022_Bevoelkerungszahl.zip\"\n", "target_file = \"Zensus2022_Bevoelkerungszahl_100m-Gitter.csv\"\n", "\n", "response = requests.get(url)\n", "\n", "if response.status_code == 200:\n", " with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref: # Open the ZIP file in memory\n", " if target_file in zip_ref.namelist():\n", " with zip_ref.open(target_file) as csv_file:\n", " df_pop = pd.read_csv(csv_file, delimiter=';') \n", " else:\n", " print(f\"{target_file} not found in the ZIP archive.\")\n", "else:\n", " print(f\"Failed to download file: {response.status_code}\")" ] }, { "cell_type": "markdown", "id": "c0cc550b", "metadata": {}, "source": [ "Let’s take a quick look at the first few rows of the data to understand its structure:" ] }, { "cell_type": "code", "execution_count": 3, "id": "fbf9ac13", "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", "
GITTER_ID_100mx_mp_100my_mp_100mEinwohner
0CRS3035RES100mN2689100E4337000433705026891504
1CRS3035RES100mN2689100E43411004341150268915011
2CRS3035RES100mN2690800E4341200434125026908504
3CRS3035RES100mN2691200E43412004341250269125012
4CRS3035RES100mN2691300E4341200434125026913503
\n", "
" ], "text/plain": [ " GITTER_ID_100m x_mp_100m y_mp_100m Einwohner\n", "0 CRS3035RES100mN2689100E4337000 4337050 2689150 4\n", "1 CRS3035RES100mN2689100E4341100 4341150 2689150 11\n", "2 CRS3035RES100mN2690800E4341200 4341250 2690850 4\n", "3 CRS3035RES100mN2691200E4341200 4341250 2691250 12\n", "4 CRS3035RES100mN2691300E4341200 4341250 2691350 3" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_pop.head()" ] }, { "cell_type": "markdown", "id": "6f9a7a5e", "metadata": {}, "source": [ "While we can't immediately tell the meaning of each column just from looking at the data, the [data description](https://www.zensus2022.de/static/Zensus_Veroeffentlichung/Datensatzbeschreibung_Bevoelkerungszahl_Gitterzellen.xlsx) clarifies the following:\n", "- `GITTER_ID_100m`: The unique identifier for each 100m x 100m grid cell.\n", "- `x_mp_100m`: The longitude of the cell's center using the [ETRS89-LAEA Europe (EPSG: 3035)](https://epsg.io/3035) coordinate reference system.\n", "- `y_mp_100m`: The latitude of the cell's center using the [ETRS89-LAEA Europe (EPSG: 3035)](https://epsg.io/3035) coordinate reference system.\n", "- `Einwohner`: The population count within each cell." ] }, { "cell_type": "markdown", "id": "965e7e9f", "metadata": {}, "source": [ "## Prepare Data" ] }, { "cell_type": "code", "execution_count": 4, "id": "5fa1dbdc-d61e-4daa-91af-8d440c9301da", "metadata": {}, "outputs": [], "source": [ "df_pop.rename(columns={'Einwohner': 'Population'}, inplace=True)\n", "df_pop['Population'] = pd.to_numeric(df_pop['Population'], errors='coerce') # coerce: any value that cannot be converted to a numeric type will be replaced with NaN" ] }, { "cell_type": "markdown", "id": "6733c003", "metadata": {}, "source": [ "To visualize the data on a map, we need to transform the coordinates into polygons representing each grid cell. The dataset uses the ETRS89-LAEA Europe coordinate reference system, which measures distances in meters and is well-suited for European datasets.\n", "\n", "Since each grid cell is 100m x 100m, and the coordinates provided represent the center of each cell, we can use this information—combined with the fact that the CRS measures in meters—to create accurate polygons. When creating the `geopandas` GeoDataFrame, we’ll ensure that the correct CRS (ETRS89-LAEA) is set. However, for visualization in `lonboard`, we need to convert the coordinates to the more commonly used [WGS84 (EPSG:4326)](https://epsg.io/4326) CRS, which is based on latitude and longitude.\n", "\n", "The following code will generate `geopandas Polygons` for each cell and convert the CRS for visualization:" ] }, { "cell_type": "code", "execution_count": 5, "id": "ebaad8e0", "metadata": {}, "outputs": [], "source": [ "def create_polygon(x, y, half_length=50):\n", " return Polygon([\n", " (x - half_length, y - half_length),\n", " (x + half_length, y - half_length),\n", " (x + half_length, y + half_length),\n", " (x - half_length, y + half_length)\n", " ])\n", " \n", "gdf_population = gpd.GeoDataFrame(df_pop['Population'], geometry=df_pop.apply(lambda row: create_polygon(row['x_mp_100m'], row['y_mp_100m']), axis=1), crs = 'EPSG:3035')\n", "\n", "# Convert the coordinate system to WGS84 (EPSG:4326)\n", "gdf_population = gdf_population.to_crs(epsg=4326)" ] }, { "cell_type": "code", "execution_count": 6, "id": "e65dbf35", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "RangeIndex: 3088037 entries, 0 to 3088036\n", "Data columns (total 2 columns):\n", " # Column Dtype \n", "--- ------ ----- \n", " 0 Population int64 \n", " 1 geometry geometry\n", "dtypes: geometry(1), int64(1)\n", "memory usage: 47.1 MB\n" ] } ], "source": [ "gdf_population.info()" ] }, { "cell_type": "code", "execution_count": 7, "id": "88ca0ac0", "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Populationgeometry
04POLYGON ((10.21146 47.31529, 10.21278 47.31529...
111POLYGON ((10.26565 47.31517, 10.26697 47.31517...
24POLYGON ((10.26705 47.33047, 10.26837 47.33046...
312POLYGON ((10.26707 47.33407, 10.26839 47.33407...
43POLYGON ((10.26707 47.33497, 10.26839 47.33497...
.........
308803214POLYGON ((8.42288 55.02299, 8.42445 55.02301, ...
30880334POLYGON ((8.41816 55.02383, 8.41972 55.02385, ...
308803410POLYGON ((8.41972 55.02385, 8.42129 55.02387, ...
30880353POLYGON ((8.42129 55.02387, 8.42285 55.02389, ...
30880363POLYGON ((8.42285 55.02389, 8.42441 55.02391, ...
\n", "

3088037 rows × 2 columns

\n", "
" ], "text/plain": [ " Population geometry\n", "0 4 POLYGON ((10.21146 47.31529, 10.21278 47.31529...\n", "1 11 POLYGON ((10.26565 47.31517, 10.26697 47.31517...\n", "2 4 POLYGON ((10.26705 47.33047, 10.26837 47.33046...\n", "3 12 POLYGON ((10.26707 47.33407, 10.26839 47.33407...\n", "4 3 POLYGON ((10.26707 47.33497, 10.26839 47.33497...\n", "... ... ...\n", "3088032 14 POLYGON ((8.42288 55.02299, 8.42445 55.02301, ...\n", "3088033 4 POLYGON ((8.41816 55.02383, 8.41972 55.02385, ...\n", "3088034 10 POLYGON ((8.41972 55.02385, 8.42129 55.02387, ...\n", "3088035 3 POLYGON ((8.42129 55.02387, 8.42285 55.02389, ...\n", "3088036 3 POLYGON ((8.42285 55.02389, 8.42441 55.02391, ...\n", "\n", "[3088037 rows x 2 columns]" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gdf_population" ] }, { "cell_type": "markdown", "id": "bd793f05", "metadata": {}, "source": [ "## Visualize Data Using `lonboard`\n", "\n", "To visualize the population data, we use the [`SolidPolygonLayer`](https://developmentseed.org/lonboard/latest/api/layers/solid-polygon-layer/). This layer is preferred over [`PolygonLayer`](https://developmentseed.org/lonboard/latest/api/layers/polygon-layer/) because it does not render polygon outlines, making it more performant for large datasets.\n", "\n", "We’ll create the `SolidPolygonLayer` from the `GeoDataFrame` we generated earlier and then add it to a `Map` for visualization:" ] }, { "cell_type": "code", "execution_count": 8, "id": "968c5a6d-5def-4815-9a65-6e27105d7300", "metadata": {}, "outputs": [], "source": [ "polygon_layer = SolidPolygonLayer.from_geopandas(\n", " gdf_population,\n", ")\n", "\n", "m = Map(polygon_layer) " ] }, { "cell_type": "code", "execution_count": 9, "id": "70f35514-30b1-46bc-9f40-c88c06a1ea31", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d0d1472a509e42ebbccfc619816ae865", "version_major": 2, "version_minor": 1 }, "text/plain": [ "Map(layers=[SolidPolygonLayer(table=pyarrow.Table\n", "Population: uint16\n", "geometry: list" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Skewness value: 4.7072006305650085\n" ] } ], "source": [ "def thousands_formatter(x, pos):\n", " return f'{int(x):,}'\n", " \n", "sns.histplot(gdf_population['Population'], bins=100)\n", "\n", "plt.gca().yaxis.set_major_formatter(mticker.FuncFormatter(thousands_formatter))\n", "plt.show()\n", "\n", "print(\"Skewness value:\",gdf_population['Population'].skew())" ] }, { "cell_type": "markdown", "id": "c26c4cbd", "metadata": {}, "source": [ "The histogram and skewness reveal a strong positive skew — most cells have low populations, while only a few cells have very high numbers. This is why our linear colormap isn’t showing much color variation. To make these differences stand out, let’s switch to a logarithmic colormap, which will better highlight the variations in population density." ] }, { "cell_type": "code", "execution_count": 14, "id": "60aa3f6e-d13a-4f44-9cf9-fa94dcfa1cd0", "metadata": {}, "outputs": [], "source": [ "normalizer = mpl.colors.LogNorm(vmin=gdf_population['Population'].min(), vmax=gdf_population['Population'].max()) # Normalize population to the 0-1 range on a log scale.\n", "colors = colormap(normalizer(gdf_population['Population']), bytes=True)" ] }, { "cell_type": "code", "execution_count": 15, "id": "dc4ef1d9-cdda-4ff7-8b64-293f19809711", "metadata": {}, "outputs": [], "source": [ "polygon_layer.get_fill_color = colors" ] }, { "cell_type": "markdown", "id": "e7fb63e9", "metadata": {}, "source": [ "### Add a Legend\n", "To make our map even clearer, let’s add a `colorbar` that shows how population densities correspond to colors. This will help users easily interpret the map. \n", "Here’s how we can set it up:" ] }, { "cell_type": "code", "execution_count": 16, "id": "e733d133-b2fa-4eed-8f46-b1deb428042d", "metadata": {}, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def thousands_formatter(x, pos):\n", " return f'{int(x):,}'\n", "\n", "def create_colorbar():\n", " fig, ax = plt.subplots(figsize=(8, 0.8))\n", " \n", " # Define the colorbar with LogNorm and the specified colormap\n", " cbar = plt.colorbar(\n", " plt.cm.ScalarMappable(norm=LogNorm(vmin=gdf_population['Population'].min(), vmax=gdf_population['Population'].max()), cmap=colormap),\n", " cax=ax,\n", " orientation='horizontal'\n", " )\n", " \n", " tick_values = np.logspace(np.log10(gdf_population['Population'].min()), np.log10(gdf_population['Population'].max()), num=5)\n", " cbar.set_ticks(tick_values)\n", " cbar.ax.xaxis.set_major_formatter(mticker.FuncFormatter(thousands_formatter))\n", " cbar.ax.tick_params(labelsize=8)\n", "\n", " cbar.set_label('Population per cell', fontsize=10)\n", " \n", " plt.tight_layout()\n", " plt.show()\n", "\n", "create_colorbar()" ] }, { "cell_type": "markdown", "id": "d340a64b-03b7-4a83-8e5f-0c7720b7fd6b", "metadata": {}, "source": [ "By using Jupyter widgets, we can capture the `colorbar` and then combine it with the map for a more comprehensive view." ] }, { "cell_type": "code", "execution_count": 18, "id": "8902ac20-b993-4054-9aba-c64acbef72e8", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "cd77cfdd2dc14859b508d4326a1bddc6", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(Map(layers=[SolidPolygonLayer(get_fill_color=\n", "[\n", " …" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m = Map(polygon_layer)\n", "m" ] }, { "cell_type": "markdown", "id": "988682e1", "metadata": {}, "source": [ "## Summary\n", "In this notebook, we explored how to visualize large spatial datasets, specifically Germany’s population data, using the `lonboard` library. You’ve seen how to fetch, clean, and transform population data into polygons, apply color scales to represent population density, and use advanced features like interactive filters and 3D maps for richer insights.\n", "\n", "By now, you should have a better understanding of how to handle and visualize large datasets directly in Jupyter notebooks, as well as how to customize maps for more meaningful visual representation.\n", "\n", "Learn More: If you’d like to explore further, check out the [lonboard documentation](https://developmentseed.org/lonboard/latest/) for more advanced functionalities." ] } ], "metadata": { "kernelspec": { "display_name": "lonboard_vis", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }