diff --git a/10-geodaten/01-projections.ipynb b/10-geodaten/01-projections.ipynb
index e2afce5db79922c76b393541cc140976d15a4c13..e116f63a475759d8b2e8974345dd27aa6c81468b 100644
--- a/10-geodaten/01-projections.ipynb
+++ b/10-geodaten/01-projections.ipynb
@@ -25,21 +25,28 @@
    },
    "outputs": [],
    "source": [
-    "from sklearn.datasets import fetch_openml\n",
+    "import numpy as np\n",
+    "import os\n",
     "import geopandas as gpd\n",
+    "from shapely import affinity\n",
+    "from sklearn.datasets import fetch_openml\n",
     "\n",
-    "url = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_countries.geojson'\n",
-    "geo_countries = gpd.read_file(url)\n",
     "path = 'geo_countries.geojson'\n",
-    "geo_countries.to_file(path)\n",
+    "if not os.path.exists(path):\n",
+    "    url = 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_countries.geojson'\n",
+    "    geo_countries = gpd.read_file(url)\n",
+    "    geo_countries.to_file(path)      # save local copy\n",
+    "\n",
+    "geo_countries = gpd.read_file(path)  # load from local copy\n",
     "geo_countries = geo_countries[['name', 'iso_a3', 'geometry']].set_index('iso_a3')\n",
     "\n",
     "countries_df, _ = fetch_openml('Countries-of-the-World', as_frame=True, return_X_y=True)\n",
     "countries_df = countries_df.rename(columns={'Area_(sq._mi.)': 'ref_area', 'Country': 'name'})  # unit was km² already\n",
     "countries_df = countries_df[['name', 'ref_area']]\n",
-    "countries_df['name'] = countries_df.name.str.strip()"
+    "countries_df['name'] = countries_df.name.str.strip()\n",
+    "countries_df.loc[len(countries_df)] = ['Antarctica', 14_200_000]  # from: https://en.wikipedia.org/wiki/Antarctica"
    ],
-   "id": "0003-4c6fa2e8089a613e6a3300cf4bd6fe01d88ed16280b83e1e378065b75a9"
+   "id": "0003-1224f13613ae889be0984402ff1578d533b9774aae88ca6305a774f6860"
   },
   {
    "cell_type": "markdown",
@@ -60,20 +67,63 @@
    "source": [
     "countries = geo_countries.merge(countries_df, on='name')\n",
     "\n",
+    "scaledown = False\n",
     "# epsg = 25832  # UTM Zone 32N, good for germany, does not work on global scale\n",
     "# epsg = 4087\n",
     "# epsg = 3395\n",
-    "epsg = 3857\n",
     "# epsg = 4326\n",
     "# ...\n",
     "\n",
+    "# EPSG:3857 is the original projection CRS of the background map and thus the map corresponds to the computed projection error. So in this case we can scale geometries down to show true sizes of countries.\n",
+    "epsg, scaledown = 3857, True\n",
+    "\n",
     "countries = countries.to_crs(f'EPSG:{epsg}')\n",
+    "\n",
+    "# calculate relative difference of projected and reference areas\n",
     "countries['proj_area'] = countries.area / 1e6  # in km²\n",
-    "countries['rel_diff_area'] = ((countries['proj_area'] / countries['ref_area'] - 1).round(2) * 100).astype(int)  # in percent\n",
+    "ratio = countries['proj_area'] / countries['ref_area']\n",
+    "countries['rel_diff_area'] = ((ratio - 1).round(2) * 100).astype(int)  # in percent\n",
+    "\n",
+    "if scaledown:\n",
+    "    # split MultiPolygons into separate Polygons, e. g. 1 MultiPolygon of Germany → 6 Polygons (6 rows with the same data)\n",
+    "    countries = countries.explode()\n",
+    "\n",
+    "    # scale down each poly\n",
+    "    scale = 1 / np.sqrt(ratio)\n",
+    "    countries.geometry = [affinity.scale(g, s, s) for g, s in zip(countries.geometry, scale[countries.index])]\n",
+    "\n",
+    "    # special handling for Antarctica, since it would be below the visible area\n",
+    "    max_antarctica_area = countries.loc[countries.name == 'Antarctica'].area.max()\n",
+    "    largest_antarctica_poly = countries.area == max_antarctica_area\n",
+    "    shifted_antarctica = affinity.translate(countries.loc[largest_antarctica_poly, 'geometry'].squeeze(), yoff=np.sqrt(max_antarctica_area)*7)\n",
+    "    countries.loc[largest_antarctica_poly, 'geometry'] = shifted_antarctica\n",
+    "\n",
+    "    # reverse splitting, i. e. combine Polygons with the same name into a single MultiPolygon\n",
+    "    countries = countries.dissolve('name')\n",
+    "\n",
+    "# plot\n",
     "vmax = countries['rel_diff_area'].abs().quantile(0.97)\n",
-    "countries.explore(column='rel_diff_area', tooltip=['name', 'rel_diff_area', 'proj_area', 'ref_area'], popup=True, tiles=\"CartoDB voyager\", vmin=-vmax, vmax=vmax, cmap='coolwarm', attribution=countries.crs.name)"
+    "countries.explore(\n",
+    "    column='rel_diff_area',\n",
+    "    vmin=-vmax, vmax=vmax, cmap='coolwarm',\n",
+    "    tooltip=['name', 'rel_diff_area', 'proj_area', 'ref_area'],\n",
+    "    popup=True,\n",
+    "    tiles=\"CartoDB voyager\",\n",
+    "    attribution=countries.crs.name\n",
+    ")"
+   ],
+   "id": "0005-87f4bcf5159c2d37428cabbcd55f47bf4cd95bbb86ed15eced56f1b465b"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Siehe auch:\n",
+    "\n",
+    "-   https://truesizeofcountries.com/\n",
+    "-   https://www.thetruesize.com"
    ],
-   "id": "0005-b64aa750773482384137dd8fa7e5b5b069b9c692b3395c5c211650bf153"
+   "id": "0007-c166693366c20bec45d0a68d7bc44664690db85407f20dd66ceb12f7251"
   }
  ],
  "nbformat": 4,
diff --git a/10-geodaten/02-bike-track-sol.ipynb b/10-geodaten/02-bike-track-sol.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..6b05430658581b67bd2ea765fd38ded57cf6d3a8
--- /dev/null
+++ b/10-geodaten/02-bike-track-sol.ipynb
@@ -0,0 +1,142 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Bike Track\n",
+    "\n",
+    "Es ist ein GPS-Track von einer Fahrradtour gegeben. Dieser, sowie ein\n",
+    "GeoDataFrame von Kreisen und kreisfreien Städten wird schon im\n",
+    "Start-Code geladen. Finden Sie heraus durch welche Kreise und kreisfreie\n",
+    "Städte die Tour lief.\n",
+    "\n",
+    "*Bonus: Stellen Sie die Kreise und kreisfreie Städte, durch die die\n",
+    "Fahrradtour lief, in einer interaktiven Karte da. Ggf. stellen Sie\n",
+    "zusätzlich den GPS-Track dar. Siehe\n",
+    "[hier](https://geopandas.org/en/stable/docs/user_guide/interactive_mapping.html)\n",
+    "um mehrere GeoDataFrames in einer Karte darzustellen.*"
+   ],
+   "id": "0002-96cd7cef045240a7b1b0c57d7e374c0dd9716f4dc6f170b31077d61a462"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import geopandas as gpd\n",
+    "import zstandard\n",
+    "\n",
+    "cities_path = 'kreise-geo-mittel.geojson.zst'\n",
+    "with open(cities_path, 'rb') as fh:\n",
+    "    dctx = zstandard.ZstdDecompressor()\n",
+    "    with dctx.stream_reader(fh) as reader:\n",
+    "        grid = gpd.read_file(reader).set_index('ID_3')\n",
+    "\n",
+    "track_path = 'bike_track_2024-05-25.geojson.zst'\n",
+    "with open(track_path, 'rb') as fh:\n",
+    "    dctx = zstandard.ZstdDecompressor()\n",
+    "    with dctx.stream_reader(fh) as reader:\n",
+    "        track = gpd.read_file(reader)"
+   ],
+   "id": "0003-f5d67afb904d0f49edc541c7a33c18d0985cd0356b8747150b8687a8e18"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Lösung\n",
+    "\n",
+    "Mit `sjoin` können wir die Polygone auswählen, in denen die Punkte des\n",
+    "GPS-Track liegen. Dabei wird unter anderem eine Spalte `index_right`\n",
+    "erzeugt, die wir nutzen um die Kreise und kreisfreien Städte\n",
+    "entsprechend des Tracks zu sortieren. Aber eigentlich brauchen wir nur\n",
+    "den Namen `NAME_3` und die Geometrie zum Plotten. Da für jeden Punkt im\n",
+    "GPS-Track eine Zeile hinzugefügt wird entfernen wir die Duplikate."
+   ],
+   "id": "0005-cd644122221c1cbcbcf18caa67556650fe84a458610ba7d11da6109e7b1"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "cities = grid.sjoin(track).sort_values('index_right')\n",
+    "cities = cities[['NAME_3', 'geometry']]\n",
+    "cities = cities.drop_duplicates()"
+   ],
+   "id": "0006-2c45647891a1292c7e47ee77fd9ed51784bffeba4a292c7a25555e9e529"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "['Mettmann', 'Ennepe-Ruhr', 'Bochum Städte', 'Dortmund Städte', 'Unna', 'Soest', 'Hochsauerlandkreis', 'Waldeck-Frankenberg', 'Höxter', 'Kassel', 'Kassel Städte']"
+     ]
+    }
+   ],
+   "source": [
+    "cities['NAME_3'].to_list()"
+   ],
+   "id": "0007-4a30b87cb18cc8bb69afce6b37dc80d502773b474ca99b60625b42e2ccf"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Für die Bonusaufgabe ist nun nicht mehr viel zu tun. Aber wir geben uns\n",
+    "etwas Mühe mit dem GPS-Track und Plotten die Farbe entsprechend der\n",
+    "Geschwindigkeit und jeden Segmentanfang (beim Anhalten pausiert die\n",
+    "Aufnahme) etwas größer."
+   ],
+   "id": "0008-5ee78ebd0383ea7c29c4bade37d4ab2368b2ed24fb9719f4e7d070eb1d3"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "m = cities.explore()\n",
+    "track.explore(\n",
+    "    column='speed',\n",
+    "    style_kwds={\"style_function\": lambda x: {\"radius\": (x[\"properties\"][\"track_seg_point_id\"] == 0) * 3 + 1}},\n",
+    "    m=m\n",
+    ")"
+   ],
+   "id": "0009-bcaada0a444065b3b854e5a440df01ed30acd0e553f55216d4f44cc7e51"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Die vorbereiteten Daten zu den Kreisen und kreisfreien Städten stammen\n",
+    "von\n",
+    "[hier](https://github.com/isellsoap/deutschlandGeoJSON/tree/main/4_kreise)\n",
+    "(3_mittel.geo.json).\n",
+    "\n",
+    "Wenn man nicht nur Kreise und kreisfreie Städte haben will, sondern eine\n",
+    "höhere Auflösung, kann man z. B.\n",
+    "[Gemeindegrenzen](https://hub.arcgis.com/datasets/esri-de-content::gemeindegrenzen-2022/about)\n",
+    "verwenden."
+   ],
+   "id": "0011-2a24f7ded33e3832d9f7e06b4143c0b22b4763564f9cc12dec28965e820"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/10-geodaten/02-bike-track.ipynb b/10-geodaten/02-bike-track.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..096060027f70b5e238d15c74c5b333fdd25358e6
--- /dev/null
+++ b/10-geodaten/02-bike-track.ipynb
@@ -0,0 +1,51 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Bike Track\n",
+    "\n",
+    "Es ist ein GPS-Track von einer Fahrradtour gegeben. Dieser, sowie ein\n",
+    "GeoDataFrame von Kreisen und kreisfreien Städten wird schon im\n",
+    "Start-Code geladen. Finden Sie heraus durch welche Kreise und kreisfreie\n",
+    "Städte die Tour lief.\n",
+    "\n",
+    "*Bonus: Stellen Sie die Kreise und kreisfreie Städte, durch die die\n",
+    "Fahrradtour lief, in einer interaktiven Karte da. Ggf. stellen Sie\n",
+    "zusätzlich den GPS-Track dar. Siehe\n",
+    "[hier](https://geopandas.org/en/stable/docs/user_guide/interactive_mapping.html)\n",
+    "um mehrere GeoDataFrames in einer Karte darzustellen.*"
+   ],
+   "id": "0002-96cd7cef045240a7b1b0c57d7e374c0dd9716f4dc6f170b31077d61a462"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import geopandas as gpd\n",
+    "import zstandard\n",
+    "\n",
+    "cities_path = 'kreise-geo-mittel.geojson.zst'\n",
+    "with open(cities_path, 'rb') as fh:\n",
+    "    dctx = zstandard.ZstdDecompressor()\n",
+    "    with dctx.stream_reader(fh) as reader:\n",
+    "        grid = gpd.read_file(reader).set_index('ID_3')\n",
+    "\n",
+    "track_path = 'bike_track_2024-05-25.geojson.zst'\n",
+    "with open(track_path, 'rb') as fh:\n",
+    "    dctx = zstandard.ZstdDecompressor()\n",
+    "    with dctx.stream_reader(fh) as reader:\n",
+    "        track = gpd.read_file(reader)"
+   ],
+   "id": "0003-f5d67afb904d0f49edc541c7a33c18d0985cd0356b8747150b8687a8e18"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/10-geodaten/03-grid-transform.ipynb b/10-geodaten/03-grid-transform.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..1bc7ee0a96c14c2dc1a047bf8dfcdb9586b73690
--- /dev/null
+++ b/10-geodaten/03-grid-transform.ipynb
@@ -0,0 +1,107 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Gitter-Transformation\n",
+    "\n",
+    "Installieren Sie ggf. fehlende Pakete:"
+   ],
+   "id": "0001-9d10715da27eea0563e559964a2d386bf45e78b85abc9cb22a230f2e35c"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "!pip install wetterdienst"
+   ],
+   "id": "0002-2b71ddfdefd192f1b26f0e96bf6da9ba9144a57b5b0e0e4e81bbe24bb52"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "1.  Laden Sie die Wetterdaten mit dem vorbereiteten Code und schauen Sie\n",
+    "    sich die Daten grob an.\n",
+    "2.  Laden Sie das TK 25 Gitter (schon vorbereitet) und die Kreise (und\n",
+    "    kreisfreie Städte) analog.\n",
+    "3.  Interpolieren Sie die Temperaturen mit $k$-NN in einem der Gitter.\n",
+    "    Als Koordinaten können Sie die `centroid`-Koordinaten verwenden.\n",
+    "4.  Transformieren Sie die Daten in das andere Gitter."
+   ],
+   "id": "0003-d9bedf792849d3240f20100ae2d5727c2843229862be9f3803725199e01"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import geopandas as gpd\n",
+    "import pandas as pd\n",
+    "\n",
+    "import zstandard\n",
+    "from wetterdienst.provider.dwd.observation import (DwdObservationRequest,\n",
+    "                                                   DwdObservationResolution)\n",
+    "\n",
+    "\n",
+    "cache_path = '.'\n",
+    "\n",
+    "tk25_path = 'tk25-grid-utm32s.geojson.zst'\n",
+    "with open(tk25_path, 'rb') as fh:\n",
+    "    dctx = zstandard.ZstdDecompressor()\n",
+    "    with dctx.stream_reader(fh) as reader:\n",
+    "        grid = gpd.read_file(reader).set_index('id')\n",
+    "\n",
+    "\n",
+    "def retrieve_weatherdata(start_date=\"2005-01-01\", end_date=\"2023-08-31\", drop_cache=False):\n",
+    "    try:\n",
+    "        if drop_cache:\n",
+    "            raise FileNotFoundError('Dropping cache')\n",
+    "\n",
+    "        dwd_stations = pd.read_parquet(cache_path + f'/dwd_stations-{end_date}.parquet.zst')\n",
+    "        dwd_data = pd.read_parquet(cache_path + f'/dwd_data-{end_date}.parquet.zst')\n",
+    "    except FileNotFoundError:\n",
+    "        all_station_request = DwdObservationRequest(\n",
+    "            parameter=[\"temperature_air_mean_200\",  # monthly mean temperature 2 m above ground\n",
+    "                       \"precipitation_height\"],     # monthly sum of precipitation height\n",
+    "            resolution=DwdObservationResolution.MONTHLY,\n",
+    "            start_date=start_date,\n",
+    "            end_date=end_date,\n",
+    "        )\n",
+    "\n",
+    "        dwd_stations = all_station_request.all().df\n",
+    "        if not isinstance(dwd_stations, pd.DataFrame):\n",
+    "            # convert from polars to pandas\n",
+    "            dwd_stations = dwd_stations.to_pandas()\n",
+    "\n",
+    "        have_data = (dwd_stations.end_date > start_date) & (dwd_stations.start_date < end_date)\n",
+    "        dwd_stations = dwd_stations[have_data]\n",
+    "\n",
+    "        request = all_station_request.filter_by_station_id(dwd_stations.station_id.astype(int))\n",
+    "        dwd_data = request.values.all().df\n",
+    "        if not isinstance(dwd_data, pd.DataFrame):\n",
+    "            # convert from polars to pandas\n",
+    "            dwd_data = dwd_data.to_pandas()\n",
+    "\n",
+    "        dwd_data = dwd_data.dropna().drop(columns=['dataset', 'quality'])\n",
+    "\n",
+    "        dwd_stations.to_parquet(cache_path + f'/dwd_stations-{end_date}.parquet.zst', engine='pyarrow', compression='zstd')\n",
+    "        dwd_data.to_parquet(cache_path + f'/dwd_data-{end_date}.parquet.zst', engine='pyarrow', compression='zstd')\n",
+    "\n",
+    "    return dwd_stations, dwd_data"
+   ],
+   "id": "0004-bf8c1d8b475519dc7862b9af3394e1c98d67f29981feff16de470d18939"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/10-geodaten/bike_track_2024-05-25.geojson.zst b/10-geodaten/bike_track_2024-05-25.geojson.zst
new file mode 100644
index 0000000000000000000000000000000000000000..32110bdb886b54662905b995e9a54829724320f8
Binary files /dev/null and b/10-geodaten/bike_track_2024-05-25.geojson.zst differ
diff --git a/10-geodaten/dwd_data-2023-08-31.parquet.zst b/10-geodaten/dwd_data-2023-08-31.parquet.zst
new file mode 100644
index 0000000000000000000000000000000000000000..16d805fcb8cfc65b442aa6f07d86333570b52907
Binary files /dev/null and b/10-geodaten/dwd_data-2023-08-31.parquet.zst differ
diff --git a/10-geodaten/dwd_stations-2023-08-31.parquet.zst b/10-geodaten/dwd_stations-2023-08-31.parquet.zst
new file mode 100644
index 0000000000000000000000000000000000000000..9f90b0e8b812ee06a49baa14a86070f730b88bd7
Binary files /dev/null and b/10-geodaten/dwd_stations-2023-08-31.parquet.zst differ
diff --git a/10-geodaten/kreise-geo-mittel.geojson.zst b/10-geodaten/kreise-geo-mittel.geojson.zst
new file mode 100644
index 0000000000000000000000000000000000000000..5bbff1f46cc6cf5efb3cc51cc18a0bf4870087f8
Binary files /dev/null and b/10-geodaten/kreise-geo-mittel.geojson.zst differ
diff --git a/10-geodaten/tk25-grid-utm32s.geojson.zst b/10-geodaten/tk25-grid-utm32s.geojson.zst
new file mode 100644
index 0000000000000000000000000000000000000000..5e83a83fba45d7a68e5a5f816432bf58a1eafb2e
Binary files /dev/null and b/10-geodaten/tk25-grid-utm32s.geojson.zst differ