diff --git a/09-datenschutz/01-medi.ipynb b/09-datenschutz/01-medi.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..deaed9da7d0b79e3896ace545c1ae60a846850ec
--- /dev/null
+++ b/09-datenschutz/01-medi.ipynb
@@ -0,0 +1,407 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Medizindaten anonymisieren\n",
+    "\n",
+    "Dies stellt nur den Code zu den Folien bereit, die das Mini-Beispiel zur\n",
+    "$k$-Anonymisierung zeigen.\n",
+    "\n",
+    "## Personenbezogene Daten\n",
+    "\n",
+    "Wir starten aber mit den Daten:"
+   ],
+   "id": "0003-dee3387c12c6d1f91e4d3d5045948c33001f8302d6baf05ef61af4c6dc4"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "import numpy as np\n",
+    "import pandas as pd\n",
+    "from sklearn.cluster import KMeans\n",
+    "from sklearn.preprocessing import robust_scale\n",
+    "\n",
+    "columns = ['Name', 'Alter', 'Geschlecht', 'PLZ', 'Größe', 'Gewicht', 'Krankheit']\n",
+    "records = [\n",
+    "    ('Anna'     , 21, 'Weiblich', 76189, 165, 72, 'Grippe'),\n",
+    "    ('Louis'    , 35, 'Männlich', 77021, 170, 75, 'Krebs'),\n",
+    "    ('Holger'   , 39, 'Männlich', 63092, 160, 69, 'Haarausfall'),\n",
+    "    ('Frederic' , 23, 'Männlich', 63331, 167, 85, 'Muskelzerrung'),\n",
+    "    ('Anika'    , 24, 'Weiblich', 76121, 162, 70, 'Grippe'),\n",
+    "    ('Peter'    , 31, 'Männlich', 77462, 180, 81, 'Vergiftung'),\n",
+    "    ('Tobias'   , 38, 'Männlich', 77109, 175, 79, 'Demenz'),\n",
+    "    ('Charlotte', 19, 'Weiblich', 83133, 170, 68, 'Akne'),\n",
+    "    ('Sarah'    , 27, 'Weiblich', 89777, 165, 71, 'Akne'),\n",
+    "]\n",
+    "\n",
+    "df = pd.DataFrame.from_records(records, columns=columns)\n",
+    "df['Geschlecht'] = df['Geschlecht'].astype('category')\n",
+    "df"
+   ],
+   "id": "0004-7ef426945d280c8440bbe0ed26aac8ae84a363aa16b154e1277cf508e61"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Uneindeutige Werte\n",
+    "\n",
+    "Nun wählen wir geeignete Bereiche für jedes Merkmal, sodass hoffentlich\n",
+    "keine eindeutigen Werte mehr heraus kommen. Wir zielen hier auf $i = 2$:"
+   ],
+   "id": "0006-e4a6b7a2a6ccd44f368ae315d92902b5fa8de3e5f051eeb42c5498cfe35"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "dfi = df.drop(columns='Name')\n",
+    "dfi['PLZ'] = dfi['PLZ'] // 1000\n",
+    "dfi['Größe'] = pd.cut(dfi['Größe'], [159, 164, 169, 174, 200], labels=['160 – 164', '165 – 169', '170 – 174', '175 – 180'])\n",
+    "dfi['Gewicht'] = pd.cut(dfi['Gewicht'], [64, 69, 74, 79, 85], labels=['65 – 69', '70 – 74', '75 – 79', '80 – 85'])\n",
+    "dfi['Alter'] = pd.cut(dfi['Alter'], [0, 21, 30, 40], labels=['< 22', '22 – 30', '> 30'])\n",
+    "qids = ['Alter', 'Geschlecht', 'PLZ', 'Größe', 'Gewicht']"
+   ],
+   "id": "0007-386867d3d79e0deec0c6fc1b9fa194424379452dc0c086fe987e16cc1e6"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "und prüfen die Häufigkeiten für jedes Merkmal:"
+   ],
+   "id": "0008-05c91d6bb7f95eb4a538679b112f797e8bcf94eed053ddc85311239da40"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "Mindesthäufigkeiten der Werte für jedes Feature (i = 2):\n",
+      "Alter         2\n",
+      "Geschlecht    4\n",
+      "PLZ           1\n",
+      "Größe         2\n",
+      "Gewicht       2\n",
+      "dtype: int64"
+     ]
+    }
+   ],
+   "source": [
+    "print('Mindesthäufigkeiten der Werte für jedes Feature (i = 2):'); \\\n",
+    "print(dfi[qids].apply(lambda col:col.value_counts().min()))"
+   ],
+   "id": "0009-97d1dbc0b4fbc74367262d17987df25bfa2768a249288f7ac095cccab89"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hmm… die PLZ hat eine Mindesthäufigkeit von 1. Wir schauen, woran das\n",
+    "liegt:"
+   ],
+   "id": "0010-cce9a11cba1b2ad8fb05c761fd70af7cdf5b3a6927d291695b52d8e40f0"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "PLZ\n",
+      "77    3\n",
+      "76    2\n",
+      "63    2\n",
+      "83    1\n",
+      "89    1\n",
+      "Name: count, dtype: int64"
+     ]
+    }
+   ],
+   "source": [
+    "dfi['PLZ'].value_counts()"
+   ],
+   "id": "0011-3dc0b3f7a194d1d35a9b76000f1f7a5f88116194561bce476e8614cdf73"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "OK, wir bessern nach indem wir die PLZen, die mit 8 beginnen weiter\n",
+    "abschneiden:"
+   ],
+   "id": "0012-15f1791204b3e3b60671637844ac1d0742204b81fdce703f99431724496"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "PLZ\n",
+      "77    3\n",
+      "76    2\n",
+      "63    2\n",
+      "8     2\n",
+      "Name: count, dtype: int64"
+     ]
+    }
+   ],
+   "source": [
+    "plz_1d = dfi['PLZ'] // 10\n",
+    "dfi.loc[plz_1d == 8, 'PLZ'] = plz_1d[plz_1d == 8]\n",
+    "dfi['PLZ'].value_counts()"
+   ],
+   "id": "0013-dd1f0362dec9837cb9021ce4da7929123e7425f25b44f7116079d56076f"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Nun passt es. Der DataFrame sieht nun so aus:"
+   ],
+   "id": "0014-07cf3c336b349d34b1d672dab7e45e01599209f2ea393cc63fb8a595e81"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "     Alter Geschlecht  PLZ      Größe  Gewicht      Krankheit\n",
+      "0     < 22   Weiblich   76  165 – 169  70 – 74         Grippe\n",
+      "1     > 30   Männlich   77  170 – 174  75 – 79          Krebs\n",
+      "2     > 30   Männlich   63  160 – 164  65 – 69    Haarausfall\n",
+      "3  22 – 30   Männlich   63  165 – 169  80 – 85  Muskelzerrung\n",
+      "4  22 – 30   Weiblich   76  160 – 164  70 – 74         Grippe\n",
+      "5     > 30   Männlich   77  175 – 180  80 – 85     Vergiftung\n",
+      "6     > 30   Männlich   77  175 – 180  75 – 79         Demenz\n",
+      "7     < 22   Weiblich    8  170 – 174  65 – 69           Akne\n",
+      "8  22 – 30   Weiblich    8  165 – 169  70 – 74           Akne"
+     ]
+    }
+   ],
+   "source": [
+    "dfi"
+   ],
+   "id": "0015-1bd3f9992c275ed397a515e77b98b07ad16f732207fd88b9f05f4660ce2"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Die Kombinationen sind allerdings noch eindeutig:"
+   ],
+   "id": "0016-9a3e4040911dc8fbfeceeb683b5f7df49e98d6a03f8a83ff1415147d428"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "1"
+     ]
+    }
+   ],
+   "source": [
+    "dfi[qids].value_counts().min()"
+   ],
+   "id": "0017-aacadffcf8a75c69bddd41396fa48e1c5b6a5377d7bf18f3b331d9c0fd1"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## k-Anonymisierung\n",
+    "\n",
+    "Wir zielen auf $k = 2$"
+   ],
+   "id": "0019-5618810b7f82ebbabcbead0b185b007294bf6a193d007ba9bcb32fd16e9"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "dfk = df.drop(columns='Name')\n",
+    "X = dfk[qids].drop(columns='Geschlecht')\n",
+    "X_scaled = robust_scale(X)\n",
+    "\n",
+    "kmeans = KMeans(n_clusters=3, random_state=42, n_init=5)\n",
+    "labels = kmeans.fit_predict(X_scaled)"
+   ],
+   "id": "0020-dd714bb86a726ab44415ba3d2ec2b831ad856c015571b2fb327b2372189"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Wir schauen auf die kleinste Clustergröße (sollte größer oder gleich $k$\n",
+    "sein):"
+   ],
+   "id": "0021-3b265300550256206ee10db37a47a7fe8a26d00c9f4b889dcadce922865"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "2"
+     ]
+    }
+   ],
+   "source": [
+    "np.unique(labels, return_counts=True)[1].min()"
+   ],
+   "id": "0022-682a647ff3ced3a5853b905af6a11364b00d5a5b63ee2768b9b350eedc9"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Das passt. Dann können wir die Intervalle von min bis max jedes Clusters\n",
+    "erhalten und die ursprünglichen Daten ersetzen:"
+   ],
+   "id": "0023-118d58a6f1615a893016c0bd9d61fcc650c8ad9f5f446adc10d99f377f6"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "g = X.groupby(labels)\n",
+    "mins, maxs = g.transform('min'), g.transform('max')\n",
+    "mins['PLZ'] = mins['PLZ'] // 1000\n",
+    "maxs['PLZ'] = maxs['PLZ'] // 1000\n",
+    "\n",
+    "ints = mins.astype(str) + ' – ' + maxs.astype(str)\n",
+    "dfk[ints.columns] = ints"
+   ],
+   "id": "0024-df791042ec3a51c2569004a8edc2f32992946921bc7508459d5ef2ed4c8"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Jetzt sind die Kombinationen nicht mehr eindeutig:"
+   ],
+   "id": "0025-d5a10999a95091b501a47a23f0b385654ad1956eed656e5271545f482e1"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "2"
+     ]
+    }
+   ],
+   "source": [
+    "print(dfk[qids].value_counts().min())"
+   ],
+   "id": "0026-e9330560a49eb54da8ecf72439d755a0dc6591441a28f05b87d6ceb4d2d"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Der Datensatz sieht nun so aus:"
+   ],
+   "id": "0027-0cbd55253e5d55d5fc7b81b286c6931e98dd173db8c520e36e021eb0463"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [
+    {
+     "output_type": "stream",
+     "name": "stdout",
+     "text": [
+      "   Alter Geschlecht      PLZ  Größe  Gewicht      Krankheit\n",
+      "0   29.8   Weiblich  76780.4  170.4     75.4         Grippe\n",
+      "1   29.8   Männlich  76780.4  170.4     75.4          Krebs\n",
+      "2   31.0   Männlich  63211.5  163.5     77.0    Haarausfall\n",
+      "3   31.0   Männlich  63211.5  163.5     77.0  Muskelzerrung\n",
+      "4   29.8   Weiblich  76780.4  170.4     75.4         Grippe\n",
+      "5   29.8   Männlich  76780.4  170.4     75.4     Vergiftung\n",
+      "6   29.8   Männlich  76780.4  170.4     75.4         Demenz\n",
+      "7   23.0   Weiblich  86455.0  167.5     69.5           Akne\n",
+      "8   23.0   Weiblich  86455.0  167.5     69.5           Akne"
+     ]
+    }
+   ],
+   "source": [
+    "dfk"
+   ],
+   "id": "0028-9cf26794f86b8444987f2f06396085cd427fdb5dae8d7eb21c6b581a20a"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Alternativ kann man auch Mittelwerte statt Intervalle verwenden:"
+   ],
+   "id": "0029-e6815f3094ce50cde54681a281519fe62647aba791b2637484b16fe1968"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "means = g.transform('mean')\n",
+    "dfk[means.columns] = means\n",
+    "dfk"
+   ],
+   "id": "0030-f123500ca3c9aad864acf5d88c4fa1183608f14199af1246255029c2a7e"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}
diff --git a/09-datenschutz/01-bookcrossing-sol.ipynb b/09-datenschutz/02-bookcrossing-sol.ipynb
similarity index 97%
rename from 09-datenschutz/01-bookcrossing-sol.ipynb
rename to 09-datenschutz/02-bookcrossing-sol.ipynb
index a81aeaef50963e3ed6bd9d2038ebb85698981b0e..929314432fd67ecd7cc200d38039819b81558074 100644
--- a/09-datenschutz/01-bookcrossing-sol.ipynb
+++ b/09-datenschutz/02-bookcrossing-sol.ipynb
@@ -16,11 +16,11 @@
     "Er stammt von – einer Community um Bücher weiterzugeben und zu tracken.\n",
     "Dabei können Bücher auch bewertet werden.\n",
     "\n",
-    "*Hinweis: Die Bewertungen haben eine skala von 1 – 10. Eine 0 meint\n",
-    "*implizite Bewertung*, also es wurde keine Bewertung abgegeben. Der\n",
-    "Begriff *implizite Bewertung\\* steht dafür, dass man aus anderen Daten\n",
+    "*Hinweis: Die Bewertungen haben eine Skala von 1 – 10. Eine 0 meint\n",
+    "**implizite Bewertung**, also es wurde keine Bewertung abgegeben. Der\n",
+    "Begriff **implizite Bewertung** steht dafür, dass man aus anderen Daten\n",
     "eine Bewertung erahnen kann – hier z. B., dass es überhaupt gelesen\n",
-    "wurde.\\*\n",
+    "wurde.*\n",
     "\n",
     "Laden Sie die Daten, säubern sie ein bisschen und versuchen Sie zwei\n",
     "Fragen zu beantworten:\n",
@@ -69,7 +69,7 @@
     "einige Werte nach 2004, obwohl der Datensatz 2004 heraus kam. Diese\n",
     "setzen wir auf NaN."
    ],
-   "id": "0014-5250011056ff5724f9be1f73922489ac25747bd60219961556539182e36"
+   "id": "0014-2acead481ecc6c0d537ae8febd1f3a0d4664a97dccfbc92444c634eb396"
   },
   {
    "cell_type": "code",
diff --git a/09-datenschutz/01-bookcrossing.ipynb b/09-datenschutz/02-bookcrossing.ipynb
similarity index 86%
rename from 09-datenschutz/01-bookcrossing.ipynb
rename to 09-datenschutz/02-bookcrossing.ipynb
index 79a9bad3efe452414bb6a32aac847e36445c2b76..7f2314e25f47f10af1e61cc80d3d076e551f0687 100644
--- a/09-datenschutz/01-bookcrossing.ipynb
+++ b/09-datenschutz/02-bookcrossing.ipynb
@@ -16,11 +16,11 @@
     "Er stammt von – einer Community um Bücher weiterzugeben und zu tracken.\n",
     "Dabei können Bücher auch bewertet werden.\n",
     "\n",
-    "*Hinweis: Die Bewertungen haben eine skala von 1 – 10. Eine 0 meint\n",
-    "*implizite Bewertung*, also es wurde keine Bewertung abgegeben. Der\n",
-    "Begriff *implizite Bewertung\\* steht dafür, dass man aus anderen Daten\n",
+    "*Hinweis: Die Bewertungen haben eine Skala von 1 – 10. Eine 0 meint\n",
+    "**implizite Bewertung**, also es wurde keine Bewertung abgegeben. Der\n",
+    "Begriff **implizite Bewertung** steht dafür, dass man aus anderen Daten\n",
     "eine Bewertung erahnen kann – hier z. B., dass es überhaupt gelesen\n",
-    "wurde.\\*\n",
+    "wurde.*\n",
     "\n",
     "Laden Sie die Daten, säubern sie ein bisschen und versuchen Sie zwei\n",
     "Fragen zu beantworten:\n",
@@ -40,7 +40,7 @@
     "\n",
     "Hier Ihr Code:"
    ],
-   "id": "0007-0eae7603b6d19844fc278f91526d3cdaf0b09213b80d7564d3b1c6d164d"
+   "id": "0007-ef5ca2d1fed4295c92f0dde89f1e3844ce22c8da501ff51f16b0c78b514"
   },
   {
    "cell_type": "code",
diff --git a/10-geodaten/01-projections.ipynb b/10-geodaten/01-projections.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..e2afce5db79922c76b393541cc140976d15a4c13
--- /dev/null
+++ b/10-geodaten/01-projections.ipynb
@@ -0,0 +1,82 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Flächenverzerrung bei Projektionen\n",
+    "\n",
+    "In dieser Demo kann man sich die Flächenverzerrungen bei verschiedenen\n",
+    "Projektionen auf einer interaktiven Karte anschauen.\n",
+    "\n",
+    "Zunächst laden wir eine GeoJSON-Datei mit groben Ländergrenzen herunter\n",
+    "und speichern sie lokal. Und wir laden auch eine mehr oder weniger gute\n",
+    "Datei mit den Referenzgrößen. Mit etwas Vorverarbeitung können wir die\n",
+    "Datensätze in `countries` zusammenführen, was wir aber erst im nächsten\n",
+    "Schritt machen."
+   ],
+   "id": "0002-a8f53080980ed874d387cc3bfb8c5d5ed65d7fdb4139eb4ed15105d383c"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "from sklearn.datasets import fetch_openml\n",
+    "import geopandas as gpd\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",
+    "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()"
+   ],
+   "id": "0003-4c6fa2e8089a613e6a3300cf4bd6fe01d88ed16280b83e1e378065b75a9"
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Hier kann ein EPSG-Code gewählt werden, der die Projektion definiert.\n",
+    "Damit bestimmt sich wie groß die projizierte Fläche ist."
+   ],
+   "id": "0004-5231db90b71a4c73fd2da36a3ca092775410c9eb4e21d28e7de691474ef"
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "style": "python"
+   },
+   "outputs": [],
+   "source": [
+    "countries = geo_countries.merge(countries_df, on='name')\n",
+    "\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",
+    "countries = countries.to_crs(f'EPSG:{epsg}')\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",
+    "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)"
+   ],
+   "id": "0005-b64aa750773482384137dd8fa7e5b5b069b9c692b3395c5c211650bf153"
+  }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+}