{ "cells": [ { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "###### Data Privacy Enhancing Techniques\n", "

\n", "
\n", "

SC 4125: Developing Data Products

\n", "

Module-8: Data Governance/Privacy Issues


\n", "\n", " \n", "
\n", "by Anwitaman DATTA
\n", "School of Computer Science and Engineering, NTU Singapore. \n", "
" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### Teaching material\n", "- .pdf deck of slides (complements the html slides)\n", "- .html deck of slides\n", "- .ipynb Jupyter notebook" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Disclaimer/Caveat emptor\n", "\n", "- Non-systematic and non-exhaustive review\n", "- Illustrative approaches are not necessarily the most efficient or elegant, let alone unique\n", "- This Jupyter notebook is accompanied by a deck of slides discussing Data Governance in general, as well as specific privacy enhancing techniques." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Acknowledgement\n", "\n", "> This module is adapted from workshop training material created by Andreas Dewes and Katherine Jarmul from Kiprotect, which they have made available under MIT License. Here's an accompanying talk on Data Science Meets Data Protection. \n", ">\n", ">If anyone reuses the material in the current format as readapted and provided here, please still also attribute the original creators of the content.\n", ">\n", "> If there are any attribution omissions to be rectified, or should anything in the material need to be changed or redacted, the copyright owners are requested to contact me at anwitaman@ntu.edu.sg " ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "# k-Anonymity" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "External data (voter registration information) can be used to deanonymyze | 2-Anonymyzed patient data\n", ":-------------------------:|:-------------------------:\n", "\"bigpic\" | \"Sampling\"\n", "Example from Mondrian Multidimensional K-Anonymity by LeFevre et al" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# this is a list of the column names in our dataset (the file doesn't contain headers)\n", "names = (\n", " 'age',\n", " 'workclass', #Private, Self-emp-not-inc, Self-emp-inc, Federal-gov, Local-gov, State-gov, Without-pay, Never-worked.\n", " 'fnlwgt', # final weight. In other words, this is the number of people the census believes the entry represents.\n", " 'education',\n", " 'education-num',\n", " 'marital-status',\n", " 'occupation',\n", " 'relationship',\n", " 'race',\n", " 'sex',\n", " 'capital-gain',\n", " 'capital-loss',\n", " 'hours-per-week',\n", " 'native-country',\n", " 'income',\n", ")" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# some fields are categorical and thus they need to be treated accordingly\n", "# note that integers are used to represent some of the categorical data\n", "categorical = set((\n", " 'workclass',\n", " 'education',\n", " 'marital-status',\n", " 'occupation',\n", " 'relationship',\n", " 'sex',\n", " 'native-country',\n", " 'race',\n", " 'income',\n", "))" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(48842, 15)\n" ] }, { "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", " \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", " \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", " \n", " \n", " \n", " \n", "
ageworkclassfnlwgteducationeducation-nummarital-statusoccupationrelationshipracesexcapital-gaincapital-losshours-per-weeknative-countryincome
3851537Private285637HS-grad9Never-marriedTransport-movingNot-in-familyBlackMale0050United-States<=50k
1974457Private29375HS-grad9SeparatedSalesNot-in-familyAmer-Indian-EskimoFemale0035United-States<=50k
958419Private201743Some-college10Never-marriedOther-serviceOwn-childWhiteFemale0026United-States<=50k
1827323State-gov35633Some-college10Never-marriedOther-serviceNot-in-familyWhiteMale0050United-States<=50k
1483832Self-emp-inc161153HS-grad9Married-civ-spouseFarming-fishingHusbandWhiteMale0190255United-States>50k
1658544Private192381Bachelors13Married-civ-spouseProf-specialtyHusbandWhiteMale0184840United-States>50k
2257043Self-emp-inc602513Masters14Married-civ-spouseExec-managerialHusbandWhiteMale0050United-States>50k
2529769Federal-gov14384911th7WidowedAdm-clericalNot-in-familyWhiteFemale0020United-States<=50k
3994235Private351772Some-college10Married-civ-spouseSalesHusbandWhiteMale0060United-States>50k
443121Private174503HS-grad9Never-marriedAdm-clericalNot-in-familyWhiteFemale0030United-States<=50k
\n", "
" ], "text/plain": [ " age workclass fnlwgt education education-num \\\n", "38515 37 Private 285637 HS-grad 9 \n", "19744 57 Private 29375 HS-grad 9 \n", "9584 19 Private 201743 Some-college 10 \n", "18273 23 State-gov 35633 Some-college 10 \n", "14838 32 Self-emp-inc 161153 HS-grad 9 \n", "16585 44 Private 192381 Bachelors 13 \n", "22570 43 Self-emp-inc 602513 Masters 14 \n", "25297 69 Federal-gov 143849 11th 7 \n", "39942 35 Private 351772 Some-college 10 \n", "4431 21 Private 174503 HS-grad 9 \n", "\n", " marital-status occupation relationship \\\n", "38515 Never-married Transport-moving Not-in-family \n", "19744 Separated Sales Not-in-family \n", "9584 Never-married Other-service Own-child \n", "18273 Never-married Other-service Not-in-family \n", "14838 Married-civ-spouse Farming-fishing Husband \n", "16585 Married-civ-spouse Prof-specialty Husband \n", "22570 Married-civ-spouse Exec-managerial Husband \n", "25297 Widowed Adm-clerical Not-in-family \n", "39942 Married-civ-spouse Sales Husband \n", "4431 Never-married Adm-clerical Not-in-family \n", "\n", " race sex capital-gain capital-loss hours-per-week \\\n", "38515 Black Male 0 0 50 \n", "19744 Amer-Indian-Eskimo Female 0 0 35 \n", "9584 White Female 0 0 26 \n", "18273 White Male 0 0 50 \n", "14838 White Male 0 1902 55 \n", "16585 White Male 0 1848 40 \n", "22570 White Male 0 0 50 \n", "25297 White Female 0 0 20 \n", "39942 White Male 0 0 60 \n", "4431 White Female 0 0 30 \n", "\n", " native-country income \n", "38515 United-States <=50k \n", "19744 United-States <=50k \n", "9584 United-States <=50k \n", "18273 United-States <=50k \n", "14838 United-States >50k \n", "16585 United-States >50k \n", "22570 United-States >50k \n", "25297 United-States <=50k \n", "39942 United-States >50k \n", "4431 United-States <=50k " ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "datapath ='data/kiprotectdata/' # change this to adjust relative path\n", "df = pd.read_csv(datapath+\"adult.all.txt\", sep=\", \", header=None, names=names, index_col=False, engine='python')\n", "for name in categorical:\n", " df[name] = df[name].astype('category')\n", "print(df.shape)\n", "df.sample(10)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Greedy heuristic" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Helper function: spans of columns of a dataframe\n", "\n", "> We first need a function that returns the spans (max-min for numerical columns, number of different values for categorical columns) of all columns for a partition of a dataframe." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def get_spans(df, partition, scale=None):\n", " \"\"\"\n", " :param df: the dataframe for which to calculate the spans\n", " :param partition: the partition for which to calculate the spans\n", " :param scale: if given, the spans of each column will be divided\n", " by the value in `scale` for that column\n", " : returns: The spans of all columns in the partition\n", " \"\"\" \n", " spans = {}\n", " for column in df.columns:\n", " if column in categorical:\n", " span = len(df[column][partition].unique())\n", " else:\n", " span = df[column][partition].max()-df[column][partition].min()\n", " if scale is not None:\n", " span = span/scale[column]\n", " spans[column] = span\n", " return spans" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "text/plain": [ "{'age': 73,\n", " 'workclass': 9,\n", " 'fnlwgt': 1478115,\n", " 'education': 16,\n", " 'education-num': 15,\n", " 'marital-status': 7,\n", " 'occupation': 15,\n", " 'relationship': 6,\n", " 'race': 5,\n", " 'sex': 2,\n", " 'capital-gain': 99999,\n", " 'capital-loss': 4356,\n", " 'hours-per-week': 98,\n", " 'native-country': 42,\n", " 'income': 2}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "full_spans = get_spans(df, df.index)\n", "full_spans" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Helper function: split a dataframe in two partitions based on a `column`\n", "\n", "> partition the dataframe, returning two partitions such that\n", "> - all rows with values of a chosen/indicated column `column` below the median are in one partition \n", "> - all rows with values above or equal to the median are in the other\n", "> - for categorical data, divide them into two disjoint sets of (roughly) equal number of categories" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def split(df, partition, column):\n", " \"\"\"\n", " :param df: The dataframe to split\n", " :param partition: The partition to split\n", " :param column: The column along which to split\n", " : returns: A tuple containing a split of the original partition\n", " \"\"\"\n", " dfp = df[column][partition]\n", " if column in categorical:\n", " values = dfp.unique()\n", " lv = set(values[:len(values)//2])\n", " rv = set(values[len(values)//2:])\n", " return dfp.index[dfp.isin(lv)], dfp.index[dfp.isin(rv)]\n", " else: \n", " median = dfp.median()\n", " dfl = dfp.index[dfp < median]\n", " dfr = dfp.index[dfp >= median]\n", " return (dfl, dfr)" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Helper function: Determines if a specific partition has at least k entrees" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def is_k_anonymous(df, partition, sensitive_column, k=5):\n", " \"\"\"\n", " :param df: The dataframe on which to check the partition.\n", " :param partition: The partition of the dataframe to check.\n", " :param sensitive_column: The name of the sensitive column\n", " :param k: The desired k\n", " :returns : True if the partition is valid according to our k-anonymity criteria, False otherwise.\n", " \"\"\"\n", " if len(partition) < k:\n", " return False\n", " return True" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "### Greedy partitioning" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def partition_dataset(df, feature_columns, sensitive_column, scale, is_valid):\n", " \"\"\"\n", " :param df: The dataframe to be partitioned.\n", " :param feature_columns: A list of column names along which to partition the dataset.\n", " :param sensitive_column: The name of the sensitive column (to be passed on to the `is_valid` function)\n", " :param scale: The column spans as generated before.\n", " :param is_valid: A function that takes a dataframe and a partition and returns True if the partition is valid.\n", " :returns : A list of valid partitions that cover the entire dataframe.\n", " \"\"\"\n", " finished_partitions = []\n", " partitions = [df.index]\n", " while partitions:\n", " partition = partitions.pop(0)\n", " spans = get_spans(df[feature_columns], partition, scale)\n", " for column, span in sorted(spans.items(), key=lambda x:-x[1]):\n", " #we try to split this partition along a given column\n", " lp, rp = split(df, partition, column)\n", " if not is_valid(df, lp, sensitive_column) or not is_valid(df, rp, sensitive_column):\n", " continue\n", " # the split is valid, we put the new partitions on the list and continue\n", " partitions.extend((lp, rp))\n", " break\n", " else:\n", " # no split was possible, we add the partition to the finished partitions\n", " finished_partitions.append(partition)\n", " return finished_partitions" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "460\n" ] } ], "source": [ "# we apply the partitioning method to two columns of the dataset, using \"income\" as the sensitive attribute\n", "feature_columns = ['age', 'education-num']\n", "sensitive_column = 'income'\n", "finished_partitions = partition_dataset(df, feature_columns, sensitive_column, full_spans, is_k_anonymous)\n", "print(len(finished_partitions))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "subslide" } }, "source": [ "#### Visualizing the partitioning achieved" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "scrolled": false, "slideshow": { "slide_type": "fragment" } }, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "import matplotlib.patches as patches\n", "part_sizes=[len(x) for x in finished_partitions]\n", "plt.hist(part_sizes,bins=200,log=True)\n", "plt.ylabel('# of partitions (log)')\n", "plt.xlabel('size of partition');" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "slideshow": { "slide_type": "skip" } }, "outputs": [], "source": [ "# supporting functions and pre-computations for plotting 2D-partitions \n", "\n", "def build_indexes(df):\n", " indexes = {}\n", " for column in categorical:\n", " values = sorted(df[column].unique())\n", " indexes[column] = { x : y for x, y in zip(values, range(len(values)))}\n", " return indexes\n", "\n", "def get_coords(df, column, partition, indexes, offset=0.1):\n", " if column in categorical:\n", " sv = df[column][partition].sort_values()\n", " l, r = indexes[column][sv[sv.index[0]]], indexes[column][sv[sv.index[-1]]]+1.0\n", " else:\n", " sv = df[column][partition].sort_values()\n", " next_value = sv[sv.index[-1]]\n", " larger_values = df[df[column] > next_value][column]\n", " if len(larger_values) > 0:\n", " next_value = larger_values.min()\n", " l = sv[sv.index[0]]\n", " r = next_value\n", " # we add some offset to make the partitions more easily visible\n", " l -= offset\n", " r += offset\n", " return l, r\n", "\n", "def get_partition_rects(df, partitions, column_x, column_y, indexes, offsets=[0.1, 0.1]):\n", " rects = []\n", " for partition in partitions:\n", " xl, xr = get_coords(df, column_x, partition, indexes, offset=offsets[0])\n", " yl, yr = get_coords(df, column_y, partition, indexes, offset=offsets[1])\n", " rects.append(((xl, yl),(xr, yr)))\n", " return rects\n", "\n", "def get_bounds(df, column, indexes, offset=1.0):\n", " if column in categorical:\n", " return 0-offset, len(indexes[column])+offset\n", " return df[column].min()-offset, df[column].max()+offset\n", "\n", "def plot_rects(df, ax, rects, column_x, column_y, edgecolor='grey', facecolor='none'):\n", " for (xl, yl),(xr, yr) in rects:\n", " ax.add_patch(patches.Rectangle((xl,yl),xr-xl,yr-yl,linewidth=1,edgecolor=edgecolor,facecolor=facecolor, alpha=0.5))\n", " ax.set_xlim(*get_bounds(df, column_x, indexes))\n", " ax.set_ylim(*get_bounds(df, column_y, indexes))\n", " ax.set_xlabel(column_x)\n", " ax.set_ylabel(column_y)\n", " \n", "indexes = build_indexes(df)\n", "column_x, column_y = feature_columns[:2]\n", "rects = get_partition_rects(df, finished_partitions, column_x, column_y, indexes, offsets=[0.0, 0.0])" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Details of plotting function plot_rects has been skipped in the slide: Check the Jupyter notebook\n", "plt.figure(figsize=(15,8))\n", "ax = plt.subplot()\n", "plot_rects(df, ax, rects, column_x, column_y, facecolor='r')\n", "# we can also plot the datapoints themselves\n", "plt.scatter(df[column_x], df[column_y])" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "### Generating the k-anonymous dataset\n", "\n", "> So far we have identified the partition boundaries, along which to group the data items.\n", "> * We need to aggregate data within the partitions, to create the k-anonymous dataset." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "slideshow": { "slide_type": "fragment" } }, "outputs": [], "source": [ "def agg_categorical_column(series):\n", " return [','.join(set(series))]\n", "\n", "def agg_numerical_column(series):\n", " return [series.mean()]" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "slideshow": { "slide_type": "subslide" } }, "outputs": [], "source": [ "# Caution: I have tested it with other feature_columns choices and the code is brittle and it breaks.\n", "# It serves the pedagogical purpose, but you may have to fix/reimplement for general purpose use.\n", "def build_anonymized_dataset(df, partitions, feature_columns, sensitive_column, max_partitions=None):\n", " aggregations = {}\n", " for column in feature_columns:\n", " if column in categorical:\n", " aggregations[column] = agg_categorical_column\n", " else:\n", " aggregations[column] = agg_numerical_column \n", " rows = []\n", " for i, partition in enumerate(partitions):\n", " #if i % 100 == 1:\n", " # print(\"Finished {} partitions...\".format(i))\n", " if max_partitions is not None and i > max_partitions:\n", " break\n", " grouped_columns = df.loc[partition].agg(aggregations, squeeze=False)\n", " sensitive_counts = df.loc[partition].groupby(sensitive_column).agg({sensitive_column : 'count'})\n", " values = grouped_columns.iloc[0].to_dict()\n", " for sensitive_value, count in sensitive_counts[sensitive_column].items():\n", " if count == 0:\n", " continue\n", " values.update({\n", " sensitive_column : sensitive_value,\n", " 'count' : count,\n", " })\n", " rows.append(values.copy())\n", " \n", " return pd.DataFrame(rows)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "slideshow": { "slide_type": "subslide" } }, "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
ageeducation-numincomecount
60517.04.000000<=50k5
11017.05.000000<=50k36
11117.06.000000<=50k198
017.07.200599<=50k334
12017.09.000000<=50k14
...............
72690.09.000000>50k4
73690.010.545455<=50k9
73790.010.545455>50k2
77290.014.000000<=50k2
77390.014.000000>50k3
\n", "

776 rows × 4 columns

\n", "
" ], "text/plain": [ " age education-num income count\n", "605 17.0 4.000000 <=50k 5\n", "110 17.0 5.000000 <=50k 36\n", "111 17.0 6.000000 <=50k 198\n", "0 17.0 7.200599 <=50k 334\n", "120 17.0 9.000000 <=50k 14\n", ".. ... ... ... ...\n", "726 90.0 9.000000 >50k 4\n", "736 90.0 10.545455 <=50k 9\n", "737 90.0 10.545455 >50k 2\n", "772 90.0 14.000000 <=50k 2\n", "773 90.0 14.000000 >50k 3\n", "\n", "[776 rows x 4 columns]" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# we sort the resulting dataframe using the feature columns and the sensitive attribute\n", "dfn=build_anonymized_dataset(df, finished_partitions, feature_columns, sensitive_column)\\\n", " .sort_values(feature_columns+[sensitive_column])\n", "dfn" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "#### Ungraded task \n", "\n", "Ungraded Task 8.1.i: Generate k-anonymous, l-diverse dataset. \n", "\n", "Hint: You need minor modification to how/when partitioning is done.\n", "\n", "Ungraded Task 8.1.ii: For a dataset of your choice, train a ML model (of your choice) with the raw data versus the k-anonymyzed data and compare their performances (you may use default parameter choices, or try to optimize for each case). \n", "\n", "Note: When using k-anonymyzed data to train, the test data could still be the way the original \"raw\" data was. " ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "fragment" } }, "source": [ "Ungraded Task 8.2: Generate differentially private data from the census dataset, using the same set of features and sensitive columns. Benchmark the performance of models trained with different parameter choices, comparing with the model built using the original (non-noisy) data. \n", "\n", "Hint: There are only two categories for the sensitive (income) data." ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "![anon](pics/anon.png)" ] } ], "metadata": { "celltoolbar": "Slideshow", "kernelspec": { "display_name": "Python 3", "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.8.5" } }, "nbformat": 4, "nbformat_minor": 4 }