{ "cells": [ { "cell_type": "markdown", "id": "3f18d338", "metadata": {}, "source": [ "# Lomas: Client demo" ] }, { "cell_type": "markdown", "id": "1582a2ae", "metadata": {}, "source": [ "This notebook showcases how researcher could use the Lomas platform. It explains the different functionnalities provided by the `lomas-client` library to interact with the secure server.\n", "\n", "The secure data are never visible by researchers. They can only access to differentially private responses via queries to the server.\n", "\n", "Each user has access to one or multiple projects and for each dataset has a limited budget with $\\epsilon$ and $\\delta$ values." ] }, { "cell_type": "code", "execution_count": 1, "id": "23bb4f13-7800-41b2-b429-68c2d02243d0", "metadata": {}, "outputs": [ { "data": { "image/png": "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QjugkjZv2mDHrNOMgT+UnLABAFCRvLydTIvWbUyLVm1No11q9mAPSIbQDogeQjsgvbI5tKN7LJAiVVSsXrHEjlO0quQvgR0AIFXbt+s7ZCnfIQAAAEgZoR2QgmLXFXbR/Flm00YqQgEAVaNq7cUl3yUL504v+W5Z55YCAAAA5RHaAUl89923ZvmSBfbkau3qFW4pAADVs27NSlt1p++Y77791i0FAAAAyhDaAQloNlidUGl2WM0SCwBAOpXNQD6dGcgBAABQDqEdELBl8yazZOEc2x22aP0atxQAgMwoWr/W/kik7x59BwEAAABCaAf4aEZYdYVduWyR+X7bNrcUAIDM+v77bfa7R99B+i4CAAAACO0AAAAAAACAiCG0A3xatGprOnTpaVq33c3UqVPXLQUAILP0naPvng5detjvIgAAAIDQDgho0LCRad+hqz1xKmja3C0FACAzCpo0t985+u5p0HAXtxQAAAC1HaEdkEDT5q1Mh849TNvCzqZe/QZuKQAA6VGvXv2S75hONrDTdw4AAADgR2gHJLFzyQlVm/YdbXjXrEVrtxQAgOrRd4rCujbtO9nvGgAAACCI0A5IQX6TZvbkqrDj7qZho8ZuKQAAlaPur+1Lvkt269yj5LuFIRgAAACQGKEdkKKddqpjWrUptOFdy9btTF5enrsFAICKtWityY56mNYl3yV16tRxSwEAAIBwedtLuHZS/oCiqKjItdJr8cpi1wKi75v1a8zi+bPMt1u3uCWJ7dK4wOTtlDgjr79z5k/eJk6c6FrGHHrooa6V3caNG+daxgwcONC1AKBytn73vWtVzvYffjAbN1R8TKTur7t16maaNGvplgDRUdg637VqRkFBgWuVfIZSOw0Bap3i4vScF+dveNK1gNqtuPEw16q+/Pya/d4ktAOqYeuWzWbNqmVmzcplZtu279zS8nru1S9pt9qaOGDOxYPkmtgvAch9VT3+0I820z77wF0rr06dura6rkWrtnZmciCKCO2A6CG0A9Irm0M7uscC1VC/QUPTbrcutrtTk2Yt3FIAQG1X0LS5/W5o36ErgR0AAACqhNAOSAN1edIMswrwFOQBAGqnevUbmLaFne13QtPmrdxSAAAAoPII7YA0qbtzPbNruw72RK15y13dUgBAbdGsRWv7HdCmfUc7jh0AAABQHYR2QJo1LmhqOnbdw17q1t3ZLtO4RgCA3KR9vLrCdtp9T5PfpJlbCgAAAFQPoR2QIaq222Of/U3zVm3cEgBArlEXWO3rNdkEAAAAkE6EdkAGqdKuY5eedowjAEBu0b69c7deZued67klAAAAQPoQ2gEAAAAAAAARQ2gHAAAAAAAARAyhHQAAAAAAABAxhHYAAAAAAABAxBDaAQAAAAAAABFDaAcAAAAAAABEDKEdAAAAAAAAEDGEdgAAAAAAAEDEENoBAAAAAAAAEUNoBwAAAAAAAEQMoR0AAAAAAAAQMYR2AAAAAAAAQMQQ2gEAAAAAAAARQ2gHAAAAAAAARAyhHQAAAAAAABAxhHYAAAAAAABAxBDaAQAAAAAAABFDaAcAAAAAAABEDKEdAAAAAAAAEDGEdgAAAAAAAEDEENoBAAAAAAAAEUNoBwAAAAAAAEQMoR0AAAAAAAAQMYR2AAAAAAAAQMTkbS/h2knl5eW5ljFFRUWuBSBbFBQUuJYxKX7sI4/9EgAA2SUXj0eAdCsuLnYtAFGTn5/vWjWDSjsAAAAAAAAgYgjtAAAAAAAAgIghtAMAAAAAAAAihtAOAAAAAAAAiBhCOwAAAAAAACBiCO0AAAAAAACAiCG0AwAAAAAAACKG0A4AAAAAAACIGEI7AAAAAAAAIGII7QAAAAAAAICIIbQDAAAAAAAAIobQDgAAAAAAAIgYQjsAAAAAAAAgYgjtAAAAAAAAgIghtAMAAAAAAAAihtAOAAAAAAAAiBhCOwAAAAAAACBiCO0AAAAAAACAiCG0AwAAAAAAACKG0A4AAAAAAACIGEI7AAAAAAAAIGII7QAAAAAAAICIIbQDAAAAAAAAIobQDgAAAAAAAIgYQjsAAAAAAAAgYgjtAAAAAAAAgIjJ217CtZPKy8tzLQDZLsWPfeSxXwIAIHvlyvEIkG7FxcWuBSBq8vPzXatmENoBtRChHQAA2NEI7QAASC7l7rHnnnuuawHIZrn0WWa/BABAduI7HACAiqVcaQcAAAAAAACgZjARBQAAAAAAABAxhHYAAAAAAABAxBDaAQAAAAAAABFDaAcAAAAAAABEDKEdAAAAAAAAEDGEdgAAAAAAAEDEENoBAAAAAAAAEUNoBwAAAAAAAEQMoR0AAAAAAAAQMYR2AAAAAAAAQMQQ2gEAAAAAAAARQ2gHAAAAAAAARAyhHQAAAAAAABAxhHYAAAAAAABApBjz/9UAdGV1iSE7AAAAAElFTkSuQmCC", "text/plain": [ "" ] }, "execution_count": 1, "metadata": { "image/png": { "width": 800 } }, "output_type": "execute_result" } ], "source": [ "from IPython.display import Image\n", "Image(filename=\"images/image_demo_client.png\", width=800)" ] }, { "cell_type": "markdown", "id": "5b73135c", "metadata": {}, "source": [ "🐧🐧🐧\n", "In this notebook the researcher is a penguin researcher named Dr. Antarctica. She aims to do a grounbdbreaking research on various penguins dimensions.\n", "\n", "Therefore, the powerful queen Icerbegina 👑 had the data collected. But in order to get the penguins to agree to participate she promised them that no one would be able to look at the data and that no one would be able to guess the bill width of any specific penguin (which is very sensitive information) from the data. Nobody! Not even the researchers. The queen hence stored the data on the Secure Data Disclosure Server and only gave a small budget to Dr. Antarctica.\n", "\n", "This is not a problem for Dr. Antarctica as she does not need to see the data to make statistics thanks to the Secure Data Disclosure Client library `lomas-client`. \n", "🐧🐧🐧" ] }, { "cell_type": "markdown", "id": "01ae30d2", "metadata": {}, "source": [ "## Step 1: Install the library\n", "To interact with the secure server on which the data is stored, Dr.Antartica first needs to install the library `lomas-client` on her local developping environment. \n", "\n", "It can be installed via the pip command:" ] }, { "cell_type": "code", "execution_count": 31, "id": "28fbdd79-8c15-49a9-bcf9-fcdeac09d2b5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: lomas-client in /usr/local/lib/python3.12/site-packages (0.3.3)\n", "Requirement already satisfied: diffprivlib>=0.6.4 in /usr/local/lib/python3.12/site-packages (from lomas-client) (0.6.4)\n", "Requirement already satisfied: diffprivlib-logger>=0.0.3 in /usr/local/lib/python3.12/site-packages (from lomas-client) (0.0.3)\n", "Requirement already satisfied: numpy>=1.26.2 in /usr/local/lib/python3.12/site-packages (from lomas-client) (1.26.2)\n", "Requirement already satisfied: opendp==0.10.0 in /usr/local/lib/python3.12/site-packages (from lomas-client) (0.10.0)\n", "Requirement already satisfied: opendp-logger==0.3.0 in /usr/local/lib/python3.12/site-packages (from lomas-client) (0.3.0)\n", "Requirement already satisfied: pandas>=2.2.2 in /usr/local/lib/python3.12/site-packages (from lomas-client) (2.2.2)\n", "Requirement already satisfied: requests>=2.32.0 in /usr/local/lib/python3.12/site-packages (from lomas-client) (2.32.0)\n", "Requirement already satisfied: scikit-learn==1.4.0 in /usr/local/lib/python3.12/site-packages (from lomas-client) (1.4.0)\n", "Requirement already satisfied: smartnoise-synth==1.0.4 in /usr/local/lib/python3.12/site-packages (from lomas-client) (1.0.4)\n", "Requirement already satisfied: smartnoise-synth-logger==0.0.3 in /usr/local/lib/python3.12/site-packages (from lomas-client) (0.0.3)\n", "Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.12/site-packages (from scikit-learn==1.4.0->lomas-client) (1.14.1)\n", "Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.12/site-packages (from scikit-learn==1.4.0->lomas-client) (1.4.2)\n", "Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.12/site-packages (from scikit-learn==1.4.0->lomas-client) (3.5.0)\n", "Requirement already satisfied: Faker>=17.0.0 in /usr/local/lib/python3.12/site-packages (from smartnoise-synth==1.0.4->lomas-client) (30.6.0)\n", "Requirement already satisfied: opacus<0.15.0,>=0.14.0 in /usr/local/lib/python3.12/site-packages (from smartnoise-synth==1.0.4->lomas-client) (0.14.0)\n", "Requirement already satisfied: pac-synth<0.0.9,>=0.0.8 in /usr/local/lib/python3.12/site-packages (from smartnoise-synth==1.0.4->lomas-client) (0.0.8)\n", "Requirement already satisfied: smartnoise-sql<2.0.0,>=1.0.4 in /usr/local/lib/python3.12/site-packages (from smartnoise-synth==1.0.4->lomas-client) (1.0.4)\n", "Requirement already satisfied: torch>=2.2.0 in /usr/local/lib/python3.12/site-packages (from smartnoise-synth==1.0.4->lomas-client) (2.4.1)\n", "Requirement already satisfied: setuptools>=49.0.0 in /usr/local/lib/python3.12/site-packages (from diffprivlib>=0.6.4->lomas-client) (75.2.0)\n", "Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/site-packages (from pandas>=2.2.2->lomas-client) (2.9.0.post0)\n", "Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/site-packages (from pandas>=2.2.2->lomas-client) (2024.2)\n", "Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/site-packages (from pandas>=2.2.2->lomas-client) (2024.2)\n", "Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.12/site-packages (from requests>=2.32.0->lomas-client) (3.4.0)\n", "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/site-packages (from requests>=2.32.0->lomas-client) (3.10)\n", "Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/site-packages (from requests>=2.32.0->lomas-client) (2.2.3)\n", "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/site-packages (from requests>=2.32.0->lomas-client) (2024.8.30)\n", "Requirement already satisfied: typing-extensions in /usr/local/lib/python3.12/site-packages (from Faker>=17.0.0->smartnoise-synth==1.0.4->lomas-client) (4.12.2)\n", "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas>=2.2.2->lomas-client) (1.16.0)\n", "Requirement already satisfied: PyYAML<7.0.0,>=6.0.1 in /usr/local/lib/python3.12/site-packages (from smartnoise-sql<2.0.0,>=1.0.4->smartnoise-synth==1.0.4->lomas-client) (6.0.2)\n", "Requirement already satisfied: antlr4-python3-runtime==4.9.3 in /usr/local/lib/python3.12/site-packages (from smartnoise-sql<2.0.0,>=1.0.4->smartnoise-synth==1.0.4->lomas-client) (4.9.3)\n", "Requirement already satisfied: graphviz<0.18,>=0.17 in /usr/local/lib/python3.12/site-packages (from smartnoise-sql<2.0.0,>=1.0.4->smartnoise-synth==1.0.4->lomas-client) (0.17)\n", "Requirement already satisfied: sqlalchemy<3.0.0,>=2.0.0 in /usr/local/lib/python3.12/site-packages (from smartnoise-sql<2.0.0,>=1.0.4->smartnoise-synth==1.0.4->lomas-client) (2.0.36)\n", "Requirement already satisfied: filelock in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (3.16.1)\n", "Requirement already satisfied: sympy in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (1.13.3)\n", "Requirement already satisfied: networkx in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (3.4.1)\n", "Requirement already satisfied: jinja2 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (3.1.4)\n", "Requirement already satisfied: fsspec in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (2024.9.0)\n", "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.1.105 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.1.105)\n", "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.1.105 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.1.105)\n", "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.1.105 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.1.105)\n", "Requirement already satisfied: nvidia-cudnn-cu12==9.1.0.70 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (9.1.0.70)\n", "Requirement already satisfied: nvidia-cublas-cu12==12.1.3.1 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.1.3.1)\n", "Requirement already satisfied: nvidia-cufft-cu12==11.0.2.54 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (11.0.2.54)\n", "Requirement already satisfied: nvidia-curand-cu12==10.3.2.106 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (10.3.2.106)\n", "Requirement already satisfied: nvidia-cusolver-cu12==11.4.5.107 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (11.4.5.107)\n", "Requirement already satisfied: nvidia-cusparse-cu12==12.1.0.106 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.1.0.106)\n", "Requirement already satisfied: nvidia-nccl-cu12==2.20.5 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (2.20.5)\n", "Requirement already satisfied: nvidia-nvtx-cu12==12.1.105 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.1.105)\n", "Requirement already satisfied: triton==3.0.0 in /usr/local/lib/python3.12/site-packages (from torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (3.0.0)\n", "Requirement already satisfied: nvidia-nvjitlink-cu12 in /usr/local/lib/python3.12/site-packages (from nvidia-cusolver-cu12==11.4.5.107->torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (12.6.77)\n", "Requirement already satisfied: greenlet!=0.4.17 in /usr/local/lib/python3.12/site-packages (from sqlalchemy<3.0.0,>=2.0.0->smartnoise-sql<2.0.0,>=1.0.4->smartnoise-synth==1.0.4->lomas-client) (3.1.1)\n", "Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.12/site-packages (from jinja2->torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (3.0.1)\n", "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /usr/local/lib/python3.12/site-packages (from sympy->torch>=2.2.0->smartnoise-synth==1.0.4->lomas-client) (1.3.0)\n", "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable.It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.\u001b[0m\u001b[33m\n", "\u001b[0m" ] } ], "source": [ "!pip install lomas-client" ] }, { "cell_type": "code", "execution_count": 2, "id": "6fb569fc", "metadata": {}, "outputs": [], "source": [ "from lomas_client import Client\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "9c63718b", "metadata": {}, "source": [ "## Step 2: Initialise the client\n", "\n", "Once the library is installed, a Client object must be created. It is responsible for sending sending requests to the server and processing responses in the local environment. It enables a seamless interaction with the server. \n", "\n", "To create the client, Dr. Antartica needs to give it a few parameters:\n", "- a url: the root application endpoint to the remote secure server.\n", "- user_name: her name as registered in the database (Dr. Alice Antartica)\n", "- dataset_name: the name of the dataset that she wants to query (PENGUIN)\n", "\n", "She will only be able to query on the real dataset if the queen Icergina has previously made her an account in the database, given her access to the PENGUIN dataset and has given her some epsilon and delta credit (as is done in the Admin Notebook for Users and Datasets management)." ] }, { "cell_type": "code", "execution_count": 3, "id": "941991f7", "metadata": {}, "outputs": [], "source": [ "APP_URL = \"http://lomas_server\"\n", "USER_NAME = \"Dr. Antartica\"\n", "DATASET_NAME = \"PENGUIN\"\n", "client = Client(url=APP_URL, user_name = USER_NAME, dataset_name = DATASET_NAME)" ] }, { "cell_type": "markdown", "id": "0ec400c8", "metadata": {}, "source": [ "And that's it for the preparation. She is now ready to use the various functionnalities offered by `lomas_client`." ] }, { "cell_type": "markdown", "id": "9b9a5f13", "metadata": {}, "source": [ "## Step 3: Understand the functionnalities of the library" ] }, { "cell_type": "markdown", "id": "c7cb5531", "metadata": {}, "source": [ "### a. Getting dataset metadata\n", "\n", "Dr. Antartica has never seen the data and as a first step to understand what is available to her, she would like to check the metadata of the dataset. Therefore, she just needs to call the `get_dataset_metadata()` function of the client. As this is public information, this does not cost any budget.\n", "\n", "This function returns metadata information in a format based on [SmartnoiseSQL dictionary format](https://docs.smartnoise.org/sql/metadata.html#dictionary-format), where among other, there is information about all the available columns, their type, bound values (see Smartnoise page for more details). Any metadata is required for Smartnoise-SQL is also required here and additional information such that the different categories in a string type column column can be added." ] }, { "cell_type": "code", "execution_count": 4, "id": "0fdebac9-57fc-4410-878b-5a77425af634", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'max_ids': 1,\n", " 'rows': 344,\n", " 'row_privacy': True,\n", " 'censor_dims': False,\n", " 'columns': {'species': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'string',\n", " 'cardinality': 3,\n", " 'categories': ['Adelie', 'Chinstrap', 'Gentoo']},\n", " 'island': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'string',\n", " 'cardinality': 3,\n", " 'categories': ['Torgersen', 'Biscoe', 'Dream']},\n", " 'bill_length_mm': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 30.0,\n", " 'upper': 65.0},\n", " 'bill_depth_mm': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 13.0,\n", " 'upper': 23.0},\n", " 'flipper_length_mm': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 150.0,\n", " 'upper': 250.0},\n", " 'body_mass_g': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 2000.0,\n", " 'upper': 7000.0},\n", " 'sex': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'string',\n", " 'cardinality': 2,\n", " 'categories': ['MALE', 'FEMALE']}}}" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "penguin_metadata = client.get_dataset_metadata()\n", "penguin_metadata" ] }, { "cell_type": "markdown", "id": "d338ed96", "metadata": {}, "source": [ "Based on this Dr. Antartica knows that there are 7 columns, 3 of string type (species, island, sex) with their associated categories (i.e. the species column has 3 possibilities: 'Adelie', 'Chinstrap', 'Gentoo') and 4 of float type (bill length, bill depth, flipper length and body mass) with their associated bounds (i.e. the body mass of penguin ranges from 2000 to 7000 gramms). She also knows based on the field `max_ids: 1` that each penguin can only be once in the dataset and on the field `row_privacy: True` that each row represents a single penguin. Finally, she learns that there are 344 rows in the dataset and hence 344 penguins." ] }, { "cell_type": "code", "execution_count": 5, "id": "8719c070-16a3-4228-a09f-944178aa1ba7", "metadata": {}, "outputs": [], "source": [ "NB_PENGUINS = penguin_metadata[\"rows\"]" ] }, { "cell_type": "markdown", "id": "5a3c899d", "metadata": {}, "source": [ "### b. Get a dummy dataset\n", "\n", "Now, that she has seen and understood the metadata, she wants to get an even better understanding of the dataset (but is still not able to see it). A solution to have an idea of what the dataset looks like it to create a dummy dataset. \n", "\n", "Based on the public metadata of the dataset, a random dataframe can be created created. By default, there will be 100 rows and the seed is set to 42 to ensure reproducibility, but these 2 variables can be changed to obtain different dummy datasets.\n", "Getting a dummy dataset does not affect the budget as there is no differential privacy here. It is not a synthetic dataset and all that could be learn here is already present in the public metadata (it is created randomly on the fly based on the metadata).\n", "\n", "Dr. Antartica first create a dummy dataset with 100 rows and chooses a seed of 0." ] }, { "cell_type": "code", "execution_count": 6, "id": "01f4365a", "metadata": {}, "outputs": [], "source": [ "NB_ROWS = 100\n", "SEED = 0" ] }, { "cell_type": "code", "execution_count": 7, "id": "3f553b29", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(100, 7)\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", "
speciesislandbill_length_mmbill_depth_mmflipper_length_mmbody_mass_gsex
0GentooBiscoe46.79957716.196816239.6801233010.840470FEMALE
1ChinstrapDream38.13305214.875077208.3320056689.525543MALE
2ChinstrapTorgersen58.06582019.725266154.0218222473.883392MALE
3AdelieTorgersen62.32355614.951074221.1486822024.497075FEMALE
4AdelieDream39.31456018.776879206.9025853614.604018MALE
\n", "
" ], "text/plain": [ " species island bill_length_mm bill_depth_mm flipper_length_mm \\\n", "0 Gentoo Biscoe 46.799577 16.196816 239.680123 \n", "1 Chinstrap Dream 38.133052 14.875077 208.332005 \n", "2 Chinstrap Torgersen 58.065820 19.725266 154.021822 \n", "3 Adelie Torgersen 62.323556 14.951074 221.148682 \n", "4 Adelie Dream 39.314560 18.776879 206.902585 \n", "\n", " body_mass_g sex \n", "0 3010.840470 FEMALE \n", "1 6689.525543 MALE \n", "2 2473.883392 MALE \n", "3 2024.497075 FEMALE \n", "4 3614.604018 MALE " ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df_dummy = client.get_dummy_dataset(\n", " nb_rows = NB_ROWS, \n", " seed = SEED\n", ")\n", "\n", "print(df_dummy.shape)\n", "df_dummy.head()" ] }, { "cell_type": "markdown", "id": "bb03bf8f-62b7-4929-8757-e662eee7de41", "metadata": {}, "source": [ "### c. Check privacy loss budget ε, δ (initial, current, remaining)" ] }, { "cell_type": "markdown", "id": "d4d5b481-0cf9-4de9-86b9-158ac413af6b", "metadata": {}, "source": [ "It is the first time that Dr. Antartica connects to the server and she wants to know how much buget has beeen assigned to her.\n", "Therefore, she calls the fonction `get_initial_budget`." ] }, { "cell_type": "code", "execution_count": 8, "id": "9bd99db9-9de9-4b25-8718-989fea27b15a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "InitialBudgetResponse(initial_epsilon=10.0, initial_delta=0.005)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.get_initial_budget()" ] }, { "cell_type": "markdown", "id": "b48a8f93-8be9-4b9d-a6e8-8811d749650d", "metadata": {}, "source": [ "She sees that she has 10.0 epsilon and 0.005 epsilon at her disposal.\n", "\n", "Then she checks her total spent budget `get_total_spent_budget`. As she only did queries on metadata on dummy dataframes, this should still be 0." ] }, { "cell_type": "code", "execution_count": 9, "id": "99a4dd26-53af-412e-bcd1-f06fff57e6a4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "SpentBudgetResponse(total_spent_epsilon=1.0, total_spent_delta=4.999999999999449e-05)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.get_total_spent_budget()" ] }, { "cell_type": "markdown", "id": "c4c7d708-90f9-4bdf-a93e-ea3007609b62", "metadata": {}, "source": [ "It will also be useful to know what the remaining budget is. Therefore, she calls the function `get_remaining_budget`. It just substarcts the total spent budget from the initial budget." ] }, { "cell_type": "code", "execution_count": 10, "id": "f67e0596-5f96-4c8b-a843-3fbaef02bab1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "RemainingBudgetResponse(remaining_epsilon=9.0, remaining_delta=0.004950000000000006)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.get_remaining_budget()" ] }, { "cell_type": "markdown", "id": "1ac73b0b-f9db-4513-af13-0bb2f75f8977", "metadata": {}, "source": [ "As expected, for now the remaining budget is equal to the inital budget." ] }, { "cell_type": "markdown", "id": "a05e25b0-4ece-45b7-af37-cb3d5c987bac", "metadata": {}, "source": [ "## Step 4: Use DP libraries to analyse the dataset\n", "Available DP libraires are:\n", "- Smartnoise-SQL for SQL-like queries\n", "- Smartnoise-Synth for generating synthetic datasets\n", "- OpenDP for summary statistics\n", "- DiffPrivLib for training Machine Learning models" ] }, { "cell_type": "markdown", "id": "7533c5e9-b937-41c7-8271-2a9d46e8d228", "metadata": {}, "source": [ "For each library, there are three possibilities: \n", "- estimate the cost of a query (will NOT spend privacy loss budget)\n", "- query on a 'dummy' dataset (explained below) (will NOT spend privacy loss budget)\n", "- query on the private dataset (WILL SPEND PRIVACY LOSS BUDGET)" ] }, { "cell_type": "markdown", "id": "dd5d21f0-73de-426d-b25b-7e991787b7af", "metadata": {}, "source": [ "### a. Compute average bill length with Smartnoise-SQL" ] }, { "cell_type": "markdown", "id": "42df0cae-1a9e-4c4b-ba0f-beac838d8826", "metadata": {}, "source": [ "Dr. Antartica wants to know the average bill length of penguins. Therefore, she will use `smartnoise-sql` library and write the associated SQL command." ] }, { "cell_type": "code", "execution_count": 11, "id": "a0de0cfa-af54-46f7-9144-8778fb1a66c5", "metadata": {}, "outputs": [], "source": [ "# Average bill length in mm\n", "QUERY = \"SELECT AVG(bill_length_mm) AS avg_bill_length_mm FROM df\"" ] }, { "cell_type": "markdown", "id": "211510aa-b4ce-4cfa-9ffb-4a540b5e0c49", "metadata": {}, "source": [ "#### Estimate cost of a query with smartnoise-sql\n", "She will then estimate the cost of this query. In the various DP librairies the budget that will by used by a query in the server might be slightly different than what is asked by the user in inptu. The `estimate cost` function of each library returns the cost that will effectively be sent and deduced if the query is applied on the sensitive dataset.\n", "\n", "The user can then decide to use the budget or modify it. Again, of course, this will not impact the user's budget.\n", "\n", "Dr. Antartica checks the budget that computing the average bill length will really cost her if she asks the query with an `epsilon` and a `delta`." ] }, { "cell_type": "code", "execution_count": 12, "id": "47663a1f-2b91-4f8a-8565-b3d7c9667e76", "metadata": {}, "outputs": [], "source": [ "EPSILON = 0.5\n", "DELTA = 1e-4" ] }, { "cell_type": "code", "execution_count": 13, "id": "e1a2b948-cf11-4325-a05e-147a0b4aaa30", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "CostResponse(epsilon=1.0, delta=4.999999999999449e-05)" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost = client.smartnoise_sql.cost(\n", " query = QUERY, \n", " epsilon = EPSILON, \n", " delta = DELTA\n", ")\n", "cost" ] }, { "cell_type": "code", "execution_count": 14, "id": "4547b70f-0623-4ae6-93f1-9eaca724e514", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "This query would actually cost her 1.0 epsilon and 4.999999999999449e-05 delta.\n" ] } ], "source": [ "print(f\"This query would actually cost her {cost.epsilon} epsilon and {cost.delta} delta.\")" ] }, { "cell_type": "markdown", "id": "a93f7c2b-e30a-4be5-bf56-ad61f2834673", "metadata": {}, "source": [ "She decides that it is good enough." ] }, { "cell_type": "markdown", "id": "98e6fda2-dde7-4f8b-a787-c9a1e3571ebe", "metadata": {}, "source": [ "#### Query average bill length on dummy dataset with smartnoise-sql\n", "She now wants to start querying the real dataset for her research. \n", "\n", "However, her budget is limited and it would be a waste to spend it by mistake on a coding error. Therefore the client/server pipeline has functionnal testing capabilities for the users. It is possible to test a query on a `dummy` dataset to ensure that everything is working properly. Dr. Antartica will not be able to use the results of a dummy query for her analysis (as the data is random) but if the query on the dummy dataset works, she can be confident that her query will also work on the real dataset.\n", "This functionnal testing on the dummy does not have any impact on the budget as it is on random data only.\n", "\n", "To test on the dummy data instead of the real data, the function call is exactly the same with the only exception of the flag `dummy=True`. In the following cell, she will test with `smartnoise_query` but it is the same flag for `opendp.query`. She can optionnaly give two additional parameters to set the seed and the number of rows of the dummy dataset.\n", "\n", "Another more advanced possibility for functionnal tests with the dummy is to compare results of queries on a local dummy and the remote dummy with a very high budget: \n", "- create a local dummy on the notebook with a specific seed and number of rows\n", "- compute locally the wanted query on this local dummy with python functions like numpy\n", "- query the server on the same remote dummy with (`dummy=True`, same seed and same number of row) and a very big buget to limit noise as much as possible (don't worry this won't cost any real budget)\n", "- compare and verify that the local and remote dummy have similar results." ] }, { "cell_type": "markdown", "id": "d1f8ea18-ccab-4f75-9490-b4d1144b39db", "metadata": {}, "source": [ "Dr. Antartica will follow the best practice and now try the query to get the average bill length (in mm) on the dummy dataset. She does not forget to \n", "- set the `dummy` flag to True\n", "- set very high budget values to be able to compare results with a similar local dummy (with the same seed and number of rows) if she wants to verify that the function do what is expected. Here she will just check that the number of rows is close to what she sets as parameter." ] }, { "cell_type": "code", "execution_count": 15, "id": "90cf2a6d", "metadata": {}, "outputs": [], "source": [ "# On the remote server dummy dataframe\n", "dummy_res = client.smartnoise_sql.query(\n", " query = QUERY,\n", " epsilon = 100.0,\n", " delta = 0.99,\n", " dummy = True, \n", " nb_rows = NB_ROWS,\n", " seed = SEED\n", ")" ] }, { "cell_type": "code", "execution_count": 16, "id": "a30f277e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Average bill length in remote dummy: 48.58mm.\n" ] } ], "source": [ "print(f\"Average bill length in remote dummy: {np.round(dummy_res.result.df['avg_bill_length_mm'][0], 2)}mm.\")" ] }, { "cell_type": "markdown", "id": "167e8c6d-6c93-4ab4-9ba7-bf7e783a6bc2", "metadata": {}, "source": [ "No functionnal errors happened and the average bill length is within reasonable bounds. She is now even more confident in using her query on the server." ] }, { "cell_type": "markdown", "id": "e5379edf", "metadata": {}, "source": [ "#### Query average bill length on private dataset with smartnoise-sql\n", "Now that all the safeguard functions were tested, Dr. Antartica is ready to query on the real dataset and get a differentially private response of the number of penguins and average bill length. By default, the flag `dummy` is False so setting it is optional. She uses the values of `epsilon` and `delta` that she selected just before.\n", "\n", "Careful: This command DOES spend the budget of the user and the remaining budget is updated for every query." ] }, { "cell_type": "code", "execution_count": 17, "id": "19e60263", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "RemainingBudgetResponse(remaining_epsilon=9.0, remaining_delta=0.004950000000000006)" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.get_remaining_budget()" ] }, { "cell_type": "code", "execution_count": 18, "id": "69767fac", "metadata": {}, "outputs": [], "source": [ "response = client.smartnoise_sql.query(\n", " query = QUERY, \n", " epsilon = EPSILON, \n", " delta = DELTA,\n", " dummy = False # APPLIED ON SENSITIVE DATA, WILL SPEND BUDGET\n", ")" ] }, { "cell_type": "code", "execution_count": 19, "id": "6dbbdf93", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Average bill length of penguins in real data: 43.52mm.\n" ] } ], "source": [ "avg_bill_length = np.round(response.result.df['avg_bill_length_mm'].iloc[0], 2)\n", "print(f\"Average bill length of penguins in real data: {avg_bill_length}mm.\")" ] }, { "cell_type": "markdown", "id": "b2767e65", "metadata": {}, "source": [ "After each query on the real dataset, the budget informations are also returned to the researcher. It is possible possible to check the remaining budget again afterwards:" ] }, { "cell_type": "code", "execution_count": 20, "id": "39701fe5", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "RemainingBudgetResponse(remaining_epsilon=8.0, remaining_delta=0.004900000000000011)" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.get_remaining_budget()" ] }, { "cell_type": "markdown", "id": "e37c587f", "metadata": {}, "source": [ "As can be seen in `get_total_spent_budget()`, it is the budget estimated with `estimate_smartnoise_cost()` that was spent." ] }, { "cell_type": "code", "execution_count": 21, "id": "487f835f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "SpentBudgetResponse(total_spent_epsilon=2.0, total_spent_delta=9.999999999998899e-05)" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "client.get_total_spent_budget()" ] }, { "cell_type": "markdown", "id": "eef4afcd", "metadata": {}, "source": [ "Dr. Antartica has now a differentially private estimation of the number of penguins in the dataset and is confident to use the library for the rest of her analyses." ] }, { "cell_type": "markdown", "id": "04929993", "metadata": {}, "source": [ "### b. Compute confidence interval with opendp" ] }, { "cell_type": "code", "execution_count": 22, "id": "b9685226", "metadata": {}, "outputs": [], "source": [ "import opendp as dp\n", "import opendp.transformations as trans\n", "import opendp.measurements as meas" ] }, { "cell_type": "markdown", "id": "9d41bd58", "metadata": {}, "source": [ "She now wants the confidence interval of bill length in mm. She already has the number of penguins and the average from the metadata and previous smartnoise-sql queries respectively. She now needs the variance value.\n", "\n", "#### Prepare opendp pipeline and verify on dummy\n", "She checks the metadata of the columns again to use the relevant values in the pipeline." ] }, { "cell_type": "code", "execution_count": 23, "id": "4331d86f", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'species': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'string',\n", " 'cardinality': 3,\n", " 'categories': ['Adelie', 'Chinstrap', 'Gentoo']},\n", " 'island': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'string',\n", " 'cardinality': 3,\n", " 'categories': ['Torgersen', 'Biscoe', 'Dream']},\n", " 'bill_length_mm': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 30.0,\n", " 'upper': 65.0},\n", " 'bill_depth_mm': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 13.0,\n", " 'upper': 23.0},\n", " 'flipper_length_mm': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 150.0,\n", " 'upper': 250.0},\n", " 'body_mass_g': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'float',\n", " 'precision': 64,\n", " 'lower': 2000.0,\n", " 'upper': 7000.0},\n", " 'sex': {'private_id': False,\n", " 'nullable': False,\n", " 'max_partition_length': None,\n", " 'max_influenced_partitions': None,\n", " 'max_partition_contributions': None,\n", " 'type': 'string',\n", " 'cardinality': 2,\n", " 'categories': ['MALE', 'FEMALE']}}" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "penguin_metadata[\"columns\"]" ] }, { "cell_type": "markdown", "id": "f90e0425", "metadata": {}, "source": [ "She can define the columns names and the bounds of the relevant column." ] }, { "cell_type": "code", "execution_count": 24, "id": "ff8cb7b6", "metadata": {}, "outputs": [], "source": [ "columns = list(penguin_metadata[\"columns\"].keys())" ] }, { "cell_type": "code", "execution_count": 25, "id": "70b2bdb1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(30.0, 65.0)" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bill_length_min = penguin_metadata['columns']['bill_length_mm']['lower']\n", "bill_length_max = penguin_metadata['columns']['bill_length_mm']['upper']\n", "bill_length_min, bill_length_max" ] }, { "cell_type": "markdown", "id": "e93ae087", "metadata": {}, "source": [ "She can now define the pipeline of the transformation to have the variance that she wants on the data:" ] }, { "cell_type": "code", "execution_count": 26, "id": "75e4933b", "metadata": {}, "outputs": [], "source": [ "bill_length_transformation_pipeline = (\n", " trans.make_split_dataframe(separator=\",\", col_names=columns) >>\n", " trans.make_select_column(key=\"bill_length_mm\", TOA=str) >>\n", " trans.then_cast_default(TOA=float) >>\n", " trans.then_clamp(bounds=(bill_length_min, bill_length_max)) >>\n", " trans.then_resize(size=NB_PENGUINS, constant=avg_bill_length) >>\n", " trans.then_variance()\n", ")" ] }, { "cell_type": "markdown", "id": "411d464c", "metadata": {}, "source": [ "However, when she tries to execute it on the server, she has an error (see below). " ] }, { "cell_type": "code", "execution_count": 27, "id": "8041a647", "metadata": {}, "outputs": [ { "ename": "ValidationError", "evalue": "1 validation error for tagged-union[InvalidQueryExceptionModel,ExternalLibraryExceptionModel,UnauthorizedAccessExceptionModel,InternalServerExceptionModel]\n JSON input should be string, bytes or bytearray [type=json_type, input_value={'type': 'InvalidQueryExc...cessed in this server.'}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.9/v/json_type", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValidationError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[27], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# No instruction for noise addition mechanism: Expect to fail !!!\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mclient\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopendp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mquery\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mopendp_pipeline\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mbill_length_transformation_pipeline\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mdummy\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\n\u001b[1;32m 5\u001b[0m \u001b[43m)\u001b[49m\n", "File \u001b[0;32m/code/lomas_client/libraries/opendp.py:105\u001b[0m, in \u001b[0;36mOpenDPClient.query\u001b[0;34m(self, opendp_pipeline, fixed_delta, dummy, nb_rows, seed)\u001b[0m\n\u001b[1;32m 102\u001b[0m body \u001b[38;5;241m=\u001b[39m request_model\u001b[38;5;241m.\u001b[39mmodel_validate(body_dict)\n\u001b[1;32m 103\u001b[0m res \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mhttp_client\u001b[38;5;241m.\u001b[39mpost(endpoint, body)\n\u001b[0;32m--> 105\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mvalidate_model_response\u001b[49m\u001b[43m(\u001b[49m\u001b[43mres\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mQueryResponse\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m/code/lomas_client/utils.py:90\u001b[0m, in \u001b[0;36mvalidate_model_response\u001b[0;34m(response, response_model)\u001b[0m\n\u001b[1;32m 87\u001b[0m r_model \u001b[38;5;241m=\u001b[39m response_model\u001b[38;5;241m.\u001b[39mmodel_validate_json(data)\n\u001b[1;32m 88\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m r_model\n\u001b[0;32m---> 90\u001b[0m \u001b[43mraise_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 91\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n", "File \u001b[0;32m/code/lomas_client/utils.py:31\u001b[0m, in \u001b[0;36mraise_error\u001b[0;34m(response)\u001b[0m\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mraise_error\u001b[39m(response: requests\u001b[38;5;241m.\u001b[39mResponse) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mstr\u001b[39m:\n\u001b[1;32m 23\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Raise error message based on the HTTP response.\u001b[39;00m\n\u001b[1;32m 24\u001b[0m \n\u001b[1;32m 25\u001b[0m \u001b[38;5;124;03m Args:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[38;5;124;03m Server Error\u001b[39;00m\n\u001b[1;32m 30\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m---> 31\u001b[0m error_model \u001b[38;5;241m=\u001b[39m \u001b[43mLomasServerExceptionTypeAdapter\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate_json\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mjson\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 32\u001b[0m \u001b[38;5;28;01mmatch\u001b[39;00m error_model:\n\u001b[1;32m 33\u001b[0m \u001b[38;5;28;01mcase\u001b[39;00m InvalidQueryExceptionModel():\n", "File \u001b[0;32m/usr/local/lib/python3.12/site-packages/pydantic/type_adapter.py:135\u001b[0m, in \u001b[0;36m_frame_depth..wrapper..wrapped\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 132\u001b[0m \u001b[38;5;129m@wraps\u001b[39m(func)\n\u001b[1;32m 133\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapped\u001b[39m(\u001b[38;5;28mself\u001b[39m: TypeAdapterT, \u001b[38;5;241m*\u001b[39margs: P\u001b[38;5;241m.\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs: P\u001b[38;5;241m.\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m R:\n\u001b[1;32m 134\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_with_frame_depth(depth \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m1\u001b[39m): \u001b[38;5;66;03m# depth + 1 for the wrapper function\u001b[39;00m\n\u001b[0;32m--> 135\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m/usr/local/lib/python3.12/site-packages/pydantic/type_adapter.py:384\u001b[0m, in \u001b[0;36mTypeAdapter.validate_json\u001b[0;34m(self, data, strict, context)\u001b[0m\n\u001b[1;32m 368\u001b[0m \u001b[38;5;129m@_frame_depth\u001b[39m(\u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m 369\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mvalidate_json\u001b[39m(\n\u001b[1;32m 370\u001b[0m \u001b[38;5;28mself\u001b[39m, data: \u001b[38;5;28mstr\u001b[39m \u001b[38;5;241m|\u001b[39m \u001b[38;5;28mbytes\u001b[39m, \u001b[38;5;241m/\u001b[39m, \u001b[38;5;241m*\u001b[39m, strict: \u001b[38;5;28mbool\u001b[39m \u001b[38;5;241m|\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m, context: \u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any] \u001b[38;5;241m|\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 371\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m T:\n\u001b[1;32m 372\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Usage docs: https://docs.pydantic.dev/2.9/concepts/json/#json-parsing\u001b[39;00m\n\u001b[1;32m 373\u001b[0m \n\u001b[1;32m 374\u001b[0m \u001b[38;5;124;03m Validate a JSON string or bytes against the model.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 382\u001b[0m \u001b[38;5;124;03m The validated object.\u001b[39;00m\n\u001b[1;32m 383\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[0;32m--> 384\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidator\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalidate_json\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstrict\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstrict\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcontext\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcontext\u001b[49m\u001b[43m)\u001b[49m\n", "\u001b[0;31mValidationError\u001b[0m: 1 validation error for tagged-union[InvalidQueryExceptionModel,ExternalLibraryExceptionModel,UnauthorizedAccessExceptionModel,InternalServerExceptionModel]\n JSON input should be string, bytes or bytearray [type=json_type, input_value={'type': 'InvalidQueryExc...cessed in this server.'}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.9/v/json_type" ] } ], "source": [ "# No instruction for noise addition mechanism: Expect to fail !!!\n", "client.opendp.query(\n", " opendp_pipeline = bill_length_transformation_pipeline,\n", " dummy=True\n", ")" ] }, { "cell_type": "markdown", "id": "d06c59dc", "metadata": {}, "source": [ "This is because the server will only allow measurement pipeline with differentially private results. She adds Laplacian noise to the pipeline and should be able to instantiate the pipeline." ] }, { "cell_type": "code", "execution_count": 28, "id": "b8162859", "metadata": {}, "outputs": [], "source": [ "var_bill_length_measurement_pipeline = (\n", " bill_length_transformation_pipeline >>\n", " meas.then_laplace(scale=5.0) # Noise addition mechanism instructions\n", ")" ] }, { "cell_type": "markdown", "id": "fc7e0ecd", "metadata": {}, "source": [ "Now that there is a measurement, she is able to apply the pipeline on the dummy dataset of the server." ] }, { "cell_type": "code", "execution_count": 29, "id": "df61bce0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dummy result for variance: 42.6\n" ] } ], "source": [ "dummy_var_res = client.opendp.query(\n", " opendp_pipeline = var_bill_length_measurement_pipeline, \n", " dummy=True\n", ")\n", "print(f\"Dummy result for variance: {np.round(dummy_var_res.result.value, 2)}\")" ] }, { "cell_type": "markdown", "id": "d52fd242-d45c-4a83-9fd0-01896db3e3eb", "metadata": {}, "source": [ "#### Estimate cost with opendp" ] }, { "cell_type": "markdown", "id": "ded11ac4", "metadata": {}, "source": [ "With opendp, the function `opendp.cost` is particularly useful to estimate the used `epsilon` and `delta` based on the `scale` value." ] }, { "cell_type": "code", "execution_count": 30, "id": "7ae7f735", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "CostResponse(epsilon=0.7122093023265228, delta=0.0)" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost_res = client.opendp.cost(\n", " opendp_pipeline = var_bill_length_measurement_pipeline\n", ")\n", "cost_res" ] }, { "cell_type": "markdown", "id": "5e85bff9-1b40-4ef0-8fa4-3e504ebb916d", "metadata": {}, "source": [ "#### Execute pipeline on real dataset with opendp" ] }, { "cell_type": "markdown", "id": "1c791d36", "metadata": {}, "source": [ "She can now execute the query on the real dataset." ] }, { "cell_type": "code", "execution_count": 31, "id": "085555a5", "metadata": {}, "outputs": [], "source": [ "var_res = client.opendp.query(\n", " opendp_pipeline = var_bill_length_measurement_pipeline, \n", ")" ] }, { "cell_type": "code", "execution_count": 32, "id": "674332e7", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Variance of bill length: 35.06 (from opendp query).\n" ] } ], "source": [ "var_bill_length = np.round(var_res.result.value, 2)\n", "print(f\"Variance of bill length: {var_bill_length} (from opendp query).\")" ] }, { "cell_type": "markdown", "id": "ddb058f9-42fa-4896-a38f-3bf67fc0b2fb", "metadata": {}, "source": [ "#### Postprocessing: no additional privacy risk with DP" ] }, { "cell_type": "markdown", "id": "367081be-1159-45d8-9129-88fba20fb697", "metadata": {}, "source": [ "She can now do all the postprocessing that she wants with the returned data without adding any privacy risk. " ] }, { "cell_type": "code", "execution_count": 33, "id": "f72b19d0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Standard error of bill length: 0.32.\n" ] } ], "source": [ "# Get standard error\n", "standard_error = np.sqrt(var_bill_length / NB_PENGUINS)\n", "print(f\"Standard error of bill length: {np.round(standard_error, 2)}.\")" ] }, { "cell_type": "code", "execution_count": 34, "id": "62630a03", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The 95% confidence interval of the bill length of all penguins is [42.89, 44.15].\n" ] } ], "source": [ " # Compute the 95% confidence interval\n", "ZSCORE = 1.96\n", "lower_bound = np.round(avg_bill_length - ZSCORE * standard_error, 2)\n", "upper_bound = np.round(avg_bill_length + ZSCORE * standard_error, 2)\n", "print(f\"The 95% confidence interval of the bill length of all penguins is [{lower_bound}, {upper_bound}].\")" ] }, { "cell_type": "markdown", "id": "668a9790-4d73-45d0-a3d1-55f979f97cb0", "metadata": {}, "source": [ "### c. Train a DP Machine Learning model with DiffPrivLib" ] }, { "cell_type": "code", "execution_count": 35, "id": "9cbcd0cf-4211-4a55-aa71-0679e7b2fa63", "metadata": {}, "outputs": [], "source": [ "from sklearn.pipeline import Pipeline\n", "from diffprivlib import models\n", "import pandas as pd" ] }, { "cell_type": "markdown", "id": "780e56d9-7395-4396-bd2c-634e9bb82c63", "metadata": {}, "source": [ "She now wants a model to predict the species of a penguin based on bill depth. Therefore, she uses a Random Forest classifier from DiffPrivLib library." ] }, { "cell_type": "markdown", "id": "cf20b748-8d4b-4f78-8d11-7f47ada6dd5f", "metadata": {}, "source": [ "#### Prepare Random Forest Classifier pipeline on dummy with DiffPrivLib" ] }, { "cell_type": "code", "execution_count": 36, "id": "b91b694d-2256-4c43-ac4f-091c6afb290a", "metadata": {}, "outputs": [], "source": [ "feature_columns = ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']\n", "target_columns = ['species']" ] }, { "cell_type": "code", "execution_count": 37, "id": "0bf7ea1f-873c-4068-ae8b-edee16316a08", "metadata": {}, "outputs": [], "source": [ "def get_bounds(cols_metadata, columns):\n", " lower = [cols_metadata[col][\"lower\"] for col in columns]\n", " upper = [cols_metadata[col][\"upper\"] for col in columns]\n", " return (lower, upper)" ] }, { "cell_type": "code", "execution_count": 38, "id": "869d409c-1ee9-4eca-8189-976f844de284", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "([30.0, 13.0, 150.0, 2000.0], [65.0, 23.0, 250.0, 7000.0])" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bounds = get_bounds(penguin_metadata['columns'], columns=feature_columns)\n", "bounds" ] }, { "cell_type": "code", "execution_count": 39, "id": "6114c5f4-f8b1-4a8c-9770-e2a0ed7f180d", "metadata": {}, "outputs": [], "source": [ "dpl_pipeline = Pipeline([\n", " ('rf', models.RandomForestClassifier(\n", " n_estimators=10, \n", " epsilon = 2.0, \n", " bounds=bounds, \n", " classes=['Adelie', 'Chinstrap', 'Gentoo'])\n", " ),\n", "])" ] }, { "cell_type": "code", "execution_count": 40, "id": "389f3ca5-66c0-41d2-86bd-e7131bbe9184", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
Pipeline(steps=[('rf',\n",
       "                 RandomForestClassifier(accountant=BudgetAccountant(spent_budget=[(2.0, 0)]),\n",
       "                                        bounds=(array([  30.,   13.,  150., 2000.]),\n",
       "                                                array([  65.,   23.,  250., 7000.])),\n",
       "                                        classes=['Adelie', 'Chinstrap',\n",
       "                                                 'Gentoo'],\n",
       "                                        epsilon=2.0))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ], "text/plain": [ "Pipeline(steps=[('rf',\n", " RandomForestClassifier(accountant=BudgetAccountant(spent_budget=[(2.0, 0)]),\n", " bounds=(array([ 30., 13., 150., 2000.]),\n", " array([ 65., 23., 250., 7000.])),\n", " classes=['Adelie', 'Chinstrap',\n", " 'Gentoo'],\n", " epsilon=2.0))])" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dummy_response = client.diffprivlib.query(\n", " pipeline = dpl_pipeline,\n", " feature_columns = feature_columns,\n", " target_columns = target_columns,\n", " test_size = 0.2,\n", " test_train_split_seed = 1,\n", " dummy = True\n", ")\n", "model = dummy_response.result.model\n", "model" ] }, { "cell_type": "markdown", "id": "1875feea-17d0-4015-bfd0-de85200ee62c", "metadata": {}, "source": [ "#### Estimate budget of Linear Regression with DiffPrivLib" ] }, { "cell_type": "code", "execution_count": 41, "id": "e9f29610-52fc-4f0e-84fd-8d85cf52eea4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "CostResponse(epsilon=2.0, delta=0.0)" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cost_res = client.diffprivlib.cost(\n", " dpl_pipeline,\n", " feature_columns = feature_columns,\n", " target_columns = target_columns,\n", " test_size = 0.2,\n", " test_train_split_seed = 1\n", ")\n", "cost_res" ] }, { "cell_type": "markdown", "id": "3012d12c-c304-4bb2-bac1-18b55094ec07", "metadata": {}, "source": [ "#### Train random forest classifier on sensitive data with DiffPrivLib" ] }, { "cell_type": "code", "execution_count": 42, "id": "62538ac0-c6aa-4950-82e8-f53510f17d77", "metadata": {}, "outputs": [], "source": [ "response = client.diffprivlib.query(\n", " pipeline = dpl_pipeline,\n", " feature_columns = feature_columns,\n", " target_columns = target_columns,\n", " test_size = 0.1,\n", " test_train_split_seed = 1,\n", " dummy = False\n", ")" ] }, { "cell_type": "code", "execution_count": 43, "id": "7727b13a-cd2f-4f97-b550-88a4360fd601", "metadata": {}, "outputs": [], "source": [ "# Return the mean accuracy. \n", "model_score = response.result.score" ] }, { "cell_type": "code", "execution_count": 44, "id": "600d6c6e-7567-4564-92ad-d1538ac10af5", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The model has a mean accuracy of 0.32. It is a harsh metric because we are in a multi-label classification case.'" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "f\"The model has a mean accuracy of {np.round(model_score, 2)}. It is a harsh metric because we are in a multi-label classification case.\"" ] }, { "cell_type": "code", "execution_count": 45, "id": "1824eea1-6ad8-4d2f-86d5-456a89318fef", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
Pipeline(steps=[('rf',\n",
       "                 RandomForestClassifier(accountant=BudgetAccountant(spent_budget=[(2.0, 0)]),\n",
       "                                        bounds=(array([  30.,   13.,  150., 2000.]),\n",
       "                                                array([  65.,   23.,  250., 7000.])),\n",
       "                                        classes=['Adelie', 'Chinstrap',\n",
       "                                                 'Gentoo'],\n",
       "                                        epsilon=2.0))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" ], "text/plain": [ "Pipeline(steps=[('rf',\n", " RandomForestClassifier(accountant=BudgetAccountant(spent_budget=[(2.0, 0)]),\n", " bounds=(array([ 30., 13., 150., 2000.]),\n", " array([ 65., 23., 250., 7000.])),\n", " classes=['Adelie', 'Chinstrap',\n", " 'Gentoo'],\n", " epsilon=2.0))])" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model = response.result.model\n", "model" ] }, { "cell_type": "code", "execution_count": 46, "id": "1e4a56bd-95bf-4355-81a6-eaf7d16f69c8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'For these feature values, the predicted species is is Adelie.'" ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ "x_to_predict = pd.DataFrame({\n", " 'bill_length_mm': [30.0], 'bill_depth_mm': [20.0], 'flipper_length_mm': [170.0], 'body_mass_g': [5000.0]\n", "})\n", "predictions = model.predict(x_to_predict)[0]\n", "f\"For these feature values, the predicted species is is {predictions}.\"" ] }, { "cell_type": "markdown", "id": "09cb0eff-9864-42a3-a87a-cef2f7eca216", "metadata": {}, "source": [ "### d. Get a Synthetic Dataset with Smartnoise-Synth" ] }, { "cell_type": "markdown", "id": "790fe272-ae04-4daf-933e-6a9d86ac691f", "metadata": {}, "source": [ "Finally she gets a synthetic dataset to do the rest of her analysis. She chooses to only train on a subset on 3 columns: \"island\", \"bill_length_mm\" and \"bill_depth_mm\" but if we wanted she could train on the whole dataset.\n", "She also decides to use the `patectgan` synthesizer and keep all other default parameters." ] }, { "cell_type": "markdown", "id": "ded384a2-dcf6-43af-98b0-ae058b2a03ba", "metadata": {}, "source": [ "#### Train patectgan synthesizer on dummy data with Smartnoise-Synth" ] }, { "cell_type": "code", "execution_count": 47, "id": "ed292cec-8497-4e5b-b3bf-cca9227abf7d", "metadata": {}, "outputs": [], "source": [ "res_dummy = client.smartnoise_synth.query(\n", " synth_name=\"patectgan\",\n", " select_cols = [\"island\", \"bill_length_mm\", \"bill_depth_mm\"],\n", " epsilon=1.0,\n", " dummy=True,\n", ")\n", "dummy_synth_df = res_dummy.result.df_samples" ] }, { "cell_type": "code", "execution_count": 48, "id": "6fab49d9-f5c5-437e-b70f-234b84c2f7e5", "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", "
islandbill_length_mmbill_depth_mm
0Dream59.18928318.603612
1Dream45.89374617.438823
2Dream48.17508816.088652
3Dream57.95833817.529872
4Dream51.72294319.493872
\n", "
" ], "text/plain": [ " island bill_length_mm bill_depth_mm\n", "0 Dream 59.189283 18.603612\n", "1 Dream 45.893746 17.438823\n", "2 Dream 48.175088 16.088652\n", "3 Dream 57.958338 17.529872\n", "4 Dream 51.722943 19.493872" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dummy_synth_df.head()" ] }, { "cell_type": "markdown", "id": "f75ef712-3d58-4548-904e-6f1da72493f7", "metadata": {}, "source": [ "#### Estimate cost of training patectgan synthesizer with Smartnoise-Synth" ] }, { "cell_type": "code", "execution_count": 49, "id": "82ab4aee-b4af-4fbd-93cc-b8171f7f9a52", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "CostResponse(epsilon=1.0, delta=0.00015673368198174188)" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "res_cost = client.smartnoise_synth.cost(\n", " synth_name=\"patectgan\",\n", " epsilon=1.0,\n", " select_cols = [\"island\", \"bill_length_mm\", \"bill_depth_mm\"],\n", ")\n", "res_cost" ] }, { "cell_type": "markdown", "id": "804e2119-1513-45b2-a989-f5566e6c7bf4", "metadata": {}, "source": [ "#### Train patectgan synthesizer on private data with Smartnoise-Synth" ] }, { "cell_type": "code", "execution_count": 50, "id": "fbc8b354-e4db-4472-957b-468e768eddc4", "metadata": {}, "outputs": [], "source": [ "res = client.smartnoise_synth.query(\n", " synth_name=\"patectgan\",\n", " select_cols = [\"island\", \"bill_length_mm\", \"bill_depth_mm\"],\n", " epsilon=1.0,\n", " dummy=False,\n", ")\n", "synth_df = res.result.df_samples" ] }, { "cell_type": "code", "execution_count": 51, "id": "550fa89d-9537-4daf-9f96-42fa71f242b9", "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", "
islandbill_length_mmbill_depth_mm
0Dream52.05349315.699640
1Dream47.70572817.678879
2Torgersen56.87572716.196799
3Biscoe38.80793719.253387
4Dream46.33247715.361980
\n", "
" ], "text/plain": [ " island bill_length_mm bill_depth_mm\n", "0 Dream 52.053493 15.699640\n", "1 Dream 47.705728 17.678879\n", "2 Torgersen 56.875727 16.196799\n", "3 Biscoe 38.807937 19.253387\n", "4 Dream 46.332477 15.361980" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "synth_df.head()" ] }, { "cell_type": "markdown", "id": "b3ae2259-3d83-46c6-a59c-9df178d98dc5", "metadata": {}, "source": [ "Out of curiosity, she checks the average bill length and variance of bill length on this dataset." ] }, { "cell_type": "code", "execution_count": 52, "id": "7d5a336e-80f0-48fa-84a3-33d0e51a2d3b", "metadata": {}, "outputs": [], "source": [ "synth_mean = np.round(synth_df[\"bill_length_mm\"].mean(), 2)\n", "synth_variance = np.round(synth_df[\"bill_length_mm\"].var(), 2)" ] }, { "cell_type": "code", "execution_count": 53, "id": "e890a8d9-0c7b-4805-be8a-81e66d5fa7ca", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The average with Smartnoise-SQL on private data was 43.52.\n", "The average with Smartnoise-Synth on synthetic data is 44.53.\n" ] } ], "source": [ "print(\n", " f\"The average with Smartnoise-SQL on private data was {avg_bill_length}.\\n\"\n", " + f\"The average with Smartnoise-Synth on synthetic data is {synth_mean}.\"\n", ")" ] }, { "cell_type": "code", "execution_count": 54, "id": "11a14d9f-0fe3-4a5e-b425-d2192acd1e84", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The variance with opendp on private data was 35.06.\n", "The variance with Smartnoise-Synth on synthetic data is 36.25.\n" ] } ], "source": [ "print(\n", " f\"The variance with opendp on private data was {var_bill_length}.\\n\"\n", " + f\"The variance with Smartnoise-Synth on synthetic data is {synth_variance}.\"\n", ")" ] }, { "cell_type": "markdown", "id": "94eaf59b-c108-424c-8978-b1c86e141ccb", "metadata": {}, "source": [ "## Step 4: See archives of queries" ] }, { "cell_type": "markdown", "id": "64003c53-de56-4bdc-a3c2-0c3e40031919", "metadata": {}, "source": [ "She now wants to verify all the queries that she did on the real data. It is possible because an archive of all queries is kept in a secure database. With a function call she can see her queries, budget and associated responses." ] }, { "cell_type": "code", "execution_count": 55, "id": "008fd230-cdfd-4e03-91ce-5a60b06c106d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 55, "metadata": {}, "output_type": "execute_result" } ], "source": [ "previous_queries = client.get_previous_queries()\n", "len(previous_queries)" ] }, { "cell_type": "code", "execution_count": 56, "id": "b712b269-64f2-4c7e-b8bf-d1a608933eff", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'user_name': 'Dr. Antartica',\n", " 'dataset_name': 'PENGUIN',\n", " 'dp_librairy': 'smartnoise_sql',\n", " 'client_input': {'dataset_name': 'PENGUIN',\n", " 'query_str': 'SELECT AVG(bill_length_mm) AS avg_bill_length_mm FROM df',\n", " 'epsilon': 0.5,\n", " 'delta': 0.0001,\n", " 'mechanisms': {},\n", " 'postprocess': True},\n", " 'response': {'epsilon': 1.0,\n", " 'delta': 4.999999999999449e-05,\n", " 'requested_by': 'Dr. Antartica',\n", " 'result': {'res_type': 'smartnoise_sql',\n", " 'df': {'index': [0],\n", " 'columns': ['avg_bill_length_mm'],\n", " 'data': [[43.75587056284081]],\n", " 'index_names': [None],\n", " 'column_names': [None]}}},\n", " 'timestamp': 1732024136.80644}" ] }, "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Smartnoise-SQL\n", "avg_bill_length_query = previous_queries[0]\n", "avg_bill_length_query" ] }, { "cell_type": "code", "execution_count": 57, "id": "8dfaf2b6-2b6c-480b-bcd7-250b0b2806a6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'user_name': 'Dr. Antartica',\n", " 'dataset_name': 'PENGUIN',\n", " 'dp_librairy': 'smartnoise_sql',\n", " 'client_input': {'dataset_name': 'PENGUIN',\n", " 'query_str': 'SELECT AVG(bill_length_mm) AS avg_bill_length_mm FROM df',\n", " 'epsilon': 0.5,\n", " 'delta': 0.0001,\n", " 'mechanisms': {},\n", " 'postprocess': True},\n", " 'response': {'epsilon': 1.0,\n", " 'delta': 4.999999999999449e-05,\n", " 'requested_by': 'Dr. Antartica',\n", " 'result': {'res_type': 'smartnoise_sql',\n", " 'df': {'index': [0],\n", " 'columns': ['avg_bill_length_mm'],\n", " 'data': [[43.52246114623609]],\n", " 'index_names': [None],\n", " 'column_names': [None]}}},\n", " 'timestamp': 1732024185.9737833}" ] }, "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# OpenDP\n", "var_bill_length_query = previous_queries[1]\n", "var_bill_length_query" ] }, { "cell_type": "code", "execution_count": 58, "id": "376315ec-6f38-4919-959e-d6bf244a4952", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'user_name': 'Dr. Antartica',\n", " 'dataset_name': 'PENGUIN',\n", " 'dp_librairy': 'opendp',\n", " 'client_input': {'dataset_name': 'PENGUIN',\n", " 'opendp_json': Measurement(\n", " input_domain = AtomDomain(T=String),\n", " input_metric = SymmetricDistance(),\n", " output_measure = MaxDivergence(f64)),\n", " 'fixed_delta': None},\n", " 'response': {'epsilon': 0.7122093023265228,\n", " 'delta': 0.0,\n", " 'requested_by': 'Dr. Antartica',\n", " 'result': {'res_type': 'opendp', 'value': 35.063144457712596}},\n", " 'timestamp': 1732024233.7957816}" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# DiffPrivLib\n", "reg_bill_length_query = previous_queries[2]\n", "reg_bill_length_query" ] }, { "cell_type": "code", "execution_count": 59, "id": "638817e0-d88f-407a-8136-210309651cf2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'user_name': 'Dr. Antartica',\n", " 'dataset_name': 'PENGUIN',\n", " 'dp_librairy': 'diffprivlib',\n", " 'client_input': {'dataset_name': 'PENGUIN',\n", " 'diffprivlib_json': '{\"module\": \"diffprivlib\", \"version\": \"0.6.5\", \"pipeline\": [{\"type\": \"_dpl_type:RandomForestClassifier\", \"name\": \"rf\", \"params\": {\"n_estimators\": 10, \"n_jobs\": 1, \"random_state\": null, \"verbose\": 0, \"warm_start\": false, \"max_depth\": 5, \"epsilon\": 2.0, \"bounds\": {\"_tuple\": true, \"_items\": [[30.0, 13.0, 150.0, 2000.0], [65.0, 23.0, 250.0, 7000.0]]}, \"classes\": [\"Adelie\", \"Chinstrap\", \"Gentoo\"], \"shuffle\": false, \"accountant\": \"_dpl_instance:BudgetAccountant\"}}]}',\n", " 'feature_columns': ['bill_length_mm',\n", " 'bill_depth_mm',\n", " 'flipper_length_mm',\n", " 'body_mass_g'],\n", " 'target_columns': ['species'],\n", " 'test_size': 0.1,\n", " 'test_train_split_seed': 1,\n", " 'imputer_strategy': 'drop'},\n", " 'response': {'epsilon': 2.0,\n", " 'delta': 0.0,\n", " 'requested_by': 'Dr. Antartica',\n", " 'result': {'res_type': 'diffprivlib',\n", " 'score': 0.3235294117647059,\n", " 'model': Pipeline(steps=[('rf',\n", " RandomForestClassifier(accountant=BudgetAccountant(spent_budget=[(2.0, 0)]),\n", " bounds=(array([ 30., 13., 150., 2000.]),\n", " array([ 65., 23., 250., 7000.])),\n", " classes=['Adelie', 'Chinstrap',\n", " 'Gentoo'],\n", " epsilon=2.0))])}},\n", " 'timestamp': 1732024235.0577705}" ] }, "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Smartnoise-Synth\n", "sysynth_query = previous_queries[3]\n", "sysynth_query" ] }, { "cell_type": "code", "execution_count": null, "id": "416d3afc-6455-4252-b883-2b6984233513", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "code", "execution_count": null, "id": "70cafc60-9ca5-46ca-83d9-2077a22a53dd", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.7" } }, "nbformat": 4, "nbformat_minor": 5 }