diff --git a/examples/quickstart.ipynb b/examples/quickstart.ipynb new file mode 100644 index 00000000..dd52a036 --- /dev/null +++ b/examples/quickstart.ipynb @@ -0,0 +1,405 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "PipelineDP Quick Start ", + "provenance": [], + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "bW1gifIe0pUt" + }, + "source": [ + "\n", + " \n", + " \n", + "
\n", + " Run in Google Colab\n", + " \n", + " View source on GitHub\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "3Pa1EeIdJyZn" + }, + "source": [ + "This is a simple example that shows how to calculate anonymized statistics using PipelineDP. The input data is a simulated dataset of visits to some restaurant during a 7 day period. Each visit is characterized by a visitor ID, the entry date, and the amount of money spent. In this colab we use Pipeline DP\n", + "Core API to calculate the count of restaurant visits per day.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zxcPpZGuAPq8" + }, + "source": [ + "# Install dependencies and download data\n", + "\n", + "Run the code below to install the necessary dependencies, load and explore the input data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "E8yzpKYNbHTF", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 206 + }, + "outputId": "0e60ad12-094a-4e0d-9c44-d8377accc47c", + "cellView": "form" + }, + "outputs": [ + { + "output_type": "execute_result", + "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", + "
user_identer_timespent_minutesspent_moneyday
05809:27AM29171
112159:16AM45181
244811:55AM12161
312510:47AM27201
448411:08AM35131
\n", + "
\n", + " \n", + " \n", + " \n", + "\n", + " \n", + "
\n", + "
\n", + " " + ], + "text/plain": [ + " user_id enter_time spent_minutes spent_money day\n", + "0 580 9:27AM 29 17 1\n", + "1 1215 9:16AM 45 18 1\n", + "2 448 11:55AM 12 16 1\n", + "3 125 10:47AM 27 20 1\n", + "4 484 11:08AM 35 13 1" + ] + }, + "metadata": {}, + "execution_count": 1 + } + ], + "source": [ + "#@markdown Install dependencies and download data\n", + "\n", + "import os\n", + "os.chdir('/content')\n", + "!git clone https://github.com/OpenMined/PipelineDP.git\n", + "!pip install -r PipelineDP/requirements.dev.txt\n", + "\n", + "import sys\n", + "sys.path.insert(0,'/content/PipelineDP')\n", + "\n", + "#Download restaurant dataset from github\n", + "!wget https://raw.githubusercontent.com/google/differential-privacy/main/examples/go/data/week_data.csv\n", + "\n", + "from IPython.display import clear_output\n", + "clear_output()\n", + "\n", + "import apache_beam as beam\n", + "from apache_beam.runners.portability import fn_api_runner\n", + "from apache_beam.runners.interactive import interactive_runner\n", + "from apache_beam.runners.interactive.interactive_beam import *\n", + "import pyspark\n", + "from dataclasses import dataclass\n", + "import pipeline_dp\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "df = pd.read_csv('week_data.csv')\n", + "df.rename(inplace=True, columns={'VisitorId' : 'user_id', 'Time entered' : 'enter_time', 'Time spent (minutes)' : 'spent_minutes', 'Money spent (euros)' : 'spent_money', 'Day' : 'day'})\n", + "rows = [index_row[1] for index_row in df.iterrows()]\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "source": [ + "# Run the pipeline" + ], + "metadata": { + "id": "hzPiLxByC5BJ" + } + }, + { + "cell_type": "code", + "source": [ + "# Set the backend to local backend. Other options (Beam or Spark)\n", + "# are possible.\n", + "backend = pipeline_dp.LocalBackend()\n", + "\n", + "# Define the total budget.\n", + "budget_accountant = pipeline_dp.NaiveBudgetAccountant(total_epsilon=1, total_delta=1e-6)\n", + "\n", + "# Create DPEngine which will execute the logic.\n", + "dp_engine = pipeline_dp.DPEngine(budget_accountant, backend)\n", + "\n", + "# Define privacy ID, partition key and aggregated value extractors.\n", + "# The aggregated value extractor isn't used in this example.\n", + "data_extractors = pipeline_dp.DataExtractors(\n", + " partition_extractor=lambda row: row.day,\n", + " privacy_id_extractor=lambda row: row.user_id,\n", + " value_extractor=lambda row: 1)\n", + "\n", + "# Configure the aggregation parameters.\n", + "params = pipeline_dp.AggregateParams(\n", + " noise_kind=pipeline_dp.NoiseKind.LAPLACE,\n", + " # This example computes only count but we can compute multiple\n", + " # ... metrics at once.\n", + " metrics=[pipeline_dp.Metrics.COUNT],\n", + " # Limits visits contributed by a visitor. A visitor can contribute to\n", + " # ... up to 3 days \n", + " max_partitions_contributed=3,\n", + " # ... and up to 2 visits per day. \n", + " max_contributions_per_partition=2,\n", + " # Configure the output partition keys as they are publicly known.\n", + " # The output should include all week days.\n", + " public_partitions=list(range(1, 8)))\n", + "\n", + "# Create a computational graph for the aggregation.\n", + "# All computations are lazy. dp_result is iterable, but iterating it would\n", + "# fail until budget is computed (below).\n", + "# It’s possible to call DPEngine.aggregate multiple times with different\n", + "# metrics to compute.\n", + "dp_result = dp_engine.aggregate(rows, params, data_extractors)\n", + "\n", + "# Compute budget per each DP operation. \n", + "budget_accountant.compute_budgets()\n", + "\n", + "# Here's where the lazy iterator initiates computations and gets transformed\n", + "# into actual results\n", + "dp_result = list(dp_result)\n" + ], + "metadata": { + "id": "rFj2u61qBx0r" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "# Inspect the result" + ], + "metadata": { + "id": "hfHqnCLcDqpU" + } + }, + { + "cell_type": "code", + "source": [ + "#@markdown ##Inspect the result\n", + "#@markdown Below you can see the DP and non-DP results.\n", + "\n", + "# Compute non-DP result\n", + "non_dp_count = [0] * 7\n", + "days = range(1, 7)\n", + "for row in rows:\n", + " index = row['day'] - 1\n", + " non_dp_count[index] += 1\n", + "\n", + "# Copy the DP result to a list\n", + "dp_count = [0] * 7 \n", + "for count_sum_per_day in dp_result:\n", + " index = count_sum_per_day[0] - 1\n", + " dp_count[index] = count_sum_per_day[1][0]\n", + "\n", + "days = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n", + "x = np.arange(len(days))\n", + "\n", + "width = 0.35\n", + "fig, ax = plt.subplots()\n", + "rects1 = ax.bar(x - width/2, non_dp_count, width, label='non-DP')\n", + "rects2 = ax.bar(x + width/2, dp_count, width, label='DP')\n", + "ax.set_ylabel('Visit count')\n", + "ax.set_title('Count visits per day')\n", + "ax.set_xticks(x)\n", + "ax.set_xticklabels(days)\n", + "ax.legend()\n", + "fig.tight_layout()\n", + "plt.show()\n", + "\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 297 + }, + "id": "sTkYZ0wSbo3h", + "outputId": "80ab959d-5a2a-4901-fe10-2b99c1bd090b", + "cellView": "form" + }, + "execution_count": null, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + } + } + ] + } + ] +}