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": {} +}